/* ==========================================================================
   UtilityBox v2 — customer UI
   Dark premium. Committed single theme: the dark ground is the identity.
   Motif: the prepaid meter LCD. Amber = live current, teal = credit.
   Self-contained. No Bootstrap, Metronic, Framework7 or jQuery Mobile.

   All colour lives in the :root tokens below, so swapping the brand palette
   is a single-block edit — nothing downstream hardcodes a hex value.

   Created      2026-07-31   branch feature/v2-customer-ui
   Last changed 2026-07-31
   Change history: uBoxWebStatic2/Pages/v2/CHANGELOG.md
   ========================================================================== */

/*  Palette taken from the UtilityBox brand mark (2026-07-31).

    The logo is a purple-to-blue gradient U with an orange and red dot
    cluster, over a deep purple-black. So:

      brand      purple -> blue, used for the mark, focus and emphasis
      accent     orange, used for actions — it is the warmest thing in the
                 mark and the only colour that should read as "press this"
      readout    sky blue, for the token display; it sits inside the brand
                 range rather than fighting it

    The ground carries a violet bias rather than a blue one so the mark sits
    on a surface from its own family. Everything downstream reads these
    tokens; nothing hardcodes a hex value.  */
:root {
    /*  ---- ONE INSET FOR EVERY DISCLOSURE ARROW ----

        ⚠ EVERY ▾ ON THIS SITE MEASURES FROM THIS, so they cannot drift apart.
        The cards that carry them are stacked directly on top of one another,
        which makes a quarter of a rem of difference plainly visible — and that
        is exactly what happened: .ub-prof padded its summary 1rem and
        .ub-ownerbox positioned its arrow at .75rem, so "Your details" and the
        card below it were misaligned.

        Fixing that by writing 1rem in both places would have lined them up
        today and let them drift again the moment either card's padding
        changed. They now read the same variable, so changing this ONE value
        moves all of them together and they are aligned BY CONSTRUCTION rather
        than by two numbers that happen to match.

        Card padding uses it too, so the arrow always sits on the same margin
        as the text it belongs to. */
    --disc-inset: 1rem;

    /*  ---- ⚠ THE HOUSE RULE FOR EVERY DISCLOSURE ON THIS SITE ----

        ARROW ON THE LEFT, BEFORE THE HEADING. ▸ closed, ▾ open.

        It used to sit on the RIGHT, at the far end of the row. On a card that
        is the last place the eye reaches, so the control announcing "there is
        more inside this" was found after the customer had already decided
        there was not. On the left it is read before the heading it belongs to,
        which is the order it matters in.

        ▸ rather than ▾ when closed, because a right-pointing triangle says
        "this opens" while a down-pointing one reads as decoration until it
        moves. Rotating one glyph to the other keeps the two states obviously
        the same control.

        ⚠ EVERY PULL-DOWN ADDED FROM NOW ON USES .ub-disc. Do not hand-roll
        another marker: three different ones existed on this page at once and
        it read as three kinds of control rather than one repeated. */
    --disc-mark: .8125rem;

    /* Ground — violet-black, lifting to a bluer purple on raised surfaces,
       which is the lighter tone from the mark rather than a grey. --------- */
    --ink:       #0A0718;
    --ink-2:     #120D28;
    --surface:   #191138;
    --surface-2: #221847;
    --line:      #2C2158;
    --line-hi:   #3E2F79;

    /* Brand — the two purples of the mark. This carries the interface.
       `--amber` is kept as a NAME only so existing rules keep working; it
       resolves to the brand purple, not to orange. --------------------- */
    --brand:     #8B5CF6;   /* the U and "Box" */
    --brand-2:   #6366F1;   /* mid gradient */
    --brand-3:   #3B82F6;   /* blue end of the U */
    --brand-hi:  #A78BFA;   /* lighter purple, for hover and glow */
    --brand-dim: #2C2060;

    --amber:     #8B5CF6;
    --amber-hi:  #A78BFA;
    --amber-dim: #4C3A8F;

    /* Token readout — the blue end of the mark's own gradient ------------- */
    --lcd:       #60A5FA;
    --lcd-dim:   #131E44;
    --lcd-glow:  rgba(96, 165, 250, .35);

    /*  ---- SUCCESS GREEN, NOT THE READOUT BLUE ----
        Reported live 2026-08-12: the post-payment "Payment successful" box
        was blue, not green. .ub-warnbox--good had been built reusing --lcd,
        because --lcd was already the closest "good news" colour on this
        page and the readout theme was mid-flight — but --lcd means "this is
        a token display", not "this succeeded"; the electricity token itself
        also renders in --lcd, so a payment confirmation in the same blue
        read as one more piece of read-out data rather than a green light.
        #22C55E is not a new colour to this codebase: it is the exact green
        ubox-admin.css already uses for .uadm-pill--ok, .uadm-led--ok and the
        Commissions net-margin figures.

        ⚠ THEMED SINCE 2026-08-16, and it used to say here that it must NOT
        be. The argument was that "successful" should not shift shade with
        the ground it sits on — but the both-themes audit measured #22C55E
        as TEXT on a light card at ~2.1:1, which is a success message a
        customer cannot read. Green stays green in both themes; the light
        theme block simply carries a darker shade (#15803D), exactly the
        treatment --danger has always had. The admin keeps the flat
        #22C55E because the admin is pinned dark, where it reads fine. */
    --good:      #22C55E;
    --good-bg:   rgba(34, 197, 94, .15);

    /*  Spark — the orange and red of the dot cluster.

        On the mark these are six small dots and nothing more, so they are
        used the same way here: tiny status marks and counts only. Never a
        button, never a heading, never a surface. If it occupies more room
        than a full stop, it is being used wrongly.  */
    --spark:     #F97316;
    --spark-2:   #EF4444;

    /*  The dot cluster, promoted (2026-08-01).

        The mark has six warm dots beside the purple U, and the interface
        used none of them — so every card, button, figure and heading was
        purple on white and the whole thing read as one colour.

        These four are the accents. They are drawn from the mark itself
        (purple U, its blue gradient end, the orange and red of the
        cluster). They are still never a surface and never a button: they
        are a 3px rule over a tile, a dot beside a heading, a tint on one
        strip. Small marks that give the page more than one colour without
        any of them taking over.  */
    --dot-1: #8B5CF6;   /* purple — the U */
    --dot-2: #3B82F6;   /* blue   — the U's gradient end */
    --dot-3: #F97316;   /* orange — the cluster */
    --dot-4: #EC4899;   /* pink   — the cluster's warm red, lifted */

    /* Text -------------------------------------------------------------- */
    --text:   #F0EDFA;
    --text-2: #A99FC7;
    --text-3: #6F6491;

    --danger:    #FF6B6B;
    --danger-bg: #2E1420;

    /*  Pending-but-not-failed — the pill orange. Per theme like --danger,
        since the 2026-08-16 both-themes audit: the raw spark orange
        (--spark, #F97316) used as pill TEXT was only legible on the dark
        ground it was written against (~2.8:1 on a white card). Values in
        the theme blocks alongside --danger's. */
    --warn:      #FDBA74;
    --warn-bg:   #3B2A14;

    /*  ---- THE ONE DOWN ARROW ----

        One data URI, referenced everywhere something opens downward: every
        <select> (.ub-sel / .uadm-sel below) and the admin nav's grouped
        dropdowns (.uadm-navgroup in ubox-admin.css). Reported live,
        2026-08-12: the nav groups still drew a plain '▾' character - exactly
        the "differs by OS and browser" failure the select chevron was built
        to kill on 2026-08-09, just relocated to a control nobody had checked
        yet. A shared custom property means the next new picker inherits this
        by using the variable, not by finding and copying the URI. */
    --ub-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%239DB0C6' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 6 8 10.5 12.5 6'/%3E%3C/svg%3E");

    --r: 14px;
    --r-lg: 22px;
    --r-pill: 999px;

    --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
            "Helvetica Neue", Arial, sans-serif;
    --mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;

    --shadow: 0 2px 8px rgba(0,0,0,.5), 0 18px 50px rgba(0,0,0,.45);

    /*  1140 -> 1280 -> 1200, 2026-08-12. 1140 read as bare gutter on a wide
        screen; 1280 overcorrected — "you're gonna be too wide... I need a
        little more space on each edge, so it comes in a bit." 1200 plus the
        wider .wrap padding just below is the middle of those two complaints.
        One token, so every `.wrap` on the site moves together. Terms/
        Privacy's PROSE column (.ub-prose, Terms.cshtml) does not read this
        token at all, on purpose — long-form legal text stays its own,
        narrower measure regardless of how wide the rest of the site gets. */
    --wrap: 1200px;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
    margin: 0;
    background: var(--ink);
    color: var(--text);
    font-family: var(--sans);
    font-size: 16px;
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
    overflow-x: hidden;
}

img { max-width: 100%; display: block; }
a { color: inherit; }

:focus-visible { outline: 2px solid var(--amber); outline-offset: 3px; border-radius: 6px; }

/*  ---- ⚠ THE CLOAK, AND WHY EVERY KNOCKOUT ROOT ON /v2 CARRIES IT ----

    Reported live, 2026-08-12: "I've seen these ghost screens when this
    website is refreshing... something flashes up, and then the page comes
    up." Reproduced by fetching a page's own rendered HTML, stripping every
    <script> tag (recreating exactly the DOM a real browser holds for the
    several tens of milliseconds it spends fetching and executing
    ubox-current.js, knockout-3.2.0.js and ubox-v2.js before the page's own
    inline script ever calls ko.applyBindings) and taking a screenshot: it
    showed a full-screen "Confirm & Pay" dialog, centred and dimmed exactly
    like the real one, every field blank — DisCo, Type, Email, Electricity,
    Convenience fee, You pay all empty, a live-looking CONFIRM & PAY button
    underneath. That element only exists in markup because of an
    `<!-- ko if: reviewing -->` comment pair, and comments render nothing —
    so until Knockout parses them, the div they wrap is just an ordinary,
    fully visible, fixed-position, z-index:190 overlay with none of the data
    that would normally justify it being on screen at all. Nothing about this
    is caching or memory, whatever it looks like from the outside: it is a
    real browser painting real, unbound markup for a real fraction of a
    second, once per page load.

    The fix is the standard one for any templating library that binds after
    first paint — Angular calls it ng-cloak, this is the same idea under our
    own name. `.ub-bindwait` sits on the exact element each page hands to
    ko.applyBindings as its root (search `ub-bindwait` in the .cshtml files —
    one per Knockout-bound /v2 page), present in the server-rendered HTML
    from the first byte, so it is already hiding that region before a single
    frame paints. Each page's own inline script removes the class in the
    same synchronous turn it calls ko.applyBindings, immediately afterward —
    so the very first frame Knockout's own bindings are allowed to paint IS
    the first frame anybody sees. A page that has no Knockout root, or whose
    script errors before reaching that line, never gets the class added in
    the first place — this only ever hides a region that a page's own script
    promises to reveal a moment later. */
.ub-bindwait { visibility: hidden; }

/*  Reduced motion: kill decoration, KEEP loading feedback.

    This rule used to hit `*`, which froze the spinners solid — a stopped
    spinner reads as a hung page, which is worse than the motion it was
    avoiding. Progress indicators are information, not decoration, so they
    keep turning (a little slower). Fixed 2026-07-31 after the veil spinner
    was reported as "sitting there dead".  */
@media (prefers-reduced-motion: reduce) {
    *:not(.ub-spin):not(.ub-veil__ring i):not(.ub-skeleton) {
        animation-duration: .01ms !important;
        transition-duration: .01ms !important;
        scroll-behavior: auto;
    }
    .ub-spin          { animation-duration: 1.1s !important; }
    .ub-veil__ring i  { animation-duration: 1.8s !important; }
    .ub-skeleton      { animation: none; opacity: .5; }
}

h1, h2, h3 { margin: 0; line-height: 1.1; font-weight: 700; letter-spacing: -.03em; text-wrap: balance; }
p { margin: 0; }

.wrap { width: 100%; max-width: var(--wrap); margin: 0 auto; padding: 0 1.5rem; }
.wrap--narrow { max-width: 560px; }

/*  A page that is mostly reading, not a grid — Support's FAQ, and anything
    else that turns out to want the same treatment. Full .wrap width reads
    fine for a card grid; it reads as a wall for single-column paragraphs.
    Scoped onto the same element as .wrap, same as .wrap--narrow above, so it
    narrows the existing container rather than adding a second nested one. */
.ub-narrow-page { max-width: 52rem; }

.ub-vh {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

.ub-skip {
    position: absolute; left: -9999px; top: 0; z-index: 200;
    background: var(--amber); color: #FFFFFF; padding: .75rem 1rem;
    border-radius: 0 0 var(--r) 0; font-weight: 650;
}
.ub-skip:focus { left: 0; }

/* Ambient current canvas ------------------------------------------------ */
#ub-current { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 0; opacity: .5; pointer-events: none; }
.ub-page { position: relative; z-index: 1; min-height: 100vh; display: flex; flex-direction: column; }

/* Nav -------------------------------------------------------------------- */
.ub-nav {
    position: sticky; top: 0; z-index: 60;
    background: rgba(11, 7, 22, .78);
    backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
    border-bottom: 1px solid var(--line);
}
.ub-nav__in { display: flex; align-items: center; gap: 1.5rem; height: 68px; }

.ub-brand { display: flex; align-items: center; gap: .625rem; text-decoration: none; font-weight: 700; letter-spacing: -.02em; font-size: 1.125rem; }
.ub-brand__bolt {
    width: 32px; height: 32px; border-radius: 9px; flex: none;
    background: linear-gradient(140deg, var(--brand), var(--brand-3));
    display: grid; place-items: center; color: #FFFFFF;
    box-shadow: 0 0 0 1px rgba(139,92,246,.35), 0 6px 18px rgba(139,92,246,.22);
}
.ub-brand__bolt svg { width: 17px; height: 17px; }

.ub-nav__links { margin-left: auto; display: flex; align-items: center; gap: .25rem; }
/*  A NAVIGATION LINK NEVER WRAPS.

    On an iPhone "My Account" broke into "My" / "Account" and "Sign out" into
    "Sign" / "out", inside pills sized for one line. The items then sat at
    different heights and the whole bar read as broken — which is what was
    reported, and it is a wrapping problem rather than a font-size one. It
    would have happened at any size once the row got tight enough.

    nowrap here, and the responsive rules below shrink the type so the row
    still fits rather than overflowing. */
/*  ---- THE hidden ATTRIBUTE MUST KEEP WORKING ----

    `[hidden]` is enforced by the browser's own stylesheet as
    `[hidden] { display: none }`, at specificity (0,1,0). The rule below is
    `.ub-nav__links a` at (0,1,1) — HIGHER — so the moment it declared a
    `display`, it silently beat `hidden` and every element relying on the
    attribute reappeared.

    That is exactly what happened: "My Account" is `hidden` in the markup and
    revealed by script only when signed in, and adding display:inline-flex for
    the wrapping fix made it show to signed-OUT visitors, next to "Sign in".
    Two contradictory links, which is what was reported.

    This guard goes FIRST so anything below can set display freely. */
[hidden] { display: none !important; }

.ub-nav__links a {
    text-decoration: none; color: var(--text-2); font-size: .9375rem; font-weight: 550;
    white-space: nowrap; line-height: 1.2;
    display: inline-flex; align-items: center;
    padding: .5rem .8125rem; border-radius: var(--r-pill); transition: color .15s, background .15s;
}
.ub-nav__links a:hover { color: var(--text); background: var(--surface); }
.ub-nav__links a[aria-current="page"] { color: var(--amber); background: rgba(139,92,246,.1); }
@media (max-width: 720px) { .ub-nav__links a.opt { display: none; } }

/*  ---- THE HOME ICON ----

    A house rather than the word "Home". Square padding so it reads as an
    icon button and not as a text link that lost its text, and the same pill
    hover and aria-current colour as its neighbours so it belongs to the row.

    ⚠ KEPT AT EVERY WIDTH — no `.opt`, no media query hiding it. It is the way
    back to the top of the site, and the narrow screens where the logo is
    smallest and hardest to recognise as a link are exactly the ones that need
    it most. */
.ub-nav__links a.ub-home { padding: .5rem; }
.ub-nav__links a.ub-home svg { width: 1.25rem; height: 1.25rem; display: block; }


.ub-main { flex: 1; }

/* Hero ------------------------------------------------------------------- */
.ub-hero { padding: clamp(1.75rem, 4vw, 3rem) 0 clamp(2rem, 4vw, 3rem); }
/*  stretch, not center (2026-08-02).

    Centring the two columns against each other meant neither their tops nor
    their bottoms lined up with anything. The text column now runs the full
    height of the row so its last item - the DisCo strip - can be pushed to the
    floor and land level with the foot of the buy card.

    The card itself is align-self:start so it keeps its own height instead of
    stretching into a tall empty box on the steps where it has little in it. */
.ub-hero__grid {
    display: grid; gap: clamp(2rem, 5vw, 4rem);
    /*  480 -> 560 -> 480, 2026-08-12. 560 was tried alongside the --wrap
        widen above and reverted the same night: "you've made them way too
        big and ugly... they used to fit on the screen so you didn't have to
        scroll. Now they're just too wide." Back to the original. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 480px);
    align-items: stretch;
}
.ub-hero__grid > .ub-buy { align-self: start; }

/*  A flex column, so the headline and steps stack from the top and
    margin-top:auto on the DisCo strip can push it to the bottom. */
.ub-hero__intro { display: flex; flex-direction: column; }
@media (max-width: 940px) { .ub-hero__grid { grid-template-columns: 1fr; } }

/*  Hero order on small screens is set ONCE, further down this file, next to
    the other small-screen hero rules — search "HERO ORDER ON SMALL SCREENS".
    A competing block lived here and never applied: the rule down there is
    later AND more specific, and media queries add no specificity. */

.ub-eyebrow {
    display: inline-flex; align-items: center; gap: .5rem;
    font-size: .75rem; font-weight: 650; letter-spacing: .13em; text-transform: uppercase;
    color: var(--lcd); background: var(--lcd-dim);
    border: 1px solid rgba(96,165,250,.25);
    padding: .4375rem .75rem; border-radius: var(--r-pill); margin-bottom: 1.375rem;
}
.ub-eyebrow::before {
    content: ''; width: 6px; height: 6px; border-radius: 50%;
    background: var(--lcd); box-shadow: 0 0 8px var(--lcd);
    animation: ub-pulse 2.4s ease-in-out infinite;
}
@keyframes ub-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

.ub-hero h1 { font-size: clamp(1.875rem, 4.4vw, 2.875rem); margin-bottom: 1rem; }

/*  The headline is one sentence and reads as one line.

    Note the doubled selector: a later `.ub-hero h1` rule in the density pass
    is (0,1,1), so a bare `.ub-hero__h1` at (0,1,0) loses to it and only the
    nowrap would land — which is the worst outcome, since a nowrap that does
    not fit overflows the page rather than wrapping.

    The headline sits in the ~47% text column of .ub-hero__grid, so the size
    is tied to the viewport to keep it inside that column. Below 1100px no
    size both fits on one line and stays legible, so it wraps to two there
    instead of shrinking away. */
/*  ⚠ NO white-space: nowrap ON THE HEADLINE, AND IT MUST NOT COME BACK.

    It was there above 1100px, and the comment just above admitted the
    consequence: anything too long "overflows the page rather than wrapping".
    It does not overflow the page — it overflows its GRID COLUMN and runs
    underneath the buy card. That is what "Data bundles, whenever you want."
    was doing, and it looked like the card was sitting on top of the text.

    The arithmetic: between roughly 1100px and 1300px the text column is about
    580px, while min(3.5vw, 2.875rem) renders that heading at about 600px.
    Twenty pixels of overlap — invisible at 1440px, obvious at 1200.

    Shortening the words hides it rather than fixing it; the next heading long
    enough brings it straight back.

    So the headline WRAPS. What stops it wrapping BADLY is nowrap on the two
    halves instead: the line can only break at the comma, never inside "Data
    bundles" and never inside "anytime" — which is the only break that reads
    properly anyway. */
@media (min-width: 1100px) {
    .ub-hero h1.ub-hero__h1 { font-size: min(3.5vw, 2.875rem); }
}
.ub-hero h1.ub-hero__h1 em { white-space: nowrap; }
/*  One clear line between the headline and the three steps that follow it.
    Doubled selector on purpose: the density-pass `.ub-hero h1` rule above is
    (0,1,1), so a bare `.ub-hero__h1` at (0,1,0) loses to it and the margin
    would never land. */
.ub-hero h1.ub-hero__h1 { margin-bottom: 1.5rem; }
.ub-hero h1 em {
    font-style: normal;
    background: linear-gradient(100deg, var(--amber) 10%, var(--amber-hi) 55%, #FFD98A 90%);
    -webkit-background-clip: text; background-clip: text; color: transparent;
}
/*  font-weight: 500, not the inherited 400 (2026-08-12) — "some of the
    writings a bit thin". This is the first line of body copy a customer
    reads on every service page, sitting directly under a 700-900-weight
    gradient headline; at plain regular weight it read closer to a caption
    than a sentence. A small step toward the headline's own weight, not a
    second bold block competing with it. */
.ub-lede { font-size: clamp(.9375rem, 1.4vw, 1.0625rem); font-weight: 500; color: var(--text-2); max-width: 46ch; margin-bottom: 1.75rem; }

.ub-proof { display: flex; flex-wrap: wrap; gap: 1.5rem; }
.ub-proof__n { font-size: 1.25rem; font-weight: 750; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.ub-proof__l { font-size: .8125rem; color: var(--text-3); }

/* Buy card --------------------------------------------------------------- */
/*  ⚠ ONE HEIGHT FOR THE WHOLE TRANSACTION. DO NOT REMOVE min-height.

    The card holds every step of the purchase: the form, the waiting state
    while Quidly's sheet is open, the return from Quidly, and the finished
    token or airtime. Those have very different amounts of content, so the
    card grew and shrank as the customer moved through them - worst on the
    return from the payment sheet, where it jumped twice in a second.

    The owner's requirement, 2026-08-17: the card is the SAME SIZE throughout,
    and its bottom edge stays level with the row of provider logos beside it.
    Content is placed inside a fixed frame; the frame does not move.

    The value is a floor, not a fixed height - a step with unusually long
    content (a long DisCo address, a debt line) still grows rather than
    clipping, which is the one thing worse than moving. It is set in one place
    so every service inherits it and they cannot drift apart, which is exactly
    how the buttons and the units ended up inconsistent between services. */
.ub-buy {
    background: linear-gradient(168deg, var(--surface) 0%, var(--ink-2) 100%);
    border: 1px solid var(--line-hi);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow);
    padding: clamp(1.25rem, 3vw, 1.75rem);
    position: relative; overflow: hidden;

    min-height: var(--ub-buy-h, 27rem);
    display: flex;
    flex-direction: column;
}

/*  The step fills the frame rather than sitting at the top of it, so a short
    step (the "opening the payment window" wait, or a delivered token) is
    centred in the same box the form occupied instead of leaving the card
    looking half-empty. */
.ub-buy > form,
.ub-buy > .ub-buy__body,
.ub-buy > .ub-bindwait {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
}
.ub-buy::before {
    content: ''; position: absolute; inset: 0 0 auto 0; height: 1px;
    background: linear-gradient(90deg, transparent, rgba(139,92,246,.7), transparent);
}
.ub-buy__head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin-bottom: 1.25rem; }
.ub-buy__head h1, .ub-buy__head h2 { font-size: 1.25rem; }
.ub-buy__step { font-size: .75rem; color: var(--text-3); letter-spacing: .06em; font-variant-numeric: tabular-nums; }

.ub-rail { display: flex; gap: 5px; margin-bottom: 1.5rem; }
.ub-rail i { flex: 1; height: 3px; border-radius: 2px; background: var(--line-hi); transition: background .35s; }
.ub-rail i.on { background: var(--amber); box-shadow: 0 0 10px rgba(139,92,246,.5); }

/* Fields ----------------------------------------------------------------- */
.ub-f { margin-bottom: 1.125rem; }
.ub-lab { display: block; font-size: .8125rem; font-weight: 600; color: var(--text-2); margin-bottom: .5rem; }
.ub-lab span { display: block; font-weight: 400; color: var(--text-3); font-size: .75rem; margin-top: .125rem; }

/*  ---- ⚠ A FIELD SIZED FOR CONTENT IT WILL NEVER HOLD LOOKS UNCONSIDERED ----

    These were width:100% and nothing else, so on a wide card an email box ran
    the full width — roughly twice what any address needs. Nothing was broken;
    it just read as nobody having thought about it.

    max-width in `ch` rather than pixels, because ch is the width of a "0" in
    the current font: the cap then follows the type instead of a number
    somebody guessed once and never revisited.

    ⚠ WIDTH STAYS 100% AND THE CAP IS max-width, which is what keeps this
    safe on a phone. The field still fills a narrow screen; it simply stops
    growing past the point where growing helps.

    ⚠ AND THE CAP IS PER FIELD, NOT ONE NUMBER FOR ALL. A meter number is 11
    to 13 digits, an email about 30 characters, an amount 7. Giving them all
    the same width would trade one thoughtless look for another — see the
    overrides below.  */
.ub-in, .ub-sel {
    width: 100%; max-width: 34ch; min-height: 48px; padding: .6875rem 1rem;
    font-family: inherit; font-size: 1rem; color: var(--text);
    background: var(--ink-2); border: 1px solid var(--line-hi);
    border-radius: var(--r);
    transition: border-color .15s, box-shadow .15s, background .15s;
    -webkit-appearance: none; appearance: none;
}
.ub-in::placeholder { color: var(--text-3); }

/*  ---- WIDTHS THAT MATCH WHAT GOES IN THEM ----

    Measured against the real content, not chosen by eye:

      meter    13 digits, grouped "0000 0000 0000 0" — 19 characters
      amount   up to 7 characters, and it is a number nobody reads across
      textarea a message, which genuinely wants the room

    ⚠ THE METER FIELD IS MONOSPACE AND GROUPED, so it needs more ch than the
    digit count suggests — the spaces count. 22ch holds the longest grouping
    with room for a caret, and was checked against a 13-digit prepaid number
    rather than assumed. */
.ub-in--meter { max-width: 22ch; }
#ub-amount, .ub-in--amount { max-width: 14ch; }

/*  A message is the one thing here that benefits from width. It keeps the
    card's full measure. */
textarea.ub-in { max-width: 100%; }

/*  Hover used a hardcoded slate (#3C5170), which is a dark-page colour and
    read as a smudge on a light one. --amber-dim is what .ub-chip:hover
    already uses, so hovering anything interactive now looks the same. */
.ub-in:hover, .ub-sel:hover { border-color: var(--amber-dim); }

/*  ⚠ THE FOCUS BACKGROUND USED TO BE HARDCODED #0D1523, AND IT MADE LIGHT
    MODE UNUSABLE.

    The field's text is var(--text), which on a light page is #16103A - very
    nearly black. Focus then painted the ground behind it near-black too. So
    the moment somebody clicked into a field to type a meter number, the
    number they were typing became black on black and they could not read
    what they had entered. It only worked in dark mode, where near-black
    ground under light text is the correct answer.

    The ground no longer changes on focus at all. It does not need to: the
    amber border and the ring around it already say which field is active,
    in both themes, without touching the contrast between the text and what
    is behind it. */
.ub-in:focus, .ub-sel:focus {
    outline: none; border-color: var(--amber);
    box-shadow: 0 0 0 3px rgba(139,92,246,.16);
    background: var(--ink-2);
}
.ub-in[aria-invalid="true"], .ub-sel[aria-invalid="true"] {
    border-color: var(--danger); box-shadow: 0 0 0 3px rgba(255,107,107,.14);
}

/*  ---- EMPTY REQUIRED FIELD: a soft amber pulse until it is filled ----
    Reported 2026-09-20: a customer skipped the email box, could not see why
    "Review purchase" would not continue, and gave up. A required field left
    empty now pulses a warm amber ring so the eye is drawn to the box that
    still needs an answer. It is deliberately the --warn amber, not the violet
    focus ring and not the red error state: it reads as "this one still needs
    you", not "you did something wrong". The pulse stops the instant the field
    has a value, and it yields to focus so it never competes with the ring on
    the field the customer is actually typing in. */
.ub-needsfill:not(:focus) {
    border-color: var(--warn);
    animation: ub-needsfill-pulse 1.5s ease-in-out infinite;
}
@keyframes ub-needsfill-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(253,186,116,0); }
    50%      { box-shadow: 0 0 0 4px rgba(253,186,116,.30); }
}
@media (prefers-reduced-motion: reduce) {
    /* No pulse for anyone who has asked motion to stop; the ring stays put
       so the cue is still there, it simply does not move. */
    .ub-needsfill:not(:focus) {
        animation: none;
        box-shadow: 0 0 0 3px rgba(253,186,116,.28);
    }
}
.ub-in--meter { font-family: var(--mono); font-size: 1.1875rem; letter-spacing: .12em; font-variant-numeric: tabular-nums; }

/*  ---- THE ARROW, AND WHY IT IS A CHEVRON RATHER THAN A TRIANGLE ----

    Every picker on the site is meant to use this one shape - reported live,
    2026-08-09: admin's own <select> (.uadm-sel below) had no custom arrow at
    all, so it fell back to the browser's native one, which looks nothing
    like this and differs by OS and browser. An open chevron - two strokes
    meeting at a point, no fill, no flat top edge - rather than the filled
    triangle this used to be. Both `.ub-sel` and `.uadm-sel`, and the admin
    nav's grouped dropdowns (.uadm-navgroup in ubox-admin.css), now read the
    shape from the one `--ub-chevron` custom property up in :root, so they
    can never drift into different arrows again. */
.ub-sel, .uadm-sel {
    background-image: var(--ub-chevron);
    background-repeat: no-repeat; background-position: right 1rem center; background-size: 15px;
    padding-right: 2.75rem; cursor: pointer;
    -webkit-appearance: none; appearance: none;
}
.ub-sel option { background: var(--surface); color: var(--text); }

.ub-err { color: var(--danger); font-size: .8125rem; font-weight: 550; margin-top: .5rem; }

/*  ---- SEQUENTIAL FIELDS: NOT YOUR TURN YET ----

    A field disabled because whatever comes before it in the form is not
    filled in yet — the meter number before the DisCo, the phone number
    before the network and amount. Same .4 opacity `.ub-btn:disabled`
    already uses, so "not your turn yet" reads as the one idea whether it
    lands on a button or a field, rather than inventing a second look for
    the same state. */
.ub-in:disabled, .ub-sel:disabled { opacity: .4; cursor: not-allowed; }
.ub-seg.is-locked { opacity: .4; }
.ub-seg.is-locked label { cursor: not-allowed; pointer-events: none; }
.ub-chip:disabled { opacity: .4; cursor: not-allowed; }

/*  ---- ⚠ IN-SITE SUGGESTION LIST, REPLACING <input list> + <datalist> (2026-08-14) ----

    A saved meter/phone/card number used to be offered through the native
    HTML pattern `<input list> <datalist>`, which is the BROWSER's or OS's
    own popup - reported directly by the owner, on a tablet: "I get my
    numbers coming up on my tablet, I want everything to remain within that
    site." Nothing in this stylesheet can reach that popup; it renders
    however the platform draws its own suggestion UI, which is exactly the
    "not part of the site" look he is describing.

    ko.bindingHandlers.ubComboList (ubox-v2.js) draws this instead: a plain
    `<ul>` it opens under the field, filters, and closes itself, styled
    entirely here. Menu look copies .uadm-navgroup__menu (ubox-admin.css)
    rather than a new vocabulary - same surface, border, radius and shadow a
    drawn dropdown already uses on this site, just anchored to a text field
    instead of a nav item.

    ⚠ WIDTH IS SET IN JS, NOT HERE. .ub-in caps its own width
    (.ub-in--meter, etc.) that .ub-combo - a plain block wrapper - does not
    inherit, so the two can differ; the binding measures the field itself
    on open and writes an inline width, which is the only way to guarantee
    the list lines up under the exact box a customer is looking at. */
.ub-combo { position: relative; display: block; }
/*  The field a combo list hangs from carries the site's one chevron
    (--ub-chevron, :root — see "THE ARROW, AND WHY IT IS A CHEVRON" above
    .ub-sel), asked for directly by the owner 2026-08-16: without it these
    fields read as plain text boxes, and nothing said a tap would offer the
    saved numbers back. Same artwork, position and size as .ub-sel so the
    two kinds of picker can never drift into different arrows; the padding
    keeps a long grouped number from typing under it. ubComboList always
    wraps its field in .ub-combo, so this reaches every such field — the
    meter box, the four quick-buy fields on the account page, and the
    standalone pages' own — without a class to remember on each one.
    Selector is the child .ub-in rather than .ub-combo itself because the
    wrapper is a plain block; the border, and therefore the arrow's anchor,
    belong to the field inside it. */
.ub-combo > .ub-in {
    background-image: var(--ub-chevron);
    background-repeat: no-repeat; background-position: right 1rem center; background-size: 15px;
    padding-right: 2.75rem;
}
.ub-combo__list {
    display: none;
    position: absolute;
    top: calc(100% + .3rem);
    left: 0;
    z-index: 70;
    max-height: 14rem;
    overflow-y: auto;
    margin: 0;
    padding: .35rem;
    list-style: none;
    background: var(--ink-2);
    border: 1px solid var(--line);
    border-radius: var(--r);
    box-shadow: var(--shadow);
}
.ub-combo.is-open .ub-combo__list { display: block; }
.ub-combo__opt {
    padding: .5625rem .75rem;
    border-radius: 9px;
    font-size: .9375rem;
    color: var(--text-2);
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ub-combo__opt.is-active,
.ub-combo__opt:hover { color: var(--text); background: var(--surface-2); }

/*  ---- "YOU CAN'T CONTINUE YET, AND HERE IS WHY" ----

    Continue/Pay buttons in the buy cards are no longer natively `disabled`
    while a required field is outstanding — a disabled button swallows the
    click and a customer pressing it learns nothing, which is the exact
    complaint this exists to fix ("Continue stays greyed out, but it's not
    very clear the email address needs to be put in"). `.is-blocked` gives
    the button the same greyed-out look `:disabled` always has; the click
    handler still runs, decides whether the purchase may actually proceed,
    and if not, marks the one field that is stopping it using the SAME
    ub-err / aria-invalid pair every other field-level error on this site
    already uses, then shakes it.

    ⚠ REPORTED 2026-08-11: "some of the continue buttons look grayed out" —
    on the ENABLED ones. Opacity .4 alone wasn't the problem; the problem is
    that .ub-btn--go's own rest-state glow (below) used to be so faint that
    a full-opacity, fully-saturated button and a .4-opacity one read as two
    shades of the same washed-out purple rather than "off" vs. "press me".
    Two changes, not one: the ready state now gets a real glow (see
    .ub-btn--go/.ub-btn--pay), and this blocked state is pushed further
    toward flat grey — desaturated as well as dimmed, and with its glow
    switched off outright — so the two states can't be confused for
    lighter/darker versions of each other.

    ⚠ RE-REPORTED SAME DAY, after the above shipped as d5c20b1: "still gray."
    Checked both real possibilities before touching anything again. (1) A
    LOGIC bug — canValidate/canContinue/canPay wrongly false with every
    required field actually filled in. Driven live against the running
    Knockout view models on both Electricity (Index.cshtml) and Airtime,
    setting the same observables the real inputs set and reading the
    computeds back: every one flips true exactly when it should, and the
    button's actual class list drops is-blocked at the same moment. Read
    the other four buy pages' canContinue/canPay too — same shape, no
    outlier. Not a logic bug anywhere. (2) The CSS itself not being
    visible enough. It's fine — injecting THIS file's actual rules over the
    live page and screenshotting shows a real, visible purple halo, not a
    washed-out button. What was actually stale was the DEPLOY: fetching the
    live site's own served ubox-v2.css back from itself showed the OLD
    box-shadow (0 6px 20px / .32 alpha, no glow) and no "REPORTED
    2026-08-11" text at all — d5c20b1 had never been built onto the box the
    owner was testing against. Nothing here needed changing again; the
    running container did. See CLAUDE.md's "A TEST MEANS A CLEAN CLONE" —
    this is that rule biting from the other side, where the SHIP, not the
    change, was the untested step. */
.ub-btn.is-blocked {
    opacity: .4;
    filter: saturate(.35) grayscale(.4);
    box-shadow: none;
}
.ub-btn.is-blocked:hover { box-shadow: none; }
@keyframes ub-shake {
    10%, 90% { transform: translateX(-1px); }
    20%, 80% { transform: translateX(2px); }
    30%, 50%, 70% { transform: translateX(-4px); }
    40%, 60% { transform: translateX(4px); }
}
.ub-f.is-shake .ub-in, .ub-f.is-shake .ub-sel, .ub-f.is-shake .ub-seg { animation: ub-shake .4s; }

/* Segmented control ------------------------------------------------------ */
.ub-seg {
    display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
    background: var(--ink-2); border: 1px solid var(--line-hi);
    border-radius: var(--r-pill); padding: 4px;
}
.ub-seg input { position: absolute; opacity: 0; pointer-events: none; }
.ub-seg label {
    display: flex; align-items: center; justify-content: center; min-height: 44px;
    border-radius: var(--r-pill); font-size: .9375rem; font-weight: 600;
    color: var(--text-3); cursor: pointer; transition: all .2s;
}
.ub-seg input:checked + label { background: var(--amber); color: #FFFFFF; box-shadow: 0 4px 14px rgba(139,92,246,.28); }
.ub-seg input:focus-visible + label { outline: 2px solid var(--amber); outline-offset: 3px; }

/* Buttons ---------------------------------------------------------------- */
/*  ---- ⚠ THE BUTTON MUST MATCH THE FIELDS ABOVE IT ----

    Capping the inputs at 34ch without capping this left every Sign in button
    running wider than the boxes it belongs to — a form that looked like two
    different forms stacked. Reported immediately, and fairly: "you can't have
    the buttons longer than the boxes."

    Same 34ch cap as .ub-in, so a column of fields and the button under them
    share one edge. If one of these numbers ever changes, BOTH change — they
    are one measurement, not two that happen to agree.

    ⚠ The exceptions below are deliberate and each says why. Anything full
    width on purpose has to opt back in, rather than this quietly not
    applying. */
.ub-btn {
    display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
    /*  ⚠ AS WIDE AS ITS LABEL, NOT AS WIDE AS ITS CONTAINER.

        This was width:100% with a 34ch cap, which stretched every button to
        the width of whatever it sat in. Owner, 2026-08-29: "no human being
        is going to make a button that is longer than the information they
        need to put into it... it just looks stupid. Adjust those buttons
        down to surround the actual wording."

        That reverses the earlier "everything in a stack shares one width"
        rule, which was about the boxes - inputs and selects - and had been
        carried onto the buttons as well. Inputs still share one width: a
        text field has no natural size and has to be told one. A button does
        - its label - and padding is what gives it room.

        max-width:100% so a very long label still cannot push its own card
        sideways. */
    width: auto; max-width: 100%; min-height: 48px; padding: .6875rem 1.25rem;
    /*  ⚠ width:auto IS NOT ENOUGH ON ITS OWN. Several of the buy forms are
        `display:flex; flex-direction:column`, and a flex item's default
        cross-axis alignment is stretch - so Continue was still coming out
        344px wide on a 430px phone with width:auto sitting right there in
        the rule. Measured on the deployed page; reading the CSS said it was
        already fixed.

        flex-start rather than center: a form's action belongs under the left
        edge of the fields it submits. Row containers put it back to center
        just below, where the cross axis is vertical instead. */
    align-self: flex-start;
    font: inherit; font-size: .9375rem; font-weight: 650; letter-spacing: -.01em;
    border: 1px solid transparent; border-radius: var(--r); cursor: pointer;
    transition: transform .08s, box-shadow .2s, background .2s, opacity .2s;
    text-decoration: none;
}
.ub-btn:active:not(:disabled) { transform: translateY(1px); }
/*  Same "flat grey, not just dimmed purple" treatment as .is-blocked above,
    so a natively-disabled button and a click-blocked one read as the one
    idea rather than two different fades. */
.ub-btn:disabled { opacity: .4; filter: saturate(.35) grayscale(.4); box-shadow: none; cursor: not-allowed; }
.ub-btn--go { background: linear-gradient(160deg, var(--amber-hi), var(--amber)); color: #FFFFFF; box-shadow: 0 6px 22px rgba(139,92,246,.3); }
.ub-btn--go:hover:not(:disabled) { box-shadow: 0 8px 30px rgba(139,92,246,.44); }

/*  ⚠ "Confirm & pay ₦X,XXX.XX" ON THE BASE .ub-btn's SINGLE ROW WRAPPED
    UGLY, NOT DELIBERATELY. .ub-btn is `inline-flex` with a fixed `max-
    width: 34ch` — on real amounts and real screen widths the label and the
    naira figure did not both fit on one line, so the button wrapped, but
    with the row's `align-items: center` centring two lines of very
    different lengths, which reads as a lopsided gap rather than a
    two-line button. Requested 2026-08-11: put "Confirm & pay" on its own
    line and the amount larger underneath, on every page that has this
    button — Index, Airtime, Bundles, TV, Education,
    InternationalAirtime, Account's four quick-buy cards. */
/*  Requested 2026-08-11: bigger overall (min-height 58 -> 72, wider
    padding) so it reads as the primary action, and "Pay" capitalized in
    the label text itself (see the .cshtml files) since this button IS the
    confirmation, not a secondary control. Superseded a few hours later,
    below, by full caps on the whole label. */
/*  Requested again 2026-08-11: the owner wanted the FULL label capitalized,
    not just "Pay". Done as a transform rather than typing "CONFIRM & PAY"
    into every .cshtml, matching how every other all-caps label in this file
    works (.ub-lcd__cap, .ub-edcs__lab, .ub-pill, etc.) — the markup keeps
    the mixed-case text a screen reader would actually want to hear, and
    this is the only place that decides it reads as capitals. */
.ub-btn--stack { flex-direction: column; gap: .1875rem; min-height: 72px; padding: 0 1.5rem; line-height: 1.15; font-size: 1.0625rem; text-transform: uppercase; }
.ub-btn--stack .ub-btn__amt { font-size: 1.1875rem; font-weight: 800; letter-spacing: -.01em; text-transform: none; }
/*  Account.cshtml's four quick-buy buttons toggle between this label and a
    separate busy/stage span on the SAME base .ub-btn, so the column layout
    has to live on the inner wrapper rather than the button itself. */
.ub-btn__label--stack { display: flex; flex-direction: column; gap: .1875rem; line-height: 1.15; font-size: 1.0625rem; text-transform: uppercase; }
.ub-btn__label--stack .ub-btn__amt { font-size: 1.1875rem; font-weight: 800; letter-spacing: -.01em; text-transform: none; }
.ub-btn--ghost { background: transparent; border-color: var(--line-hi); color: var(--text-2); }
.ub-btn--ghost:hover:not(:disabled) { border-color: var(--text-3); color: var(--text); }
.ub-btn--sm { min-height: 42px; font-size: .875rem; width: auto; padding: 0 1rem; }
.ub-btn--auto { width: auto; }
/*  ⚠ flex-wrap, AND THE BUTTONS NO LONGER SHARE THE ROW EQUALLY.
    `flex: 1` gave every button an equal slice of the row regardless of how
    long its label was, so "Buy again" came out the same width as "All
    services" and both were stretched well past their words. They size to
    their labels now and wrap to the next line when the row runs out, which
    is what stops the old equal-share rule from being needed at all. */
.ub-btnrow { display: flex; flex-wrap: wrap; gap: .625rem; }
/*  align-self back to centre: in a ROW the cross axis is vertical, and
    flex-start from the base rule would hang a taller button off the top of
    a shorter one. Same for every other row of buttons on the site. */
.ub-btnrow .ub-btn { flex: 0 0 auto; align-self: center; }
.ub-cookie__actions .ub-btn,
.ub-xsell__row .ub-btn { align-self: center; }

/*  ---- ONE SPINNER, AT WHATEVER SIZE ----

    There were three: this one (a single arc), the veil ring between pages
    (three concentric arcs, one of them reversed) and the payment ring (three
    arcs again, different easing and different colours). Three loading states
    on one site reads as three different sites.

    The veil ring is the one to keep — it is the biggest, the most seen, and
    the one that looks deliberate. So this becomes the same thing, scaled.

    ⚠ BUILT FROM ::before AND ::after, NOT EXTRA ELEMENTS. The veil ring gets
    its three arcs from three <i> children. .ub-spin is used in four places as
    a bare <span> or <div>, and giving it children would mean editing every
    one — including inside buttons where a stray element changes the flex
    layout. Two pseudo-elements give the same three rings and no markup moves.

    Everything is proportional to the element's own size, so it is correct at
    17px in a button and at 62px on its own. Set width and height and it
    scales; nothing else needs touching. */
/*  ---- THE ARCS ARE THE MARK'S OWN COLOURS ----

    Purple, blue and orange — --dot-1, --dot-2 and --dot-3, which are taken
    from the six dots beside the U. The spinner is the thing people watch
    while they wait, so it may as well be the brand turning rather than three
    arbitrary colours.

    ORANGE IS THE INNER RING ON PURPOSE. The round-8 rule still holds: orange
    and red are the loudest thing in the palette and must not become the
    largest. Innermost is the smallest arc, so it reads as a spark in the
    middle rather than a ring of it.

    ⚠ These follow the SEASONS. --dot-1 and --dot-3 are overridden for
    Christmas, Independence Day, Ramadan and the rest, so the spinner turns
    green and gold on 1 October without anybody touching this. That is a
    feature — it is the same cluster the logo uses. */
/*  ⚠ 24px, NOT 17px. At 17 this sat inside a 48px button and read as a
    speck — "we can't really see the spinner in that box". It is the only
    thing telling somebody their sign-in or their payment is still working, so
    it gets to be seen. The three arcs below scale with it. */
/*  ⚠ 3px ARCS, NOT 2px, AND LIFTED.

    Reported 7 Aug: "some of the boxes have spinners and they're too small, or
    they're not bright enough — if they're small, make them a bit brighter."

    Size was already argued out above and is staying at 24px. What was left is
    WEIGHT. Three concentric 2px arcs on a 24px circle is a very thin line, and
    thin warm colours on a white card wash out badly — the spinner was present
    and simply not noticeable, which for the one element saying "your payment
    is still working" is the same as being absent.

    So the arcs get 3px, and saturate() lifts the dot colours without changing
    them: the palette still comes from the logo cluster and still turns green
    and gold on 1 October. drop-shadow gives a faint halo so the arcs separate
    from a white card as well as a dark one. */
.ub-spin {
    position: relative;
    width: 24px; height: 24px; flex: none;
    border-radius: 50%;
    border: 3px solid transparent;
    border-top-color: var(--dot-1);
    animation: ub-spin 1s linear infinite;
    filter: saturate(1.35) drop-shadow(0 0 1.5px rgba(139, 92, 246, .45));
}
.ub-spin::before,
.ub-spin::after {
    content: '';
    position: absolute;
    border-radius: 50%;
    border: 3px solid transparent;
}
/*  Middle arc — reversed, like the veil's second ring. The counter-rotation
    is what stops three concentric circles reading as one thick one. */
.ub-spin::before {
    inset: 14%;
    border-right-color: var(--dot-2);
    animation: ub-spin 1.5s linear infinite reverse;
}
.ub-spin::after {
    inset: 30%;
    border-bottom-color: var(--dot-3);
    animation: ub-spin 2s linear infinite;
}

@keyframes ub-spin { to { transform: rotate(360deg); } }

.ub-assure { display: flex; flex-wrap: wrap; gap: .5rem 1.125rem; margin-top: 1.125rem; font-size: .75rem; color: var(--text-3); }
.ub-assure span { display: inline-flex; align-items: center; gap: .375rem; }
.ub-assure svg { width: 13px; height: 13px; color: var(--lcd); }

/* Confirmed customer ----------------------------------------------------- */
.ub-confirm {
    background: var(--lcd-dim); border: 1px solid rgba(96,165,250,.25);
    border-radius: var(--r); padding: .875rem 1rem; margin-bottom: 1.25rem;
    display: flex; gap: .75rem; align-items: flex-start;
}
.ub-confirm svg { width: 18px; height: 18px; color: var(--lcd); flex: none; margin-top: 2px; }
.ub-confirm b { display: block; font-size: .9375rem; letter-spacing: -.01em; }
.ub-confirm small { color: var(--text-2); font-size: .8125rem; }

/* Line items ------------------------------------------------------------- */
/*  ⚠ background is a TOKEN, not a literal. It was rgba(11,18,32,.5) - a dark
    navy at half opacity, which is a dark-theme colour. On the light page it
    composited against white into a grey slab sitting in the middle of the buy
    card, with the meter number and DisCo barely legible on it. It went
    unnoticed for the same reason the contrast bug did: it only appears once
    the meter is confirmed, which is a step past the landing page.
    Fourth literal colour in this file to break light theme. Use the tokens. */
.ub-lines { border: 1px solid var(--line); border-radius: var(--r); padding: .5rem 1rem; margin-bottom: 1.25rem; background: var(--surface-2); }
.ub-lines dl { margin: 0; }
.ub-line { display: flex; justify-content: space-between; gap: 1rem; padding: .5rem 0; font-size: .9375rem; }
.ub-line dt { color: var(--text-3); margin: 0; }
.ub-line dd { margin: 0; font-weight: 600; text-align: right; font-variant-numeric: tabular-nums; }
.ub-line--tot { border-top: 1px dashed var(--line-hi); margin-top: .25rem; padding-top: .8125rem; font-size: 1.1875rem; }
.ub-line--tot dt { color: var(--text-2); }
.ub-line--tot dd { font-weight: 750; color: var(--amber); }

/* Amount ----------------------------------------------------------------- */
.ub-amt { position: relative; }
.ub-amt__ng { position: absolute; left: 1rem; top: 50%; transform: translateY(-50%); font-size: 1.1875rem; font-weight: 650; color: var(--text-3); pointer-events: none; }
.ub-amt .ub-in { padding-left: 2.375rem; font-size: 1.375rem; font-weight: 700; font-variant-numeric: tabular-nums; }

/*  ⚠ WRAPS. NEVER a hidden-scrollbar horizontal scroll - owner's ruling,
    2026-09-05, from the dev box on a phone width: the ₦5,000 chip sat cut
    off at the page edge with nothing to say the row scrolls ("take it off
    or make it visible fully"). Every chip is fully visible on every width,
    on every service that uses this row - a second line is the cost, an
    invisible option was the bug. */
.ub-chips {
    display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .75rem;
}
.ub-chip {
    min-height: 40px; padding: 0 .9375rem; font: inherit; font-size: .875rem; font-weight: 600;
    background: var(--ink-2); border: 1px solid var(--line-hi); color: var(--text-2);
    border-radius: var(--r-pill); cursor: pointer; transition: all .15s; font-variant-numeric: tabular-nums;
    white-space: nowrap; flex-shrink: 0;
}
.ub-chip:hover { border-color: var(--amber-dim); color: var(--text); }
.ub-chip.on { background: rgba(139,92,246,.14); border-color: var(--amber); color: var(--amber); }

/*  Purchase timeline ------------------------------------------------------
    Shared by /track and the electricity page's collecting-your-token state
    (owner, 2026-09-05: the timeline appears right after "payment
    successful", in the flow itself). data-stage on the <ol> drives which
    dots are filled and which one pulses; keep the stage numbers in sync
    with ComputeStage in Track.cshtml.cs. Tokens only, so both themes work
    unchanged. */
/*  The owner's chosen design (2026-09-06, "b" + his own spec): courier
    style. Timestamps on the right, the SPINNER IS THE ACTIVE STAGE'S DOT,
    the active row is magnified with the live message on it, and a done
    stage shrinks back to a green tick. No separate spinner above the
    timeline anywhere. data-stage on the <ol> + data-at per <li> drive all
    of it - stage N active means rows < N are done. */
.ub-trackline { list-style: none; margin: 1.25rem 0; padding: 0; }
.ub-trackline li { position: relative; padding: 0 0 1.35rem 2.5rem; transition: padding .25s; }
.ub-trackline li:last-child { padding-bottom: 0; }
.ub-trackline li::before {
    content: ""; position: absolute; left: .35rem; top: .15rem;
    width: 1.15rem; height: 1.15rem; border-radius: 50%; box-sizing: border-box;
    border: 2px solid var(--line-hi, #8886); background: transparent;
    display: flex; align-items: center; justify-content: center;
    font-size: .72rem; font-weight: 800; color: #fff; line-height: 1;
    transition: background .3s, border-color .3s;
}
.ub-trackline li::after {
    content: ""; position: absolute; left: .88rem; top: 1.5rem; bottom: -.05rem;
    width: 2px; background: var(--line-hi, #8886);
}
.ub-trackline li:last-child::after { display: none; }
/*  ⚠ padding-right RESERVES THE TIMESTAMP'S COLUMN. 2026-09-07, owner saw
    "Asking your DisCo" run straight under the right-aligned time and asked
    for the label to WRAP so the actual transaction time stays readable. The
    label is a full-width block and .ub-tl-t is absolutely positioned over
    the row's top-right, so without this the text flows under the time. The
    reserve is on the label and the live message so both wrap clear of it, on
    every service at once (this stylesheet is shared). */
.ub-trackline li b { display: block; font-size: .95rem; color: var(--text-2, inherit); transition: font-size .25s; padding-right: 4.25rem; }
/*  The per-row live message: hidden except on the ACTIVE row. */
.ub-trackline li .ub-tl-msg { display: none; font-size: .85em; opacity: .8; padding-right: 4.25rem; }
/*  The timestamp, right-aligned on the row. Empty spans take no space.
    nowrap so a "6:42 PM" never itself wraps into two lines under the label. */
.ub-trackline li .ub-tl-t {
    position: absolute; right: 0; top: .25rem; font-size: .78em; opacity: .6;
    font-variant-numeric: tabular-nums; white-space: nowrap;
}
/*  DONE: green tick, green rail below, settled size. */
.ub-trackline[data-stage="1"] li[data-at="1"]::before,
.ub-trackline[data-stage="2"] li[data-at="1"]::before, .ub-trackline[data-stage="2"] li[data-at="2"]::before,
.ub-trackline[data-stage="3"] li[data-at="1"]::before, .ub-trackline[data-stage="3"] li[data-at="2"]::before, .ub-trackline[data-stage="3"] li[data-at="3"]::before,
.ub-trackline[data-stage="4"] li::before {
    content: "✓";
    background: var(--go, #2ea44f); border-color: var(--go, #2ea44f);
}
.ub-trackline[data-stage="2"] li[data-at="1"]::after,
.ub-trackline[data-stage="3"] li[data-at="1"]::after, .ub-trackline[data-stage="3"] li[data-at="2"]::after,
.ub-trackline[data-stage="4"] li::after {
    background: var(--go, #2ea44f);
}
.ub-trackline[data-stage="1"] li[data-at="1"] b,
.ub-trackline[data-stage="2"] li[data-at="1"] b, .ub-trackline[data-stage="2"] li[data-at="2"] b,
.ub-trackline[data-stage="3"] li[data-at="1"] b, .ub-trackline[data-stage="3"] li[data-at="2"] b, .ub-trackline[data-stage="3"] li[data-at="3"] b {
    color: var(--text, inherit);
}
/*  ACTIVE: the SITE'S OWN THREE-COLOUR RING spins as the stage's dot
    (owner, 2026-09-06: "use the site-wide spinner... before the timeline
    green circle goes green and ticks") - real markup (.ub-tl-spin, three
    <i> arcs in the brand dots, same as .ub-veil__ring) because a pseudo-
    element cannot hold three arcs. The row is magnified, its message
    shows. data-stage 0 exists for "nothing proven yet" - the bank watch -
    with row 1 as the active one. */
.ub-trackline[data-stage="0"] li[data-at="1"],
.ub-trackline[data-stage="1"] li[data-at="2"],
.ub-trackline[data-stage="2"] li[data-at="3"],
.ub-trackline[data-stage="3"] li[data-at="4"] { padding-bottom: 1.6rem; }
.ub-trackline[data-stage="0"] li[data-at="1"]::before,
.ub-trackline[data-stage="1"] li[data-at="2"]::before,
.ub-trackline[data-stage="2"] li[data-at="3"]::before,
.ub-trackline[data-stage="3"] li[data-at="4"]::before { display: none; }
.ub-trackline[data-stage="0"] li[data-at="1"] .ub-tl-spin,
.ub-trackline[data-stage="1"] li[data-at="2"] .ub-tl-spin,
.ub-trackline[data-stage="2"] li[data-at="3"] .ub-tl-spin,
.ub-trackline[data-stage="3"] li[data-at="4"] .ub-tl-spin { display: block; }
.ub-trackline[data-stage="0"] li[data-at="1"] b,
.ub-trackline[data-stage="1"] li[data-at="2"] b,
.ub-trackline[data-stage="2"] li[data-at="3"] b,
.ub-trackline[data-stage="3"] li[data-at="4"] b {
    font-size: 1.16rem; font-weight: 750; color: var(--text, inherit);
}
.ub-trackline[data-stage="0"] li[data-at="1"] .ub-tl-msg,
.ub-trackline[data-stage="1"] li[data-at="2"] .ub-tl-msg,
.ub-trackline[data-stage="2"] li[data-at="3"] .ub-tl-msg,
.ub-trackline[data-stage="3"] li[data-at="4"] .ub-tl-msg { display: block; }
/*  The ring itself: the site veil ring (.ub-veil__ring) at a reduced
    timeline size - owner, 2026-09-06, wants it unmistakably the website
    three-colour spinner, not a thin single arc. Veil is 62px insets 0/9/18
    at 2px; this is 34px with insets scaled to the SAME ratio (0 / 5px /
    10px) and 2.5px arcs so each of the three rings is distinct at small
    size. Seasonal --dot overrides carry through exactly as on the veil. */
.ub-trackline li .ub-tl-spin {
    display: none; position: absolute; left: -.05rem; top: -.35rem;
    width: 2.1rem; height: 2.1rem;
}
.ub-trackline li .ub-tl-spin i {
    position: absolute; inset: 0; border-radius: 50%;
    border: 2.5px solid transparent; display: block;
}
.ub-trackline li .ub-tl-spin i:nth-child(1) { border-top-color: var(--dot-1); animation: ub-spin 1s linear infinite; }
.ub-trackline li .ub-tl-spin i:nth-child(2) { inset: 5px; border-right-color: var(--dot-2); animation: ub-spin 1.5s linear infinite reverse; }
.ub-trackline li .ub-tl-spin i:nth-child(3) { inset: 10px; border-bottom-color: var(--dot-3); animation: ub-spin 2s linear infinite; }
/*  Stage 4 on data-stage 4 is the Delivered beat: magnified green tick
    with a glow, its message showing - the moment before the card. */
.ub-trackline[data-stage="4"] li[data-at="4"] { padding-bottom: 1.6rem; }
.ub-trackline[data-stage="4"] li[data-at="4"]::before {
    width: 1.5rem; height: 1.5rem; left: .15rem; top: 0; font-size: .95rem;
    box-shadow: 0 0 0 8px rgba(46,164,79,.25);
}
.ub-trackline[data-stage="4"] li[data-at="4"] b {
    font-size: 1.16rem; font-weight: 750; color: var(--text, inherit);
}
.ub-trackline[data-stage="4"] li[data-at="4"] .ub-tl-msg { display: block; color: var(--go, #2ea44f); }
@keyframes ubtrackspin { to { transform: rotate(360deg); } }
.ub-tracktoken {
    border: 1px solid var(--line-hi, #8886); border-radius: .6rem;
    padding: .8rem 1rem; margin: 1rem 0;
}
.ub-tracktoken .mono { font-size: 1.35em; letter-spacing: .06em; }
.ub-trackresend { margin: 1rem 0; }

/* The LCD token ---------------------------------------------------------- */
.ub-lcd {
    /*  ⚠ FOLLOWS THE THEME. Light page, light panel. It was hardcoded
        #061410 in both themes on the argument that "a meter display is not
        white" - a nice idea that made the light theme not actually light,
        and dragged every label and button on this panel into being pinned
        light to stay readable. One rule: light theme is light throughout,
        dark theme is dark throughout, and inputs and panels obey it too. */
    background: var(--ink-2);
    border: 1px solid rgba(96,165,250,.3);
    border-radius: var(--r);
    padding: 1.5rem 1.25rem; text-align: center;
    position: relative; overflow: hidden;
    box-shadow: inset 0 0 60px rgba(96,165,250,.08), 0 0 30px rgba(96,165,250,.1);
}
.ub-lcd::after {
    content: ''; position: absolute; inset: 0; pointer-events: none;
    background: repeating-linear-gradient(0deg, rgba(0,0,0,.28) 0 1px, transparent 1px 3px);
}
/*  ⚠ THE SCANLINES ARE A FIXED rgba(0,0,0,.28), NOT A THEME TOKEN.
    On the dark panel that reads as a subtle CRT texture; on the light panel
    the same dark lines at the same opacity, every 3px, over a light
    background is what was reported as "too many lines... quite hard to
    read" - a scanline effect only works when it's a shade darker than its
    own background, not a hard dark stripe on a light one. Light theme keeps
    the effect but at a fraction of the strength. */
:root[data-theme="light"] .ub-lcd::after {
    background: repeating-linear-gradient(0deg, rgba(0,0,0,.05) 0 1px, transparent 1px 3px);
}
/*  ⚠ BIGGER, AND IT MUST NOT WRAP — reported directly 2026-08-12: "MTN
    data needs to be a lot bigger... that name must be bigger... some of the
    services wrap, we don't want it to wrap, you need to make the thing
    small enough if it's a long line." This is WHO the purchase was from -
    "MTN Data", "Startimes Smartcard subscription" - and belongs at the
    weight of a headline, not a caption; the old .6875rem/.22em pairing
    (an eyebrow label size) was both too small to read as the headline it
    is and, at .22em letter-spacing, wide enough that even "MTN Data" only
    just fit.

    A clamp() answers "how wide is the screen", not "how long is this
    string" - "MTN Data" and "Startimes Smartcard subscription" both get
    the same font-size from the same viewport width, so clamp() alone
    cannot stop the longer one wrapping once the size is big enough to
    read. white-space:nowrap plus the actual shrink is JS - see
    fitCapText/ubFitText in ubox-v2.js, which measures the rendered width
    against the space available and scales the font-size down by exactly
    that ratio, once, per string. This rule only sets the SIZE IT SHRINKS
    FROM: the biggest a short name like "MTN Data" is ever shown at. */
.ub-lcd__cap {
    font-size: clamp(1rem, 4vw, 1.375rem);
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: rgba(96,165,250,.65);
    margin-bottom: .875rem;
    white-space: nowrap;
    overflow: hidden;
    max-width: 100%;
}
/*  The token must read as ONE line — it is copied off the screen and typed
    into a meter keypad, and a wrapped token invites transcription errors.
    A 20-digit token in groups of four is 24 characters, so the size is
    driven by viewport width and allowed to scroll rather than wrap. */
.ub-lcd__val {
    font-family: var(--mono); font-weight: 700;
    font-size: clamp(.875rem, 3.5vw, 1.375rem);
    letter-spacing: .04em;
    color: var(--lcd); text-shadow: 0 0 12px var(--lcd-glow);
    line-height: 1.3; font-variant-numeric: tabular-nums;
    white-space: nowrap;
    overflow-x: auto; overflow-y: hidden;
    padding-bottom: .125rem;
    scrollbar-width: none;
}
.ub-lcd__val::-webkit-scrollbar { display: none; }
.ub-lcd__sub { margin-top: .875rem; display: flex; justify-content: center; gap: 1.75rem; }
.ub-lcd__sub b { display: block; font-family: var(--mono); font-size: 1.25rem; color: var(--lcd); font-variant-numeric: tabular-nums; }
.ub-lcd__sub small { font-size: .6875rem; letter-spacing: .14em; text-transform: uppercase; color: rgba(96,165,250,.55); }

/* ---- THE SAME READOUT, FOR THE OTHER FOUR SERVICES ---------------------- */
/*  ⚠ THE METER FACE IS THE BEST THING ON THE ELECTRICITY PAGE, and the four
    service pages ended a purchase with a line of text at the bottom of a long
    card. Same moment, same money, completely different treatment.

    So they share .ub-lcd and change only what the DEVICE is: the thing the
    customer is about to look at next. Airtime and data land on a phone, a
    subscription lands on a television, a pin is a slip you keep.

    Modifiers rather than new components, deliberately. The scanline, the
    glow, the theme-following ground and the tabular figures are the house
    style for "this is a readout, not a paragraph", and every service should
    inherit the next improvement to it without being touched. */

/*  The value is a phrase on these, not a 20-digit token, so it may wrap.
    .ub-lcd__val is nowrap because an electricity token must never break
    across lines - a wrapped token gets mistyped into a meter keypad. */
.ub-lcd--say .ub-lcd__val {
    white-space: normal;
    font-size: clamp(1rem, 4vw, 1.5rem);
    letter-spacing: .01em;
}

/*  ⚠ THE MONEY, FIRST, IN THE SIZE .ub-lcd__val USED TO CARRY ALONE.

    Reported directly, from a real ₦5,000 airtime purchase: the old single
    line read "MTN airtime ₦5,000.00" — the naira figure buried at the end
    of a sentence about what it bought. .ub-lcd__amt takes that big
    treatment over for the amount alone; .ub-lcd__val--sub (below) demotes
    the network/package line under it to a second, smaller line the moment
    there is an amount to make room for. Empty and untouched by this on TV
    and education, which never ran an amount into that string to begin
    with — see the note on resultAmount in _ServiceResult.cshtml.

    ⚠ color/text-shadow deliberately match .ub-lcd__val's OWN reset further
    down this file (var(--brand-hi), no glow), not the dark-pinned
    var(--lcd)/lcd-glow the base rule above still carries — this is a new
    class with nothing else to reset it, and copying the glow would quietly
    reintroduce the "light theme has a dark, hard-to-read box" problem that
    reset exists to stay clear of. */
.ub-lcd--say .ub-lcd__amt {
    display: block;
    font-family: var(--mono); font-weight: 700;
    font-size: clamp(1.125rem, 4.5vw, 1.75rem);
    letter-spacing: .01em;
    color: var(--brand-hi); text-shadow: none;
    line-height: 1.25; font-variant-numeric: tabular-nums;
    margin-bottom: .25rem;
}
/*  Only applied when .ub-lcd__amt is also on screen — see the `css`
    binding in _ServiceResult.cshtml. Without an amount above it (TV,
    education) .ub-lcd__val keeps the size .ub-lcd--say .ub-lcd__val
    already gives it. */
.ub-lcd--say .ub-lcd__val--sub {
    font-size: clamp(.8125rem, 3vw, 1rem);
    font-weight: 600;
    opacity: .85;
}

/*  The device, drawn small above the readout. Line art rather than an image:
    it scales, it follows the theme, and it costs no request. */
.ub-lcd__dev {
    width: 40px; height: 40px;
    margin: 0 auto .75rem;
    color: var(--lcd);
    opacity: .75;
    display: block;
}
.ub-lcd__dev svg { width: 100%; height: 100%; }

/*  Who it went to, under the value: the number, the smartcard, the phone the
    pins were texted to. ⚠ ALWAYS SHOWN. The single most useful thing after a
    purchase is confirming it went to the right place, and it is the one
    detail somebody checks before they believe you. */
.ub-lcd__to {
    margin-top: .75rem;
    font-family: var(--mono);
    font-size: .9375rem;
    color: var(--lcd);
    font-variant-numeric: tabular-nums;
    opacity: .9;
}
.ub-lcd__to span {
    display: block;
    font-family: inherit;
    font-size: .6875rem; letter-spacing: .14em; text-transform: uppercase;
    color: rgba(96,165,250,.55);
    margin-bottom: .1875rem;
}

/*  Exam pins are the one case with MORE THAN ONE value: a parent buying three
    result checkers gets three. They stack, each on its own line, so none is
    lost in a run of characters. */
.ub-lcd__pins { display: grid; gap: .5rem; }
.ub-lcd__pins code {
    display: block;
    font-family: var(--mono); font-weight: 700;
    font-size: clamp(.875rem, 3.2vw, 1.25rem);
    color: var(--lcd); text-shadow: 0 0 12px var(--lcd-glow);
    letter-spacing: .06em;
}

/*  ⚠ ONE SIZE FOR EVERY "SOMETHING HAPPENED TO YOUR MONEY" BOX. Reported
    live 2026-08-13, mid a real stuck purchase: "all these bloody messages...
    paid not received, token not received, is so fucking small... they
    should all be uniform" — this had drifted to a different font-size on
    every box (.ub-warnbox .875rem, --preview .8125rem, .ub-alert .9375rem,
    .ub-small explicitly .875rem) because each was sized on its own against
    the box around it, never against each other. --ub-statusmsg-size is the
    one number all of them read from now, pitched close to the electricity
    token readout's own clamp (.ub-lcd__val, clamp(.875rem, 3.5vw, 1.375rem))
    since that was the explicit reference point given live: "as big as the
    electricity token, size and font". Body font-family, not .ub-lcd__val's
    monospace — these are sentences a customer reads, not digits they copy
    into a meter, and setting prose in the LCD mono face would make it
    harder to read, not easier. */
:root { --ub-statusmsg-size: clamp(1rem, 3.2vw, 1.25rem); }

.ub-warnbox { background: rgba(139,92,246,.09); border: 1px solid rgba(139,92,246,.28); border-radius: var(--r); padding: .875rem 1rem; margin-top: 1.125rem; font-size: var(--ub-statusmsg-size); line-height: 1.5; color: var(--text-2); }
.ub-warnbox b { color: var(--amber); display: block; }

/*  Inside the buy card, directly under the step rail. Tighter than the
    standalone version — it sits above a form rather than floating in a
    column, so the default 1.125rem top margin would open a gap between it
    and the rail it belongs to. Font-size no longer shrinks here — see
    --ub-statusmsg-size above; only the padding/margin stay tighter. */
.ub-warnbox--preview {
    margin-top: 0;
    margin-bottom: 1.125rem;
    padding: .75rem .875rem;
    font-size: var(--ub-statusmsg-size);
    line-height: 1.5;
}
.ub-warnbox--preview b { margin-bottom: .1875rem; }

/*  ---- KEY-CHANGE (RESET) TOKENS, BONUS TOKEN, DISCO RECEIPT ----
    Owner's ruling 2026-09-20: show everything the DisCo sends that a customer
    needs, at the right moment, only when it is actually sent. Same visual
    language as the LCD readout and .ub-warnbox above; every block is hidden
    (Knockout `visible:`) on an ordinary recharge.

    KCT block first, in --warn amber (attention, not error): these must be
    keyed into the meter BEFORE the recharge token or it will not load. */
.ub-kct {
    border: 1px solid var(--warn);
    background: color-mix(in srgb, var(--warn) 12%, var(--surface));
    border-radius: var(--r); padding: .85rem .9rem; margin-top: 1.125rem;
}
.ub-kct__h { display: flex; align-items: center; gap: .4rem; font-weight: 750; font-size: .8rem; letter-spacing: 0; white-space: nowrap; color: var(--warn); }
.ub-kct__sub { margin: .35rem 0 .7rem; font-size: .8rem; line-height: 1.45; color: var(--text-2); }
.ub-kct__list { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.ub-kct__list li { display: flex; align-items: center; gap: .6rem; }
.ub-kct__n { flex: none; width: 1.5rem; height: 1.5rem; border-radius: 50%; background: var(--warn); color: #2a1a04; font-weight: 800; font-size: .8rem; display: grid; place-items: center; }
.ub-kct__list code { flex: 1; font-family: var(--mono); font-size: 1.02rem; letter-spacing: .06em; color: var(--text); background: var(--ink); border: 1px solid var(--line); border-radius: 8px; padding: .5rem .6rem; user-select: all; word-break: break-all; }
.ub-kct__then { text-align: center; font-size: .72rem; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); margin: .9rem 0 .5rem; }

/*  Bonus token — positive, in the readout blue, never mistaken for a warning. */
.ub-bonus {
    margin-top: 1.125rem;
    border: 1px solid color-mix(in srgb, var(--lcd) 45%, var(--line));
    background: color-mix(in srgb, var(--lcd) 10%, var(--surface));
    border-radius: var(--r); padding: .8rem .9rem;
}
.ub-bonus__h { font-weight: 750; font-size: .86rem; color: var(--lcd); margin: 0 0 .35rem; }
.ub-bonus code { display: block; font-family: var(--mono); font-size: 1.02rem; letter-spacing: .06em; color: var(--text); background: var(--ink); border: 1px solid var(--line); border-radius: 8px; padding: .5rem .6rem; user-select: all; word-break: break-all; }
.ub-bonus small { display: block; margin-top: .35rem; font-size: .78rem; color: var(--text-2); }

/*  DisCo's own receipt number, muted, at the foot. */
.ub-rref { margin: 1.1rem 0 0; text-align: center; font-size: .75rem; color: var(--text-3); }
.ub-rref code { font-family: var(--mono); color: var(--text-2); user-select: all; }

.ub-alert { border-radius: var(--r); padding: .875rem 1rem; margin-bottom: 1rem; font-size: var(--ub-statusmsg-size); line-height: 1.5; }
.ub-alert--error { background: var(--danger-bg); border: 1px solid rgba(255,107,107,.3); color: var(--danger); }

/*  The transaction reference printed INSIDE the delayed-token banner
    (Index.cshtml, "contact support with this reference"). Every colour is a
    theme token — the chip sits on --danger-bg, which is a pale pink in light
    and a deep red in dark, so any hard-coded ink would be unreadable on one
    of the two grounds (the exact failure the 2026-08-16 both-themes audit
    exists to stop). --ink-2 / --line-hi / --text are the input-well trio and
    already carry both themes.

    user-select: all is the point of the element: one tap selects the whole
    reference, so a customer on a phone can copy it into a chat or an email
    without a fiddly long-press drag across 30 digits. */
.ub-refchip {
    display: inline-block;
    margin-top: .5rem;
    padding: .375rem .625rem;
    border-radius: calc(var(--r) / 2);
    background: var(--ink-2);
    border: 1px solid var(--line-hi);
    color: var(--text);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: .9375rem;
    font-weight: 600;
    letter-spacing: .02em;
    word-break: break-all;
    -webkit-user-select: all;
    user-select: all;
}

/* DisCo strip ------------------------------------------------------------ */
.ub-edcs { border-block: 1px solid var(--line); padding: 1.5rem 0; }
.ub-edcs__in { display: flex; align-items: center; gap: 1rem 2.25rem; flex-wrap: wrap; justify-content: center; }
.ub-edcs__lab { font-size: .6875rem; letter-spacing: .18em; text-transform: uppercase; color: var(--text-3); }
.ub-edcs__n { font-weight: 650; color: var(--text-2); font-size: .9375rem; letter-spacing: .02em; }

/* Sections --------------------------------------------------------------- */
.ub-sec { padding: clamp(2.5rem, 5vw, 4rem) 0; }
/*  ---- DO NOT WRAP TEXT THAT HAS ROOM TO RUN ----

    This was capped at 54ch, the classic readability measure. On a section
    heading and one supporting sentence that is simply too tight: "Most people
    finish before the kettle boils. The numbering is real — it's the actual
    order of the transaction." broke onto two lines with half the row empty to
    the right of it, which reads as an arbitrary break rather than a considered
    measure.

    54ch earns its keep on paragraphs of body copy. There are none here — the
    pattern is a heading and a single sentence, and both should use the space
    they have. 78ch keeps long-form readable while letting a sentence like that
    finish on one line at any desktop width.

    The heading is uncapped outright: a short bold line has no readability
    problem at any length, and wrapping "Three steps. That's the whole thing."
    is pure loss. */
.ub-sec__head { max-width: 78ch; margin-bottom: clamp(2rem, 4vw, 3rem); }
.ub-sec__head h2 {
    font-size: clamp(1.375rem, 2.8vw, 1.875rem);
    margin-bottom: .75rem;
    max-width: none;
    text-wrap: balance;   /* if it must wrap, break it evenly rather than leaving one orphan */
}
.ub-sec__head p { color: var(--text-2); font-size: .9375rem; max-width: none; }

.ub-steps { display: grid; gap: 1.25rem; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); counter-reset: ubstep; }
.ub-step { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 1.5rem; transition: border-color .2s, transform .2s; }
.ub-step:hover { border-color: var(--line-hi); transform: translateY(-2px); }
.ub-step::before {
    counter-increment: ubstep; content: counter(ubstep, decimal-leading-zero);
    font-family: var(--mono); font-size: .75rem; color: var(--amber);
    letter-spacing: .1em; display: block; margin-bottom: .875rem;
}
.ub-step h3 { font-size: 1.125rem; margin-bottom: .5rem; }
.ub-step p { color: var(--text-2); font-size: .9375rem; }

.ub-feats { display: grid; gap: 1.25rem; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.ub-feat { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 1.5rem; transition: border-color .2s, transform .2s; }
.ub-feat:hover { border-color: var(--line-hi); transform: translateY(-2px); }
.ub-feat__ico {
    width: 40px; height: 40px; border-radius: 11px; display: grid; place-items: center;
    background: rgba(139,92,246,.12); border: 1px solid rgba(139,92,246,.25);
    color: var(--amber); margin-bottom: 1rem;
}
.ub-feat__ico svg { width: 19px; height: 19px; }
.ub-feat h3 { font-size: 1.0625rem; margin-bottom: .5rem; }
.ub-feat p { color: var(--text-2); font-size: .9375rem; }

/* Panels / dashboard / history ------------------------------------------- */
.ub-panel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 1.5rem; margin-bottom: 1.25rem; }
.ub-panel__h { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1.25rem; }
.ub-panel__h h2 { font-size: 1.1875rem; }

/*  ---- WHAT IS IN THIS PULL-DOWN ----

    Arrow, then the purple dot, then the heading, then a line of plain English
    saying what is inside. Every collapsible card on the account page carries
    the same three, so a customer can tell what they are opening BEFORE they
    open it — a row of identical arrows with nothing but a noun beside them
    makes people click each one to find out.

    The dot is the site's mark, reused as a bullet: it ties these headings to
    the brand and gives the eye a fixed point to run down the page.
    flex: none, or it is squashed into an ellipse when the heading is long. */
.ub-disc__dot {
    flex: none;
    width: .5rem; height: .5rem; border-radius: 50%;
    background: var(--amber);
    box-shadow: 0 0 0 3px var(--brand-dim);
}
.ub-disc__t { display: flex; flex-direction: column; gap: .1875rem; min-width: 0; }
.ub-disc__t h2 { margin: 0; font-size: 1.1875rem; }

/*  ⚠ HIDDEN WHEN OPEN. Once the card is open its contents explain
    themselves, and the sentence becomes a line of noise above every list.
    It is there to help somebody decide to open it, not to stay. */
.ub-disc__n {
    margin: 0; font-size: .8125rem; color: var(--text-3); line-height: 1.5;
    font-weight: 400;
}
details[open] > .ub-panel__h .ub-disc__n,
details[open] > .ub-prof__h .ub-disc__n { display: none; }

.ub-meters { display: grid; gap: .875rem; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }
.ub-meter {
    background: var(--ink-2); border: 1px solid var(--line); border-left: 3px solid var(--amber);
    border-radius: var(--r); padding: 1rem; text-align: left; font: inherit; color: var(--text);
    cursor: pointer; display: flex; flex-direction: column; gap: .375rem; align-items: flex-start;
    transition: border-color .2s, transform .1s;
}
.ub-meter:hover { border-color: var(--line-hi); border-left-color: var(--amber-hi); }
.ub-meter:active { transform: translateY(1px); }
.ub-meter__no { font-family: var(--mono); font-weight: 650; font-size: 1rem; letter-spacing: .05em; }
.ub-meter__m { font-size: .8125rem; color: var(--text-3); }

.ub-stats { display: grid; gap: .875rem; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); }
.ub-stat { background: var(--ink-2); border: 1px solid var(--line); border-radius: var(--r); padding: 1.125rem; }
.ub-stat__l { font-size: .6875rem; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); }
/*  ⚠ FLUID, NOT FIXED — same fix and same reason as .uadm-kpi__v in
    ubox-admin.css. A fixed 1.625rem only "fit" because the figure sampled
    while building this was short; a real seven-figure naira total in a
    narrow .ub-stats tile (grid-template-columns: minmax(130px, 1fr), so a
    tile can be genuinely 130px wide) wraps or clips instead. Reaches its
    ceiling at ~520px viewport width so it keeps shrinking across real phone
    widths (375-430px) rather than sitting at the same size on all of them.
    max-width/overflow-wrap are the backstop for the rare figure that still
    doesn't fit even at the floor size, so it wraps cleanly instead of
    overflowing the card. */
.ub-stat__v { font-size: clamp(1rem, 5vw, 1.625rem); font-weight: 750; margin-top: .3125rem; font-variant-numeric: tabular-nums; letter-spacing: -.02em; max-width: 100%; overflow-wrap: anywhere; }
.ub-stat__v.acc { color: var(--amber); }

.ub-pill { display: inline-flex; padding: .1875rem .5625rem; border-radius: var(--r-pill); font-size: .6875rem; font-weight: 650; text-transform: uppercase; letter-spacing: .05em; }
.ub-pill--ok, .ub-pill--prepaid { background: var(--lcd-dim); color: var(--lcd); }
.ub-pill--pending, .ub-pill--postpaid { background: rgba(139,92,246,.13); color: var(--amber); }
.ub-pill--failed { background: var(--danger-bg); color: var(--danger); }

.ub-txs { list-style: none; margin: 0; padding: 0; }
.ub-tx { display: grid; grid-template-columns: 1fr auto; gap: .3125rem .875rem; padding: 1rem 0; border-bottom: 1px solid var(--line); }
.ub-tx:last-child { border-bottom: 0; }
.ub-tx__m { font-family: var(--mono); font-weight: 650; letter-spacing: .05em; }
.ub-tx__d { font-size: .8125rem; color: var(--text-3); }
.ub-tx__a { font-weight: 700; text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.ub-tx__t {
    grid-column: 1 / -1; margin-top: .5rem;
    font-family: var(--mono); font-size: .875rem; color: var(--lcd);
    background: var(--ink-2); border: 1px dashed rgba(96,165,250,.3); border-radius: 9px;
    padding: .625rem .75rem; display: flex; align-items: center; justify-content: space-between;
    gap: .75rem; word-break: break-all; letter-spacing: .06em;
}

.ub-tools { display: flex; flex-wrap: wrap; gap: .625rem; margin-bottom: 1.25rem; }
.ub-tools .ub-in, .ub-tools .ub-sel { flex: 1 1 170px; width: auto; min-height: 46px; }

.ub-empty { text-align: center; padding: 2.5rem 1rem; color: var(--text-3); }
.ub-empty h3 { color: var(--text); margin-bottom: .5rem; }

.ub-skeleton {
    background: linear-gradient(90deg, var(--surface-2) 25%, var(--line-hi) 50%, var(--surface-2) 75%);
    background-size: 200% 100%; animation: ub-shimmer 1.4s ease-in-out infinite;
    border-radius: 8px; height: 1rem;
}
@keyframes ub-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

/* Footer ------------------------------------------------------------------
   Reshaped 2026-08-12 to the shape the owner pointed at (github.com's own
   footer): a small mark and a copyright line on the left, a row of real
   links on the right, one border-top rule separating it from the page
   above. Nothing here is a new colour — --text-3 for the quiet stuff,
   --text-2 on hover, the same pair every other footnote on this site
   already uses. ------------------------------------------------------- */
.ub-foot { border-top: 1px solid var(--line); padding: 2.5rem 0; margin-top: 2rem; }
.ub-foot__in { display: flex; flex-wrap: wrap; gap: 1.25rem 2rem; align-items: center; color: var(--text-3); font-size: .875rem; }
.ub-foot a { color: var(--text-3); text-decoration: none; }
.ub-foot a:hover { color: var(--text-2); }

/*  Mark + copyright. The mark is the supplied PNG (_UBoxLogo "mark"),
    sized down from its 36px nav height and dimmed with opacity rather than
    a second colour token — a footer credit, not a second piece of nav. */
.ub-foot__brand { display: flex; align-items: center; gap: .625rem; }
.ub-foot__brand .ub-logo__mark { height: 20px; opacity: .55; }
.ub-foot__copy { white-space: nowrap; }

/*  The link row. Was .ub-foot__sp ("spacer") — renamed because it is now a
    real <nav>, not a spacer with links incidentally inside it.

    ⚠ WEIGHT AND "CURRENT PAGE" COLOUR MATCH THE HEADER NAV (2026-08-12) —
    the header's own .ub-nav__links a is 550, not the browser default 400,
    and marks the active link in --amber, not a second grey. The footer had
    drifted from both: plain inherited weight, and its own one-off
    text-2/650 treatment for "you are here" instead of the colour this site
    already uses for that everywhere else. Not the header's pill background
    though — that reads right against a 68px nav bar; here it would just be
    padding fighting the footer's plain-text rhythm. Same meaning, the shape
    that fits this row.

    ⚠ GAP, NOT LETTER-SPACING OR MARGIN. Before this the links had no gap
    declared at all, so at desktop widths they fell back to the browser's
    default inline whitespace between them — a single space character's
    width, the same gap you'd get between two words in a sentence. Reported
    plainly as "too close together". 1.5rem is not a new number invented for
    this: it is the same gap .ub-nav__in already uses for its own top-level
    row, and it is already what the mobile rule below falls back to as its
    column-gap once links wrap — this just makes it the value at every
    width, not only under 620px. */
.ub-foot__links { display: flex; align-items: center; gap: 1.5rem; }
.ub-foot__links a { font-weight: 550; }
.ub-foot__links a[aria-current="page"] { color: var(--amber); font-weight: 650; }
@media (max-width: 620px) {
    .ub-foot__in { flex-direction: column; align-items: flex-start; gap: 1.25rem; }
    .ub-foot__links { margin-left: 0; gap: 1.25rem 1.5rem; }
}

/* Utilities -------------------------------------------------------------- */
.ub-muted { color: var(--text-2); }
.ub-small { font-size: .875rem; }
.ub-center { text-align: center; }
.ub-mt-1 { margin-top: .75rem; }
.ub-mt-2 { margin-top: 1.25rem; }

@media print {
    #ub-current, .ub-nav, .ub-foot, .ub-btn, .ub-rail { display: none !important; }
    body { background: #fff; color: #000; }
    .ub-lcd { background: #fff; border: 1px solid #000; box-shadow: none; }
    .ub-lcd::after { display: none; }
    .ub-lcd__val, .ub-lcd__amt, .ub-lcd__sub b, .ub-lcd__cap { color: #000; text-shadow: none; }
}

/* ==========================================================================
   Progress veil — shown during meter checks, sign-in and account navigation
   so a wait never reads as "nothing happened".
   ========================================================================== */
.ub-veil {
    position: fixed; inset: 0; z-index: 200;
    background: rgba(11, 7, 22, .86);
    backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 1.25rem;
    opacity: 0; visibility: hidden;
    transition: opacity .22s ease, visibility .22s;
}
.ub-veil.on { opacity: 1; visibility: visible; }

/* Concentric arcs turning at different rates — reads as instrumentation
   rather than a generic throbber. */
.ub-veil__ring { position: relative; width: 62px; height: 62px; }
.ub-veil__ring i {
    position: absolute; inset: 0; border-radius: 50%;
    border: 2px solid transparent; display: block;
}
/*  The mark's own three — see the note on .ub-spin. Orange innermost, so the
    loudest colour in the palette stays the smallest arc. Seasonal overrides
    of --dot-1 and --dot-3 carry through here too. */
.ub-veil__ring i:nth-child(1) { border-top-color: var(--dot-1); animation: ub-spin 1s linear infinite; }
.ub-veil__ring i:nth-child(2) { inset: 9px; border-right-color: var(--dot-2); animation: ub-spin 1.5s linear infinite reverse; }
.ub-veil__ring i:nth-child(3) { inset: 18px; border-bottom-color: var(--dot-3); animation: ub-spin 2s linear infinite; }
/* Bigger. This is the only thing on screen while someone waits, and it was
   set smaller than body copy. */
.ub-veil__msg {
    color: var(--text); font-size: 1.0625rem; font-weight: 600;
    text-align: center; padding: 0 1.5rem;
}

/* ---- THE CONFIRM, IN THE MIDDLE OF THE SCREEN -------------------------- */
/*  ⚠ THE PRICE WAS THE LAST THING ON A LONG CARD.

    Step 2 rendered inline, below everything else in the buy card, so the
    total - the one number somebody must read before agreeing to pay - sat
    near the bottom of the page and often below the fold on a tablet. The
    decision was being asked for in the least visible place on the screen.

    Centred, so what is being bought and what it costs are the only things in
    front of somebody at the moment they commit. Same ground and blur as
    .ub-veil deliberately: this is the same visual system, not a second one,
    and it sits just under the veil's z-index so the spinner still covers it
    once Pay is pressed. */
.ub-confirmwrap {
    position: fixed; inset: 0; z-index: 190;
    background: rgba(11, 7, 22, .86);
    backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
    display: flex; align-items: center; justify-content: center;
    padding: 1.25rem;
    overflow-y: auto;
}

.ub-confirmwrap__box {
    /*  ⚠ THIS WAS var(--card) - A TOKEN THAT DOES NOT EXIST ANYWHERE IN THIS
        FILE. An unresolved var() with no fallback makes the whole
        `background` declaration invalid, so the box painted NO background at
        all: on light theme that put near-black text (--text) directly on
        the confirm sheet's near-black scrim, unreadable. --surface is the
        token every other card on the site uses (.ub-auth__card etc). */
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 16px;
    box-shadow: 0 24px 60px rgba(0, 0, 0, .45);
    width: min(30rem, 100%);
    max-height: calc(100vh - 2.5rem);
    overflow-y: auto;
    padding: 1.375rem 1.25rem;
}

/*  ⚠ THE PROVIDER, AT THE TOP. Asked for directly: the payment sheet already
    shows the DisCo, and the confirm should too, because "am I buying from the
    right place" is the question somebody answers before they read the price.
    The artwork is the vendor's own and is already loaded on the page, so this
    costs nothing to show. */
.ub-confirmwrap__who {
    display: flex; align-items: center; gap: .625rem;
    margin-bottom: .875rem;
    padding-bottom: .875rem;
    border-bottom: 1px solid var(--line);
}
.ub-confirmwrap__who img {
    height: 34px; width: auto; display: block;
    background: #FFFFFF; border-radius: 8px; padding: 3px;
}
.ub-confirmwrap__who b { font-size: 1rem; display: block; }
.ub-confirmwrap__who span { display: block; color: var(--text-3); font-size: .8125rem; }

@media (max-width: 480px) {
    .ub-confirmwrap { padding: .75rem .75rem 2rem; align-items: flex-start; padding-top: 1.5rem; }
    .ub-confirmwrap__box { padding: 1.125rem 1rem; }
}

/* Auth ------------------------------------------------------------------- */
/*  The card sits higher than it did.

    The top padding was up to 4rem, which pushed the card down far enough
    that "Buy without an account" — the way out for everyone who does not
    have one, and a good share of the traffic — fell below the fold on a
    laptop. Someone who cannot see that link concludes an account is
    required. Roughly halved at the top and tightened at the bottom, so the
    card and its alternative are on screen together. */
.ub-auth {
    max-width: 440px; margin: 0 auto;
    padding: clamp(1rem, 2.5vw, 1.75rem) 0 clamp(1.5rem, 3vw, 2rem);
}
.ub-auth__card {
    background: linear-gradient(168deg, var(--surface) 0%, var(--ink-2) 100%);
    border: 1px solid var(--line-hi); border-radius: var(--r-lg);
    box-shadow: var(--shadow); padding: clamp(1.5rem, 4vw, 2rem);
    position: relative; overflow: hidden;
}
.ub-auth__card::before {
    content: ''; position: absolute; inset: 0 0 auto 0; height: 1px;
    background: linear-gradient(90deg, transparent, rgba(139,92,246,.7), transparent);
}
/*  ---- ⚠ INSIDE A CARD, THE CARD IS THE MEASURE ----

    The 34ch cap on .ub-in and .ub-btn is right in an open column and wrong
    here: this card is already 440px wide and deliberately so, and capping the
    controls again left them short of its right edge with a gap beside every
    field.

    Worse, it is the shape that produced "the buttons are longer than the
    boxes" in the first place — controls sized by their own rules instead of
    by their container will always find a way to disagree. One measure, set by
    the card; everything in it fills that. */
/*  ⚠ THE BUTTON CAME OUT OF THIS LIST, 2026-08-29. The boxes still share
    one width and always will - a text field has no natural size, so the card
    has to give it one, and two inputs of different widths in one form is the
    thing this rule was written to stop.

    A button is not a box. It has a natural width: its label. Stretching
    "Sign in" to 380px because the email field above it is 380px is what the
    owner called out - "no human being is going to make a button that is
    longer than the information they need to put into it". So the boxes fill
    the card and the buttons hug their words. */
.ub-auth .ub-in,
.ub-auth .ub-sel,
.ub-auth .ub-pw { max-width: 100%; width: 100%; }

.ub-auth h1 { font-size: 1.625rem; margin-bottom: .5rem; }
.ub-auth__sub { color: var(--text-2); font-size: .9375rem; margin-bottom: 1.75rem; }
.ub-auth__alt { text-align: center; margin-top: 1.5rem; font-size: .9375rem; color: var(--text-3); }
.ub-auth__alt a { color: var(--amber); font-weight: 600; text-decoration: none; }
.ub-auth__alt a:hover { text-decoration: underline; }

.ub-tabs { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; background: var(--ink-2);
           border: 1px solid var(--line-hi); border-radius: var(--r-pill); padding: 4px; margin-bottom: 1.75rem; }
.ub-tabs button {
    min-height: 44px; border: 0; border-radius: var(--r-pill); font: inherit;
    font-size: .9375rem; font-weight: 600; color: var(--text-3);
    background: transparent; cursor: pointer; transition: all .2s;
}
.ub-tabs button.on { background: var(--amber); color: #FFFFFF; }

.ub-userchip {
    display: inline-flex; align-items: center; gap: .5rem;
    padding: .375rem .75rem .375rem .375rem; border-radius: var(--r-pill);
    background: var(--surface); border: 1px solid var(--line-hi);
    font-size: .875rem; font-weight: 550; color: var(--text-2);
}
.ub-userchip__av {
    width: 26px; height: 26px; border-radius: 50%; flex: none;
    background: linear-gradient(140deg, var(--brand), var(--brand-3));
    color: #FFFFFF; display: grid; place-items: center;
    font-size: .75rem; font-weight: 750;
}

/* Usage chart ------------------------------------------------------------ */
.ub-chart__top { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 1rem; margin-bottom: 1.25rem; }
.ub-chart__tot { font-size: 1.75rem; font-weight: 750; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.ub-chart__lab { font-size: .6875rem; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); }
.ub-chart__wrap { position: relative; width: 100%; }
.ub-chart__wrap canvas { display: block; width: 100%; }

.ub-switch { display: inline-flex; gap: 3px; background: var(--ink-2); border: 1px solid var(--line-hi); border-radius: var(--r-pill); padding: 3px; }
.ub-switch button {
    min-height: 32px; padding: 0 .75rem; border: 0; border-radius: var(--r-pill);
    font: inherit; font-size: .8125rem; font-weight: 600; color: var(--text-3);
    background: transparent; cursor: pointer; transition: all .18s;
}
.ub-switch button:hover { color: var(--text); }
.ub-switch button.on { background: var(--amber); color: #FFFFFF; }

.ub-live { display: inline-flex; align-items: center; gap: .375rem; font-size: .6875rem; letter-spacing: .1em; text-transform: uppercase; color: var(--lcd); }
.ub-live::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--lcd); box-shadow: 0 0 8px var(--lcd); animation: ub-pulse 2.4s ease-in-out infinite; }

/* DisCo grid with per-provider state ------------------------------------- */
.ub-discos { display: grid; gap: .75rem; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.ub-disco {
    display: flex; align-items: center; justify-content: space-between; gap: .75rem;
    background: var(--surface); border: 1px solid var(--line);
    border-radius: var(--r); padding: .8125rem 1rem;
}
.ub-disco__n { font-weight: 650; font-size: .9375rem; letter-spacing: .01em; }
.ub-disco__r { font-size: .75rem; color: var(--text-3); }
.ub-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.ub-dot--up { background: var(--lcd); box-shadow: 0 0 8px var(--lcd-glow); }

/* Meter digit counter */
.ub-count {
    font-family: var(--mono); font-size: .6875rem; font-weight: 600;
    color: var(--text-3); letter-spacing: .06em; flex: none;
    font-variant-numeric: tabular-nums; transition: color .2s;
}
.ub-count.done { color: var(--lcd); }

/* Account page header — compact, so the data starts near the top. */
.ub-acct__top { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1.25rem; flex-wrap: wrap; }
.ub-acct__h { font-size: 1.375rem; margin-bottom: .125rem; }

/*  The buy card on the account page.

    It reuses .ub-buy wholesale, so it is the same control a customer already
    knows from /v2 — same field sizing, same type scale, same naira alignment
    — rather than a second thing that behaves almost the same. What it adds is
    position: it is the first and widest thing under the heading, and the
    panels below it (history, usage, meters, totals) are visibly secondary.  */
/*  The buy card is its own measure and its Pay button spans it deliberately —
    it is the one action on that card and the card is already narrow. Opting
    back out of the 34ch cap, explicitly, so it is a decision rather than an
    oversight. */
/*  Was width:100%. See the note on .ub-btn - the button hugs its label
    here too. */
.ub-acct__buy .ub-btn--go { width: auto; max-width: 100%; }
/*  Both column headings — "Buy electricity" and "Account summary" — are this
    element, so they are the same size on the same baseline and the cards
    beneath them start level. line-height is fixed rather than inherited so a
    heading that wraps on a narrow screen still leaves the two columns
    aligned at the top. */
.ub-acct__buyh {
    font-size: 1.1875rem;
    line-height: 1.3;
    margin: 0 0 .875rem;
}

/*  The grid puts each heading at the top of its own column. Without this the
    flex column adds its own gap after the heading and doubles the spacing. */
.ub-dash .ub-acct__buyh { margin-bottom: 0; }
.ub-mt-sm { margin-top: .625rem; }

/*  Signed in, /v2 is a purchase screen, not a landing page.

    The pitch — headline, sales copy, "no account needed" — is aimed at
    someone who has not signed up. Once they have, it is noise sitting between
    them and the form, so it goes, and the buy card takes the middle of the
    page instead of half of it. */
/*  ⚠ THE HERO INTRO IS SHOWN TO EVERYONE, SIGNED IN OR NOT.

    It used to be `display: none` once signed in, on the reasoning that a
    customer on their fifth purchase does not need the pitch. That reasoning
    was about the PITCH and it quietly removed the HOME PAGE: a signed-in
    customer arriving at "/" got the buy form and nothing else, so pressing
    Home landed them in a form asking for a meter number. There was nowhere
    on this site a signed-in customer could go to be at home.

    A home page you cannot reach while signed in is not a home page. If the
    pitch is too loud for returning customers the answer is to soften the
    copy, not to delete the only landing surface they have. */
.is-signedin .ub-hero__intro { display: block; }

/*  The element carries BOTH .wrap and .ub-hero__grid, and .wrap sets
    max-width: var(--wrap). Both classes are named here so this outranks it
    without relying on source order. */
.is-signedin .wrap.ub-hero__grid {
    grid-template-columns: minmax(0, 1fr);
    max-width: 560px;
    margin-inline: auto;
}

/* Who you are signed in as. A quiet line, right-aligned, not a banner. */
.ub-acct__who {
    text-align: right; font-size: .8125rem; color: var(--text-3);
    margin: 0 0 .625rem;
}
/*  The greeting leads and the address follows it, quieter and on its own
    line — the name is the friendly part, the email is only there to confirm
    which account you are in. Where there is no name the email stays exactly
    as it was, with no modifier applied. */
.ub-acct__who span { display: block; }
.ub-acct__who span:first-child { color: var(--text); font-size: .9375rem; font-weight: 600; }
.ub-acct__who--sub { font-size: .75rem; }

/* ==========================================================================
   Account dashboard — two columns (2026-08-01)

   One column made every card the full page width. A three-field buy form
   stretched across 1100px looked broken, the chart was pushed under a long
   history ribbon, and nothing had any hierarchy because everything was the
   same size.

   Two columns, sized by what each card holds rather than uniformly: the
   things you ACT on are a narrow left rail, the things you LOOK at get the
   width where width actually helps.
   ========================================================================== */
.ub-dash {
    display: grid;
    grid-template-columns: minmax(300px, 355px) minmax(0, 1fr);
    gap: 1rem;
    align-items: start;
}
.ub-dash__side,
.ub-dash__main { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }

/*  ---- THE TWO COLUMNS FINISH LEVEL ----

    The headings already lined up at the top; the bottoms did not. The buy
    card is short and the stack of cards beside it is not, so the left column
    stopped a long way above the right one and the page ended on a ragged
    diagonal.

    align-items: start above is what caused it — it holds each column at its
    natural height. Stretching the row instead makes both columns the height
    of the taller one, and the buy card then grows to fill its column, so the
    two finish on the same line.

    ⚠ ONLY WHERE THERE ARE TWO COLUMNS. Below 1001px this is a single column
    and stretching would do nothing useful, so the media query matches the
    one that collapses the grid rather than guessing a second breakpoint. */
@media (min-width: 1001px) {
    .ub-dash { align-items: stretch; }

    /*  The heading keeps its natural height and the card takes the rest.
        Without this the column stretches but its contents do not, and the
        card stays exactly where it was. */
    .ub-dash__side > .ub-acct__buy {
        flex: 1 1 auto;

        /*  ⚠ BUT NOT WITHOUT A CEILING. Stretching alone is right only while
            the cards opposite are shut. Open the chart and the history and
            the right column goes past 2400px, and an unbounded card followed
            it: 2381px tall with 1810px of empty violet under the last line of
            content. That is the same metre of empty card that made the
            earlier layout look unfinished, just moved to the other column.

            So it fills its column up to about a screenful and stops. Shut —
            which is how the page opens, and how it is nearly always seen —
            the column is well inside that and the two sides finish level,
            which is the whole point. Open, the card stays a sensible size and
            the space below it is simply empty page, as it was before. */
        /*  ⚠ THE CEILING IS GONE, 2026-08-29. It existed because the chart
            and the history used to spring open on their own, which sent the
            right column past 2400px and dragged an unbounded buy card with
            it. Nothing opens itself any more (the openDetailsById calls are
            removed), so the shut state - which is now every first view - has
            both columns within a screenful and they finish exactly level,
            which is what was asked for. Opening a card by hand still grows
            the right column, and the left one follows it rather than
            stopping short on a diagonal. */
        min-height: 0;
    }
}

/* The grid owns the spacing; card margins would double it. */
.ub-dash .ub-panel,
.ub-dash .ub-acct__buy,
.ub-dash .ub-stats,
.ub-dash .ub-rewards { margin-bottom: 0; }

/* Denser than a full-width card needs to be. */
.ub-dash .ub-panel { padding: 1.125rem; }
.ub-dash .ub-panel__h { margin-bottom: .875rem; }
.ub-dash .ub-panel__h h2 { font-size: 1.0625rem; }

/*  The buy card is the primary action, so it carries a brand tint and a
    slightly stronger edge — the one card on the page that looks different
    on purpose. Everything else is a neutral surface. */
.ub-dash .ub-acct__buy {
    background: linear-gradient(155deg,
        color-mix(in srgb, var(--brand) 10%, var(--surface)),
        var(--surface));
    border-color: color-mix(in srgb, var(--brand) 28%, var(--line));
}
@supports not (background: color-mix(in srgb, red 50%, blue)) {
    .ub-dash .ub-acct__buy { background: var(--surface-2); }
}
/* Narrow column: the roomy /v2 sizing is wrong here. */
.ub-dash .ub-acct__buy { padding: 1.125rem; }
.ub-dash .ub-acct__buy .ub-f { margin-bottom: .875rem; }
.ub-dash .ub-acct__buy .ub-in,
.ub-dash .ub-acct__buy .ub-sel { min-height: 44px; font-size: .9375rem; }
.ub-dash .ub-acct__buy .ub-amt .ub-in { font-size: 1.125rem; padding-left: 2rem; }
.ub-dash .ub-acct__buy .ub-amt__ng { left: .8125rem; font-size: 1rem; }
.ub-dash .ub-acct__buy .ub-btn--go { min-height: 44px; font-size: .9375rem; }
.ub-dash .ub-acct__buy .ub-chip { padding: .3125rem .625rem; font-size: .8125rem; }

/*  History shows ten rows by default and pages from there — see `paged` in
    Account.cshtml. Capping the height and scrolling was the first attempt and
    it was the wrong one: it still built every row, so a thousand-row account
    still paid for a thousand DOM nodes to show you ten. */
.ub-dash .ub-tx { padding: .625rem 0; }
.ub-more {
    display: flex; gap: .5rem; justify-content: center;
    margin-top: .875rem; padding-top: .875rem; border-top: 1px solid var(--line);
}
.ub-dash .ub-tools { gap: .5rem; margin-bottom: .875rem; }
.ub-dash .ub-tools .ub-in,
.ub-dash .ub-tools .ub-sel { flex: 1 1 130px; min-height: 38px; font-size: .8125rem; }

/*  Summary tiles sit at the top of the wide column now, so they run as one
    row of four rather than a 2x2 block — a glance, not a panel. */
.ub-dash .ub-stats { gap: .5rem; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.ub-dash .ub-stat { padding: .625rem .75rem; }
.ub-dash .ub-stat__v { font-size: 1.0625rem; }

/*  ---- THE FIGURE STAYS ON ONE LINE ----

    On a phone these tiles were wrapping: "over 4,692.2 kWh" broke across two
    lines inside a box sized for one, and the label above it did the same. A
    summary tile that wraps stops being a glance.

    Three things together, rather than only shrinking the type:
      - the value scales with the viewport and is told not to wrap;
      - the uppercase label loses its wide letter-spacing, which is what was
        pushing "TOTAL SPENT" onto two lines in a 150px box;
      - two columns instead of three, so each box has room to begin with.

    `min-width: 0` on the tile matters: without it a grid item refuses to
    shrink below its content and the row overflows instead of fitting. */
@media (max-width: 640px) {
    .ub-dash .ub-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .4375rem; }
    .ub-dash .ub-stat { padding: .5rem .625rem; min-width: 0; }
    .ub-dash .ub-stat__l {
        font-size: .625rem; letter-spacing: .06em;
        white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    }
    .ub-dash .ub-stat__v {
        font-size: clamp(.875rem, 3.6vw, 1.0625rem);
        white-space: nowrap;
        margin-top: .1875rem;
    }
    /*  The qualifier under a figure — "3 of 97 purchases have no kWh" — is
        the one thing here that SHOULD wrap. It is a sentence, not a number. */
    .ub-dash .ub-stat__n { font-size: .625rem; line-height: 1.35; }
}

/*  Small phones: one figure per row beats two cramped ones. */
@media (max-width: 380px) {
    .ub-dash .ub-stats { grid-template-columns: 1fr; }
    .ub-dash .ub-stat__v { font-size: 1.125rem; }
}

/* ---- colour: the dot cluster, applied ----------------------------------
   Each tile takes a 3px rule in one of the mark's four colours, and each
   panel heading a dot in the same set. Decorative only — nothing here
   carries meaning by colour alone, and no text sits ON these, so there is
   no contrast requirement to fail. ------------------------------------- */
.ub-dash .ub-stat { position: relative; overflow: hidden; }
.ub-dash .ub-stat::before {
    content: ''; position: absolute; inset: 0 0 auto 0; height: 3px;
    background: var(--dot, var(--brand));
}
.ub-dash .ub-stats .ub-stat:nth-child(1) { --dot: var(--dot-1); }
.ub-dash .ub-stats .ub-stat:nth-child(2) { --dot: var(--dot-2); }
.ub-dash .ub-stats .ub-stat:nth-child(3) { --dot: var(--dot-3); }
.ub-dash .ub-stats .ub-stat:nth-child(4) { --dot: var(--dot-4); }

.ub-dash .ub-panel__h h2::before {
    content: ''; display: inline-block; vertical-align: middle;
    width: .5rem; height: .5rem; border-radius: 50%;
    margin-right: .5rem; background: var(--dot, var(--brand));
}
/*  ⚠ BUMPED BY ONE ON 2026-08-06, and it is not cosmetic drift. The
    "we cannot reach your history" banner was added as the FIRST child of
    .ub-dash__main, and nth-child counts every child regardless of type — so
    every panel below it moved up a position and these two stopped matching
    what they were written for.

    The shift is constant, not conditional: the banner is always in the DOM
    and Knockout's `visible` binding only toggles display, and a
    display:none element still counts as a child. So there is no state in
    which these are off by a different amount.

    ⚠ POSITIONAL SELECTORS ARE THE REAL PROBLEM HERE. Anything added to the
    top of that column breaks them again, silently, and the only symptom is
    a dot the wrong colour. If these need touching a third time, give the
    panels their own classes instead of counting them. */
.ub-dash__main .ub-panel:nth-child(2) { --dot: var(--dot-2); }  /* usage   */
.ub-dash__main .ub-panel:nth-child(3) { --dot: var(--dot-3); }  /* history */
.ub-dash__side .ub-panel               { --dot: var(--dot-4); } /* meters  */

/*  ---- "WE CANNOT REACH YOUR HISTORY" ----

    Shown only when the account data could not be read. Deliberately NOT
    styled as an error: nothing the customer did is wrong, nothing is lost,
    and buying is unaffected. It is an amber notice, not a red alarm.

    ⚠ BUILT FROM THEME VARIABLES SO IT WORKS IN BOTH. Light mode has shipped
    with dark boxes on it more than once, by hardcoding a colour that only
    suited the dark palette. --brand-dim, --line-hi and --text are defined in
    both blocks, so this follows whichever is in force with no second rule. */
.ub-note {
    padding: .9rem 1.1rem;
    border-radius: .75rem;
    border: 1px solid var(--line-hi);
    background: var(--brand-dim);
    color: var(--text);
    font-size: .95rem;
    line-height: 1.6;
}
.ub-note strong { font-weight: 650; }

/*  The amber accent is a left edge rather than a background, so the text
    keeps the contrast the theme already guarantees. */
.ub-note--warn { border-left: 4px solid var(--amber-hi); }

/*  Rewards was another purple card among purple cards. It is the one warm
    thing on the page now, which is also what it is: a small bonus, not a
    control. */
.ub-dash .ub-rewards {
    background: linear-gradient(140deg,
        color-mix(in srgb, var(--dot-3) 14%, var(--surface)),
        var(--surface));
    border-color: color-mix(in srgb, var(--dot-3) 30%, var(--line));
}
@supports not (background: color-mix(in srgb, red 50%, blue)) {
    .ub-dash .ub-rewards { background: var(--surface-2); }
}
.ub-dash .ub-rewards__v.acc { color: var(--dot-3); }

/*  Below 1000px the two halves are each too narrow to read, so it becomes
    one column again — and the side rail goes SECOND, because on a phone the
    chart and history are what you scroll for once the buy card is done. */
@media (max-width: 1000px) {
    .ub-dash { grid-template-columns: 1fr; }
    .ub-dash .ub-txs--scroll { max-height: none; }
}

@media (max-width: 940px) {
    .ub-hero__grid { gap: 1.5rem; }
    .ub-hero { padding-top: 1rem; }
    .ub-proof { gap: 1.25rem; }
}

/*  ================= HERO ORDER ON SMALL SCREENS =================

    THIS IS THE ONLY PLACE THE HERO ORDER IS SET. Do not add another.

    On 2026-07-31 this block put the BUY CARD first below 940px, after
    feedback that the page made you scroll before you could type a meter
    number. Reversed on 2026-08-03 after the opposite feedback: on a phone
    you meet a meter field with no idea what happens next, and the
    explanation — "Electricity credits, all hours" and the numbered steps —
    only turns up if you scroll past it.

    WHY AN EARLIER ATTEMPT AT THIS DID NOTHING. I added a competing block
    2,000 lines further up. It lost twice over: `> div:first-child` here is
    (0,2,1) against my (0,2,0), and this rule is later in the file anyway.
    Media queries add no specificity. That is the third time this exact trap
    has cost a round — the nav and the hero headline were the same — so the
    rule now lives in ONE place with this note on it.

    700px, not 940px. A tablet in portrait has room for the card to sit near
    the top with the intro beside it, and was never the problem. Phones are.

    DOM order stays intro-then-card, so this only matters visually: tab order
    and screen readers get the natural reading order either way.  */
@media (max-width: 700px) {
    /*  ---- display: contents, and this is the whole trick ----

        The statistics row and the DisCo strip live INSIDE .ub-hero__intro,
        so ordering the intro as one block could only ever move all four
        things together — headline, steps, statistics, logos — above or below
        the card. Neither is what is wanted: the headline and steps belong
        ABOVE, the statistics and logos BELOW.

        `display: contents` makes the intro's box disappear while keeping its
        children, so they become grid items in their own right and can each
        take their own order. No markup change, and the desktop layout is
        untouched because this only applies under 700px.

        Selectors still match the real DOM tree — the children are still
        children of .ub-hero__intro as far as CSS is concerned — which is why
        these read `.ub-hero__intro > …` rather than `.ub-hero__grid > …`. */
    .ub-hero__intro { display: contents; }

    /*  Catch-all FIRST, so anything later added to the intro lands with the
        instructions rather than jumping to the front. Named items below
        override it — same specificity, later wins, which is the one cascade
        rule this file keeps getting caught by. */
    .ub-hero__intro > *               { order: 4; }

    .ub-hero__intro > .ub-eyebrow     { order: 1; }
    .ub-hero__intro > .ub-hero__h1    { order: 2; }
    .ub-hero__intro > .ub-lede        { order: 3; }
    .ub-hero__intro > .ub-hero__steps { order: 4; }

    /*  The card, straight after the instructions that explain it. */
    .ub-hero__grid  > .ub-buy         { order: 5; }

    /*  Then the supporting material, in the order it earns attention: proof
        we have done this before, then who we cover. Both are worth having,
        and neither is worth putting between a customer and the control they
        came to use.

        padding-top is reset because the DisCo strip carries a large top pad
        for the desktop layout, where it is pushed to the floor of the text
        column. Here it simply follows the statistics. */
    .ub-hero__intro > .ub-trust       { order: 6; margin-top: 1.5rem; }
    .ub-hero__intro > .ub-discos      { order: 7; margin-top: 1rem; padding-top: 0; }
}

/* ==========================================================================
   Payment backdrop (2026-07-31)

   Sits BEHIND Quidly's payment widget. Quidly mounts plain divs into our
   document — no iframe in embedouter.js or quidly-pay.js — with a `qw-`
   prefix, so the surrounding chrome is ours to brand. We deliberately do not
   style `qw-*` elements: those class names are not a public API and a silent
   break there would break card entry.
   ========================================================================== */
.ub-payback {
    position: fixed; inset: 0; z-index: 150;
    background: rgba(11, 7, 22, .94);
    backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
    opacity: 0; visibility: hidden;
    transition: opacity .25s ease, visibility .25s;
}
.ub-payback.on { opacity: 1; visibility: visible; }

.ub-payback__bar {
    position: absolute; top: 0; left: 0; right: 0;
    display: flex; align-items: center; gap: .875rem;
    padding: .875rem 1.25rem;
    background: var(--surface);
    border-bottom: 1px solid var(--line-hi);
}
.ub-payback__amt  { font-size: 1.0625rem; font-weight: 750; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.ub-payback__what { font-size: .75rem; color: var(--text-3); font-family: var(--mono); letter-spacing: .04em; }
.ub-payback__lock {
    margin-left: auto; display: inline-flex; align-items: center; gap: .375rem;
    font-size: .6875rem; letter-spacing: .12em; text-transform: uppercase; color: var(--lcd);
}
.ub-payback__lock svg { width: 13px; height: 13px; }
/*  ⚠ THE SERVICE LINE STAYS VISIBLE ON PHONES — 2026-08-17. This rule
    used to hide .ub-payback__what under 480px "for space", which meant a
    phone customer paying saw the logo and the amount but never WHAT they
    were paying for ("MTN Airtime · 0803 123 4567") — exactly the line
    that makes the payment sheet trustworthy, and the owner noticed its
    absence immediately. The bar wraps to a second line instead: the
    what-line takes the full width underneath the logo/amount row. Only
    the lock's caption text still yields — the padlock icon alone says
    "secure" without costing a word of what is being bought. */
@media (max-width: 480px) {
    .ub-payback__lock span { display: none; }
    .ub-payback__bar { flex-wrap: wrap; row-gap: .25rem; }
    .ub-payback__what { flex-basis: 100%; order: 5; }
}

/* --------------------------------------------------------------------------
   OUR loading state, not Quidly's (2026-08-02)

   Between initialize() and their sheet rendering there is a gap, and Quidly
   fill it with their own spinner on a white panel. On a dark branded site that
   reads as a broken page — and when their API is slow it is the only thing the
   customer sees, sometimes for a long time.

   WHAT IS SAFE TO DO HERE, AND WHAT IS NOT.

   Their widget mounts into OUR document (no iframe, verified in the DOM), so
   every one of their elements is reachable. That does not make all of it fair
   game. The FORM was restyled once and the fields became unusable: `qw-*`
   class names are not a public API, and a silent rename ships a payment form
   nobody can type into. So the form is still left completely alone.

   The LOADING state is different. Hiding a spinner cannot break card entry —
   the worst case if they rename it is that both spinners show, which is ugly
   and not broken. That asymmetry is the whole justification for treating the
   two differently.

   `.is-waiting` is removed by a MutationObserver the moment their sheet
   actually renders, so ours is never on screen at the same time as their form.
   -------------------------------------------------------------------------- */

/*  Suppress theirs while ours is up.

    A PLAIN CLASS ON <html>, NOT `body:has(...)`.

    This was written with three `body:has(.ub-payback.is-waiting) …` rules,
    and :has() is the most expensive selector there is: the engine must
    re-evaluate it against the subtree on every DOM change. Quidly's mount is
    thousands of insertions, so every one of them re-ran three :has() checks —
    the second half of why the payment sheet became slow to appear. The first
    half was a forced layout per mutation in the observer; see payBackdrop.wait.

    JS toggles `ub-paywait` on the root element instead. Same effect, and the
    cost is a single class change. */
.ub-paywait #qw-loading-spinner,
.ub-paywait .qw-loading,
.ub-paywait .qw-spinner { display: none !important; }

/* Belt and braces for browsers without :has() — their ring at least takes our
   colour rather than defaulting to a stock blue. */
.qw-spin-ring { border-top-color: var(--brand) !important; }

/* ==========================================================================
   ⚠⚠ THEIR SHEET DOES NOT FOLLOW OUR THEME. DO NOT MAKE IT. ⚠⚠

   Every colour rule that was here is gone, and the reason is worth keeping.

   I themed their fields and their panel with our tokens so the sheet would go
   dark with the site. The result was BLACK BOXES WITH INVISIBLE WRITING: our
   background token landed on their input while their own text colour stayed
   where it was, and the two met in the middle. On the payment screen. On the
   card fields.

   That is the third time restyling this sheet has damaged it — e8dc7b3
   reverted by ebf1ecb, then a `qw-` prefix attempt, then this.

   THE RULE NOW, AND IT IS NOT A GUIDELINE: their sheet keeps its own colours.
   Light theme, dark theme, it does not matter — Quidly designed a pale panel
   with dark text and it is legible exactly as they built it. We do not set
   background, color, border or anything else that decides whether a customer
   can read what they are typing.

   IF IT IS EVER TO BE BRANDED, QUIDLY MUST DO IT. Their initialize() takes no
   theme option — checked across all three of their loaders — so it is a
   request to them, not a stylesheet change here.

   What remains below is SIZE ONLY, and size cannot make text unreadable.
   ========================================================================== */

/*  ⚠ NO SIZING HERE EITHER, AND NOT BECAUSE SIZING IS UNSAFE.

    I re-added the sizing from 47099f2 at this point without checking, and
    this file ALREADY sizes their sheet properly further down — with dvh
    rather than vh so it follows the browser chrome, safe-area insets for a
    notch, and box-sizing so a phone cannot scroll sideways. Mine was the
    older, worse version of the same thing.

    Two blocks setting the same properties means the later one wins, which
    would have been the SIXTH time that has caught this stylesheet today. The
    good one lives with the rest of the mobile work — search
    `#quidly-overlay-root`. There is exactly one place their sheet is sized.  */

/*  ⚠ THEIR PAY BUTTON IS LEFT ALONE ON PURPOSE. It is the control that
    takes the money, it is already high-contrast blue, and it is the one
    element where a colour mistake costs a sale rather than looking wrong.
    Their brand marks and logo are left alone for the same reason: they are
    artwork, not chrome. */

.ub-payback__wait {
    position: absolute; inset: 0;
    display: none;
    flex-direction: column; align-items: center; justify-content: center;
    gap: 1rem;
    text-align: center; padding: 1.5rem;
}
.ub-payback.is-waiting .ub-payback__wait { display: flex; }

/*  ⚠ WHILE WE ARE SETTING UP, THAT MESSAGE IS ALONE ON THE SCREEN.

    The frame — heading, card outline, trust content — was rendering behind
    the "Opening the secure payment window" state, so the shell for a sheet
    that did not exist yet sat behind the message saying it was being
    prepared. Two screens at once, one of them describing the other.

    Their own widget sets up and waits on a clean screen. Ours does the same
    now: the frame appears only once their sheet has actually rendered, which
    is the moment the MutationObserver in payBackdrop.wait drops .is-waiting. */
.ub-payback.is-waiting .ub-payback__frame { display: none; }

/*  ---- THE SHELL AROUND QUIDLY'S SHEET ----------------------------------

    Their modal mounts into this document and floats in the middle. This
    frames it: the step rail and heading above, the trust content below, and
    a deliberately EMPTY middle for their sheet to land in.

    ⚠ pointer-events: none on the frame, and nothing inside it is clickable.
    It sits at the same z-index as their modal and a transparent panel over a
    payment form that silently swallows clicks is the worst possible bug here.
    Their sheet stays reachable because this cannot receive a click at all.

    ⚠ NOTHING HERE SELECTS A `qw-` CLASS. Their form is left alone: those
    names are not a public API and restyling them broke card entry once
    already. Only the surroundings are ours. */
.ub-payback__frame {
    position: absolute; inset: 0;
    display: flex; flex-direction: column; align-items: center;
    padding: 4.5rem 1.25rem 1.25rem;
    overflow-y: auto;
    pointer-events: none;
}

.ub-payback__steps {
    list-style: none; margin: 0 0 1.5rem; padding: 0;
    display: flex; align-items: center; gap: 1.25rem; flex-wrap: wrap;
    justify-content: center;
}
.ub-payback__steps li { display: flex; align-items: center; gap: .625rem; opacity: .45; }
.ub-payback__steps li.is-done, .ub-payback__steps li.is-now { opacity: 1; }
.ub-payback__steps b {
    width: 30px; height: 30px; border-radius: 50%; flex: none;
    display: grid; place-items: center;
    background: var(--surface-2); border: 1px solid var(--line-hi);
    font-size: .8125rem; font-weight: 700;
}
.ub-payback__steps li.is-now b { background: var(--brand); border-color: transparent; color: #fff; }
.ub-payback__steps span { display: flex; flex-direction: column; font-size: .875rem; font-weight: 650; line-height: 1.25; }
.ub-payback__steps small { font-weight: 400; font-size: .75rem; color: var(--text-3); }

.ub-payback__head { text-align: center; margin-bottom: 1.25rem; }
.ub-payback__head h2 { margin: 0 0 .25rem; font-size: 1.5rem; letter-spacing: -.02em; }
.ub-payback__head p  { margin: 0; color: var(--text-3); font-size: .9375rem; }

/*  ---- THE CARD THEIR SHEET SITS IN ----

    Where their sheet lands, drawn as one of OUR cards so the payment step
    looks like the rest of the site rather than a panel dropped on top of it.
    Same surface, same border, same radius as .ub-buy.

    Their modal floats above this and is centred, so the card reads as its
    frame. We do not move their node into it: reparenting a live payment
    widget to win a border is not a trade worth making.

    ⚠ THE FIT IS UNVERIFIED AGAINST THEIR REAL SHEET. Proving it needs a live
    payment, so max-width and min-height here are sized from the supplied
    design, not measured. If their modal is wider or taller, these two numbers
    are the ones to change and nothing else.

    min-height is modest on purpose: it only stops the footer riding up under
    their sheet. A 22rem floor pushed the footer off an ordinary laptop. */
/*  flex: none, NOT `0 1 auto`. As a shrinkable item the column squeezed it to
    128px however large min-height was, because a flex item's automatic
    minimum does not stop the container distributing negative free space.
    The card must hold its size and let the frame scroll instead. */
.ub-payback__slot {
    flex: none;
    width: 100%; max-width: 34rem;
    min-height: 21rem;
    margin: 0 auto;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 16px;
    box-shadow: 0 18px 50px rgba(0,0,0,.28);
    display: flex; flex-direction: column; justify-content: flex-end;
    padding: 1rem;
}

/*  Named in the card, because the customer is handing card details to a
    company whose name they should see. Type rather than artwork: there is no
    Quidly mark in this repository. Drop one at
    wwwroot/images/quidly.svg and swap this line for an <img>. */
.ub-payback__slot::after {
    content: 'Secured by Quidly';
    display: block; text-align: center;
    font-size: .6875rem; letter-spacing: .1em; text-transform: uppercase;
    color: var(--text-3);
}
@media (max-height: 700px) { .ub-payback__slot { min-height: 15rem; } }

.ub-payback__foot { width: 100%; max-width: 46rem; margin-top: 1.25rem; }
.ub-payback__trust {
    list-style: none; margin: 0 0 1rem; padding: 0;
    display: flex; gap: 1.5rem; flex-wrap: wrap; justify-content: center;
}
.ub-payback__trust li { display: flex; align-items: flex-start; gap: .5rem; max-width: 15rem; }
.ub-payback__trust svg { width: 18px; height: 18px; color: var(--brand-2, var(--lcd)); flex: none; margin-top: 2px; }
.ub-payback__trust span { display: flex; flex-direction: column; font-size: .8125rem; color: var(--text-3); line-height: 1.45; }
.ub-payback__trust b { color: var(--text); font-size: .875rem; }

.ub-payback__accept {
    display: flex; align-items: center; justify-content: center; gap: .625rem;
    flex-wrap: wrap; padding-top: 1rem; border-top: 1px solid var(--line);
}
.ub-payback__accept-l { font-size: .75rem; color: var(--text-3); }
.ub-payback__accept em {
    font-style: normal; font-size: .75rem; font-weight: 700; letter-spacing: .04em;
    padding: .25rem .5rem; border-radius: 6px;
    background: var(--surface-2); border: 1px solid var(--line-hi); color: var(--text-2);
}

.ub-payback__by { margin: .875rem 0 0; text-align: center; font-size: .75rem; color: var(--text-3); }
.ub-payback__by b { color: var(--text-2); }

/*  ---- SHRINK IN STAGES, AND MEASURE THE THRESHOLDS ----

    The first version dropped the step rail AND the whole footer below 760px
    tall. A perfectly ordinary laptop viewport is 679px, so it hid both on the
    machine it was being looked at on - the shell rendered as a heading and
    nothing else, which reads as the feature not working.

    Three stages now, each removing the least useful thing first, and their
    sheet keeps the middle throughout. */

/*  Trim the padding and the descriptions first: the step rail still reads as
    1-2-3 without its captions. */
@media (max-height: 820px) {
    .ub-payback__frame  { padding-top: 4rem; }
    .ub-payback__steps  { margin-bottom: 1rem; gap: 1rem; }
    .ub-payback__head   { margin-bottom: .875rem; }
    .ub-payback__head h2 { font-size: 1.3125rem; }
    .ub-payback__trust  { gap: 1rem; margin-bottom: .75rem; }
}

/*  Then the reassurance, which is the part somebody reads once. The step rail
    tells them where they are and stays longer. */
/*  ⚠ The slot's own height belongs with the slot, NOT here. This block used
    to set min-height: 8rem as well, and being later in the file it beat the
    15rem the slot sets for the same viewport - so the card rendered 128px
    tall whatever the rule beside it said.

    Fifth time in this stylesheet that a later rule of equal specificity has
    quietly won. One property, one home. */
@media (max-height: 700px) {
    .ub-payback__trust { display: none; }
}

/*  Last, everything but the bar and the heading. */
@media (max-height: 580px) {
    .ub-payback__steps, .ub-payback__foot { display: none; }
    .ub-payback__frame { padding-top: 3.75rem; }
}

/*  The same three-dot ring the rest of the site uses while it waits, so the
    payment step does not introduce a fourth kind of spinner. */
.ub-payback__ring {
    position: relative; width: 54px; height: 54px;
}
/*  ---- THE SAME RING AS EVERYWHERE ELSE ----

    This had its own colours (brand / brand-3 / spark), its own easing
    (cubic-bezier) and all three arcs turning the same way. Beside the veil
    ring — which a customer sees seconds earlier, on the way to this screen —
    it read as a different site's spinner.

    Now identical to the veil: same three colours, same linear timing, same
    reversed middle arc. Only the size differs, and that is the point. */
.ub-payback__ring i {
    position: absolute; inset: 0;
    border: 2px solid transparent;
    border-top-color: var(--dot-1);
    border-radius: 50%;
    animation: ub-spin 1s linear infinite;
}
.ub-payback__ring i:nth-child(2) {
    inset: 8px;
    border-top-color: transparent;
    border-right-color: var(--dot-2);
    animation: ub-spin 1.5s linear infinite reverse;
}
.ub-payback__ring i:nth-child(3) {
    inset: 16px;
    border-top-color: transparent;
    border-bottom-color: var(--dot-3);
    animation: ub-spin 2s linear infinite;
}
@keyframes ub-payspin { to { transform: rotate(360deg); } }

.ub-payback__wait-msg { font-size: .9375rem; color: var(--text-2); margin: 0; }
.ub-payback__wait-sub { font-size: .8125rem; color: var(--text-3); margin: 0; max-width: 26rem; }

@media (prefers-reduced-motion: reduce) {
    .ub-payback__ring i { animation-duration: 3s; }
}

/* Reward points strip (2026-07-31) — mirrors the Points / Cash Value / redeem
   block on the production dashboard. */
.ub-points {
    display: flex; align-items: center; gap: 1.25rem; flex-wrap: wrap;
    background: linear-gradient(120deg, rgba(139,92,246,.1), rgba(96,165,250,.06));
    border: 1px solid rgba(139,92,246,.28);
    border-radius: var(--r); padding: .875rem 1.125rem; margin-bottom: 1rem;
}
.ub-points__l { font-size: .6875rem; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); }
.ub-points__v { font-size: 1.375rem; font-weight: 750; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.ub-points__v.acc { color: var(--amber); }
.ub-points__sep { width: 1px; align-self: stretch; background: var(--line-hi); }
.ub-points__btn { margin-left: auto; }


/* Meter cards with name + address + last token (2026-07-31) */
.ub-meter__row  { display: flex; align-items: center; justify-content: space-between; gap: .625rem; width: 100%; }
.ub-meter__name { font-size: .8125rem; font-weight: 650; color: var(--text); }
.ub-meter__addr {
    font-size: .75rem; color: var(--text-2); line-height: 1.4;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.ub-meter__tok  { font-size: .6875rem; color: var(--text-3); }
.ub-meter__tok b { font-family: var(--mono); color: var(--lcd); font-weight: 600; letter-spacing: .04em; }

.ub-chart__sub { font-size: .6875rem; color: var(--text-3); margin-top: .1875rem; }

/* Density pass (2026-07-31): less scrolling on the account page. */
.ub-panel        { padding: 1.125rem; margin-bottom: .875rem; }
.ub-panel__h     { margin-bottom: .875rem; }
.ub-panel__h h2  { font-size: 1rem; }
.ub-stat         { padding: .75rem .875rem; }
.ub-stat__v      { font-size: 1.25rem; }
.ub-stats        { gap: .625rem; }
.ub-points       { padding: .625rem .875rem; gap: 1rem; margin-bottom: .875rem; }
.ub-points__v    { font-size: 1.0625rem; }
.ub-chart__tot   { font-size: 1.375rem; }
.ub-tx           { padding: .75rem 0; }
.ub-acct__h      { font-size: 1.1875rem; }

/* Chart empty-state sentence (2026-07-31) — an empty measure explains itself
   instead of rendering a blank axis. */
.ub-chart__none {
    font-size: .8125rem; color: var(--text-3);
    text-align: center; padding: 1.5rem 1rem 0;
}

/* ==========================================================================
   Support: clickable actions vs reference facts (2026-07-31)
   Every card in .ub-feats--actions does something on tap, so it gets an
   affordance. Opening hours / registered office are reference, not actions,
   and sit below in a plain definition list.
   ========================================================================== */
.ub-feats--actions { margin-bottom: 1.25rem; }
.ub-feats--actions .ub-feat { position: relative; padding-right: 2.5rem; }
.ub-feats--actions .ub-feat::after {
    content: '›'; position: absolute; right: 1.125rem; top: 1.125rem;
    font-size: 1.375rem; line-height: 1; color: var(--amber);
    transition: transform .15s;
}
.ub-feats--actions .ub-feat:hover::after { transform: translateX(3px); }

.ub-facts {
    display: grid; gap: .625rem 2rem; margin: 0 0 2rem;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.ub-facts dt { font-size: .6875rem; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); margin-bottom: .1875rem; }
.ub-facts dd { margin: 0; font-size: .8125rem; color: var(--text-2); }

/* ==========================================================================
   Meters as compact expandable rows (2026-07-31)
   Was a grid of large cards. Most customers have one meter; the detail is
   behind a disclosure so the list stays one line per meter.
   ========================================================================== */
.ub-mlist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
.ub-mrow {
    display: flex; align-items: flex-start; gap: .625rem;
    background: var(--ink-2); border: 1px solid var(--line);
    border-left: 3px solid var(--amber); border-radius: var(--r);
    padding: .625rem .75rem;
}
.ub-mrow details { flex: 1; min-width: 0; }
.ub-mrow summary {
    display: flex; align-items: center; gap: .625rem; flex-wrap: wrap;
    cursor: pointer; list-style: none; padding: .25rem 0;
}
.ub-mrow summary::-webkit-details-marker { display: none; }
/*  ⚠ ▾ / ▴, THE SAME TRIANGLE "Your details" USES. This was '⌄' — a
    lightweight chevron — while .ub-prof used a solid triangle and
    .ub-ownerbox drew a third shape out of two CSS borders. Three different
    marks for one gesture on one page, which reads as three different kinds
    of control rather than one repeated. See .ub-prof for the canonical pair;
    every disclosure on this site now uses it. */
.ub-mrow summary::after {
    content: '▾'; margin-left: auto; color: var(--text-3);
    font-size: .875rem; line-height: 1;
}
.ub-mrow details[open] summary::after { content: '▴'; }
.ub-mrow__no  { font-family: var(--mono); font-weight: 650; font-size: .9375rem; letter-spacing: .05em; }
.ub-mrow__edc { font-size: .75rem; color: var(--text-3); }
.ub-mrow__buy { flex: none; margin-top: .125rem; }

.ub-mrow__body { padding: .625rem 0 .25rem; border-top: 1px solid var(--line); margin-top: .375rem; }
.ub-mrow__body dl { margin: 0; display: grid; gap: .5rem; }
.ub-mrow__body dt { font-size: .625rem; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); }
.ub-mrow__body dd { margin: .125rem 0 0; font-size: .8125rem; color: var(--text-2); }
.ub-mrow__tok { font-family: var(--mono); color: var(--lcd) !important; letter-spacing: .05em; }

/* ==========================================================================
   Density pass 2 (2026-07-31) — reported as "everything is big, big".
   ========================================================================== */
.ub-sec              { padding: clamp(1.5rem, 3vw, 2.25rem) 0; }
.ub-sec__head        { margin-bottom: 1.25rem; }
.ub-sec__head h2     { font-size: clamp(1.1875rem, 2.2vw, 1.5rem); }
.ub-panel            { padding: 1rem; margin-bottom: .75rem; border-radius: var(--r); }
.ub-panel__h h2      { font-size: .9375rem; }
.ub-stat             { padding: .625rem .75rem; }
.ub-stat__v          { font-size: 1.125rem; }
.ub-stat__l          { font-size: .625rem; }
.ub-chart__tot       { font-size: 1.25rem; }
.ub-feat             { padding: 1rem; border-radius: var(--r); }
.ub-feat h3          { font-size: .9375rem; }
.ub-feat p           { font-size: .8125rem; }
.ub-feat__ico        { width: 32px; height: 32px; margin-bottom: .625rem; }
.ub-feat__ico svg    { width: 16px; height: 16px; }
.ub-step             { padding: 1.125rem; border-radius: var(--r); }
.ub-step h3          { font-size: 1rem; }
.ub-step p           { font-size: .875rem; }
.ub-steps, .ub-feats { gap: .75rem; }
.ub-acct__top        { margin-bottom: .875rem; }

/* Meter picker — one select, most-recently-used first (2026-07-31).
   Replaced the card grid: customers have one meter, occasionally two. */
.ub-mpick { display: flex; gap: .5rem; align-items: center; }
.ub-mpick .ub-sel { flex: 1; min-width: 0; font-family: var(--mono); letter-spacing: .04em; }
.ub-mdetail {
    display: grid; gap: .625rem 1.5rem; margin: .875rem 0 0;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.ub-mdetail dt { font-size: .625rem; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); }
.ub-mdetail dd { margin: .1875rem 0 0; font-size: .8125rem; color: var(--text-2); min-height: 1.2em; }

/* Common problems are four independent issues, not a sequence — no numbering. */
.ub-steps--plain { counter-reset: none; }
.ub-steps--plain .ub-step::before { content: none; }

/* Our own chat launcher. tawk's bubble lives in their iframe and cannot be
   recoloured from here, so we hide theirs and show this instead. */
.ub-chatbubble {
    position: fixed; right: 20px; bottom: 20px; z-index: 145;
    width: 52px; height: 52px; border-radius: 50%;
    border: none; cursor: pointer;
    background: linear-gradient(150deg, var(--amber-hi), var(--amber));
    color: #FFFFFF;
    box-shadow: 0 6px 22px rgba(139,92,246,.4);
    display: grid; place-items: center;
    transition: transform .15s, box-shadow .2s;
}
.ub-chatbubble:hover { transform: translateY(-2px); box-shadow: 0 10px 28px rgba(139,92,246,.5); }
.ub-chatbubble svg { width: 22px; height: 22px; }
@media (max-width: 480px) { .ub-chatbubble { right: 14px; bottom: 14px; width: 48px; height: 48px; } }

/* Density pass 3 (2026-07-31) — still reported as too much scrolling. */
.ub-hero          { padding: 1rem 0 1.25rem; }
.ub-hero__grid    { gap: 1.25rem; }
.ub-hero h1       { font-size: clamp(1.5rem, 3.2vw, 2.125rem); margin-bottom: .625rem; }
.ub-lede          { font-size: .9375rem; margin-bottom: 1rem; }
.ub-proof         { gap: 1.125rem; }
.ub-proof__n      { font-size: 1.0625rem; }
.ub-proof__l      { font-size: .6875rem; }
.ub-buy           { padding: 1rem; }
.ub-buy__head     { margin-bottom: .75rem; }
.ub-rail          { margin-bottom: 1rem; }
.ub-f             { margin-bottom: .875rem; }
.ub-sec           { padding: 1.25rem 0; }
.ub-sec__head     { margin-bottom: 1rem; }
.ub-lines         { padding: .375rem .875rem; margin-bottom: .875rem; }
.ub-line          { padding: .375rem 0; font-size: .875rem; }

/* Our own support panel (2026-07-31).
   tawk's panel lives in their iframe and cannot be themed from here, so this
   is ours — same tokens, same type. tawk stays available behind
   "Talk to an agent now" for people who want a live human. */
.ub-chatpanel {
    position: fixed; right: 20px; bottom: 84px; z-index: 146;
    width: min(340px, calc(100vw - 40px));
    background: var(--surface); border: 1px solid var(--line-hi);
    border-radius: var(--r-lg); box-shadow: var(--shadow);
    opacity: 0; visibility: hidden; transform: translateY(10px);
    transition: opacity .2s, transform .2s, visibility .2s;
    overflow: hidden;
}
.ub-chatpanel.on { opacity: 1; visibility: visible; transform: none; }
.ub-chatpanel__bar {
    display: flex; align-items: center; gap: .625rem;
    padding: .75rem .875rem; border-bottom: 1px solid var(--line);
    background: var(--ink-2);
}
.ub-chatpanel__bar b     { display: block; font-size: .875rem; }
.ub-chatpanel__bar small { font-size: .6875rem; color: var(--text-3); }
.ub-chatpanel__x {
    margin-left: auto; background: none; border: 0; cursor: pointer;
    color: var(--text-3); font-size: 1.375rem; line-height: 1; padding: 0 .25rem;
}
.ub-chatpanel__x:hover { color: var(--text); }
.ub-chatpanel__body { padding: .875rem; }
.ub-chatpanel__ta { resize: vertical; min-height: 84px; font-family: inherit; line-height: 1.5; }
@media (max-width: 480px) { .ub-chatpanel { right: 12px; left: 12px; bottom: 76px; width: auto; } }

/* Meter the chart is scoped to, shown in the panel header (2026-07-31). */
.ub-chart__meter {
    font-family: var(--mono); font-size: .75rem; color: var(--amber);
    letter-spacing: .05em; margin-left: auto;
}

/* ==========================================================================
   Density pass 4 (2026-07-31) — cards still reported as too large.
   ========================================================================== */
.ub-buy            { padding: .875rem; border-radius: var(--r); }
.ub-buy__head h1,
.ub-buy__head h2   { font-size: 1.0625rem; }
.ub-hero h1        { font-size: clamp(1.375rem, 2.6vw, 1.75rem); }
.ub-hero           { padding: .75rem 0 1rem; }
.ub-lede           { font-size: .875rem; margin-bottom: .875rem; max-width: 42ch; }
.ub-in, .ub-sel    { min-height: 42px; padding: .5rem .75rem; font-size: .9375rem; }
.ub-in--meter      { font-size: 1.0625rem; letter-spacing: .09em; }
.ub-amt .ub-in     { font-size: 1.125rem; }
.ub-btn            { min-height: 42px; font-size: .875rem; padding: .5rem 1rem; }
.ub-seg label      { min-height: 36px; font-size: .875rem; }
.ub-lab            { font-size: .75rem; margin-bottom: .375rem; }
.ub-lab span       { font-size: .6875rem; }
.ub-panel          { padding: .875rem; }
.ub-stat           { padding: .5rem .625rem; }
.ub-stat__v        { font-size: 1rem; }
.ub-chart__tot     { font-size: 1.125rem; }
.ub-feat           { padding: .875rem; }
.ub-step           { padding: .875rem; }
.ub-f              { margin-bottom: .75rem; }
.ub-assure         { font-size: .6875rem; margin-top: .75rem; }

/* Estimated-units note (2026-07-31) — shown when kWh is derived rather than
   reported by the DisCo. */
.ub-chart__est {
    font-size: .6875rem; color: var(--text-3);
    text-align: center; padding-top: .5rem; font-style: italic;
}

/* Chart skeleton overlays the canvas rather than replacing it: a hidden
   canvas has zero width, and drawing into a zero-width canvas produces
   nothing — which looked like the chart flashing on then vanishing. */
.ub-chart__wrap { position: relative; min-height: 200px; }
.ub-chart__skel { position: absolute; inset: 0; border-radius: var(--r); }

/* ==========================================================================
   Transaction rows, restructured 2026-07-31
   Reads: when and where first, meter beneath, figures right-aligned so the
   numbers form a clean column. kWh sits immediately left of the amount.
   ========================================================================== */
.ub-tx { grid-template-columns: 1fr auto; align-items: start; }
.ub-tx__main  { min-width: 0; }
.ub-tx__when  { display: flex; align-items: baseline; gap: .5rem; font-size: .8125rem; font-weight: 600; }
.ub-tx__edc   { color: var(--text-3); font-weight: 500; font-size: .75rem; }
.ub-tx__m     { font-family: var(--mono); font-size: .8125rem; color: var(--text-3);
                font-weight: 500; letter-spacing: .04em; margin-top: .125rem; }
.ub-tx__figs  { display: flex; flex-direction: column; align-items: flex-end; gap: .25rem; }
.ub-tx__row   { display: flex; align-items: baseline; gap: .625rem; }
.ub-tx__u     { font-size: .75rem; color: var(--lcd); font-variant-numeric: tabular-nums; white-space: nowrap; }
.ub-tx__a     { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; font-size: .9375rem; }
.ub-tx__rate  { font-size: .6875rem; color: var(--text-3); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Hero heading and the buy-card heading are the same weight of statement,
   so they are the same size. */
.ub-buy__head h1,
.ub-buy__head h2 { font-size: clamp(1.375rem, 2.6vw, 1.75rem); letter-spacing: -.03em; }

/* Usage panel trimmed further. */
.ub-chart__wrap  { min-height: 160px; }
.ub-chart__tot   { font-size: 1.0625rem; }
.ub-chart__lab   { font-size: .625rem; }
.ub-chart__sub   { font-size: .625rem; }
.ub-panel__h h2  { font-size: .875rem; }

/* Anchor targets must clear the sticky header, or jumping to #meters lands
   with the panel heading hidden underneath it. Header is 68px. */
[id] { scroll-margin-top: 84px; }

/* Payment failure diagnostic — the numbers a customer can read back to
   support instead of describing symptoms (2026-07-31). */
.ub-diag { margin-top: .75rem; }
.ub-diag summary {
    cursor: pointer; font-size: .75rem; color: var(--text-3);
    padding: .25rem 0; list-style: none;
}
.ub-diag summary::-webkit-details-marker { display: none; }
/*  ▾ / ▴, matching .ub-prof — see the note there. */
.ub-diag summary::before { content: '▾ '; }
/*  ⚠ .ub-diag IS THE <details>, NOT A WRAPPER ROUND ONE. This read
    `.ub-diag details[open]`, which asks for a details element INSIDE
    .ub-diag - there has never been one, so the rule never matched and the
    marker never changed. The panel showed its closed chevron while standing
    open, which is exactly the wrong signal on a box somebody is being asked
    to open. Same fault, same fix, in the later override below. */
.ub-diag[open] > summary::before { content: '▴ '; }
.ub-diag pre {
    margin: .5rem 0 0; padding: .625rem .75rem;
    background: var(--ink-2); border: 1px solid var(--line);
    border-radius: 8px; font-family: var(--mono); font-size: .6875rem;
    color: var(--text-2); white-space: pre-wrap; word-break: break-word;
}

/* Idle sign-out notice */
.ub-idlebar {
    background: rgba(139,92,246,.1); border: 1px solid rgba(139,92,246,.3);
    color: var(--amber); border-radius: var(--r);
    padding: .625rem .875rem; margin: 1rem 1.25rem 0; font-size: .8125rem;
    max-width: var(--wrap); margin-inline: auto;
}

/* Rewards strip (2026-07-31). No redeem control until the backend has one —
   a disabled button is a promise you cannot keep. */
.ub-rewards {
    display: grid; grid-template-columns: 1fr auto; gap: 1rem;
    align-items: center;
    background: linear-gradient(120deg, rgba(139,92,246,.12), rgba(96,165,250,.06));
    border: 1px solid rgba(139,92,246,.28);
    border-radius: var(--r); padding: .75rem .9375rem; margin-bottom: .875rem;
}
.ub-rewards__cash { text-align: right; padding-left: 1rem; border-left: 1px solid var(--line-hi); }
.ub-rewards__l    { font-size: .625rem; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); }
.ub-rewards__v    { font-size: 1.25rem; font-weight: 750; font-variant-numeric: tabular-nums; letter-spacing: -.02em; margin-top: .125rem; }
.ub-rewards__v.acc { color: var(--amber); }
.ub-rewards__rate { font-size: .625rem; color: var(--text-3); margin-top: .1875rem; }

/*  ---- OWNER-ONLY PANELS ----

    Work that is finished enough to look at but not finished enough to
    ship. Deliberately does NOT look like the rest of the page: a dashed
    edge and a neutral ground, so it reads as scaffolding rather than as a
    feature, and so it is obvious at a glance if one ever leaks to a
    customer. */
.ub-ownerbox {
    border: 1px dashed var(--line-hi);
    border-radius: var(--r);
    background: var(--surface-2);
    padding: .9375rem;
    margin-bottom: .875rem;
}
.ub-ownerbox__h {
    display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
    font-size: .875rem; font-weight: 700; color: var(--text-2);
    margin-bottom: .75rem;
}
.ub-ownerbox__tag {
    font-size: .5625rem; font-weight: 700; letter-spacing: .1em;
    text-transform: uppercase; color: var(--text-3);
    border: 1px solid var(--line-hi); border-radius: 999px;
    padding: .1875rem .4375rem; line-height: 1;
}
.ub-ownerbox__dl { margin: 0 0 .75rem; font-size: .8125rem; }
.ub-ownerbox__dl > div {
    display: grid; grid-template-columns: 12rem 1fr; gap: .5rem;
    padding: .3125rem 0; border-top: 1px solid var(--line);
}
.ub-ownerbox__dl > div:first-child { border-top: 0; }
.ub-ownerbox__dl dt { color: var(--text-3); }
.ub-ownerbox__dl dd { margin: 0; color: var(--text-2); }
.ub-ownerbox code {
    font-size: .8125em; background: var(--surface); padding: .0625rem .25rem;
    border-radius: 4px; border: 1px solid var(--line);
}
.ub-ownerbox__note {
    font-size: .8125rem; line-height: 1.55; color: var(--text-2);
    margin: 0 0 .625rem;
}
.ub-ownerbox__note:last-child { margin-bottom: 0; }
.ub-ownerbox__note b { color: var(--text); }

/*  On a phone the label column has nowhere to go, so it stacks. */
@media (max-width: 560px) {
    .ub-ownerbox__dl > div { grid-template-columns: 1fr; gap: .125rem; }
}

/* A declined payment is not a warning, it is a failure — different colour so
   it cannot be mistaken for "we took your money and are working on it".

   ⚠ CONTAINED, NOT FLOATING — same fix as .ub-warnbox--good got on
   2026-08-12 (see the note on that rule below), applied here because it was
   never given the same treatment: reported live as "still a centred/
   floating bar" while the green success box had already been pulled into
   the card. The base .ub-warnbox's margin-top: 1.125rem is sized for a
   warning that opens a gap below unrelated content above it; here the box
   is the FIRST thing in the result card (same slot .ub-warnbox--good
   occupies on the success path — see step 3 in Index.cshtml and its
   siblings), so that gap read as space between the card edge and a bar
   that did not belong to it, the same "floating in a column" problem
   .ub-warnbox--preview was already fixed for, just above. Zeroing it seats
   the box flush against the top of the card exactly like --good's does. */
.ub-warnbox--bad {
    background: var(--danger-bg);
    border-color: rgba(255,107,107,.35);
    margin-top: 0;
    margin-bottom: 1.125rem;
}
/*  ⚠ DIRECT CHILD ONLY (`>`), NOT A DESCENDANT SELECTOR. The failure box on
    five of the six service pages nests _SupportRoute.cshtml inside it once
    the outcome carries `support: true`, and that partial has its own
    <b>Your reference:</b> line. A bare `.ub-warnbox--bad b` (no `>`) reached
    that nested <b> too — same failure red, same ✕ glyph in front of a
    reference label that is not itself a failure, caught by screenshotting
    the airtime case (Bundles/TV/Education/InternationalAirtime share the
    same markup shape) before this landed. `>` scopes the heading treatment
    to the box's own <b>, exactly one level in, and leaves
    _SupportRoute's colour (inherited var(--text-2), unchanged) alone. */
.ub-warnbox--bad > b {
    color: var(--danger);
    display: flex; align-items: center; gap: .4375rem;
}
/*  The same icon-in-front-of-the-heading language as --good's checkmark,
    red instead of green — a bare heading read as plain text next to the
    icon-led success box, so failure now gets an equivalent mark. A ::before
    glyph rather than an <svg>, because every failure heading across all
    six service pages already renders from this one CSS class with no
    markup change required — same reasoning as the icon-free
    .ub-warnbox--preview above, minimal footprint, applies uniformly
    everywhere at once. */
.ub-warnbox--bad > b::before {
    content: '\2715';
    font-size: .8125em;
    line-height: 1;
    flex: none;
}

/*  SUCCESS AND FAILURE ARE THE SAME BOX, and that is the point.
    The gateway announced success in its own centred sheet and then closed,
    while a failure appeared in .ub-warnbox inside the card — one outcome in
    two different places, only one of which we design. Success is a modifier
    on the same component now, so both land in the same spot in the card.

    It FADES rather than vanishing: it is a confirmation, not a state, and
    what matters next is the token. Collapsing the margin too stops the
    layout jumping as it goes.

    ⚠ GREEN, NOT --lcd BLUE. Reported live 2026-08-12 as "not green" — this
    box was built reusing --lcd/--lcd-dim (the token-readout blue) rather
    than a real success colour, so every payment confirmation across all
    five services rendered blue. See --good/--good-bg above, next to --lcd,
    for why that pair exists — and, since 2026-08-16, why it is themed per
    ground rather than the flat admin #22C55E it started as. */
.ub-warnbox--good {
    background: var(--good-bg);
    border-color: rgba(34,197,94,.35);
    margin-top: 0;
    margin-bottom: 1.125rem;
    display: flex; align-items: center; gap: .625rem;
    transition: opacity .6s ease, transform .6s ease,
                margin-bottom .6s ease, padding .6s ease;
}
.ub-warnbox--good b { color: var(--good); display: inline; }
.ub-warnbox--good svg { width: 18px; height: 18px; color: var(--good); flex: none; }
.ub-warnbox--good.is-going {
    opacity: 0; transform: translateY(-4px);
    margin-bottom: 0; padding-block: 0;
}
@media (prefers-reduced-motion: reduce) {
    .ub-warnbox--good { transition: none; }
}

/*  ---- WHAT WE ARE STILL DOING FOR A CUSTOMER WHO IS OWED A TOKEN ----

    Sits under the "payment received, token not here yet" box and narrates the
    settle check as it runs, so that screen is never one that has simply
    stopped moving. Same warnbox colours, which come from the theme variables
    and so hold up in light and dark alike — the only additions are the row
    and the spinner beside the text.

    ⚠ The flex row lives HERE rather than in a style attribute. Knockout's
    `visible` binding reveals an element by setting style.display to '', so an
    inline display would be erased the first time the box was shown, and this
    box spends its whole life being shown and hidden. */
.ub-settlenote {
    display: flex; align-items: flex-start; gap: .625rem;
}
/*  The spinner keeps its own size — see the note on .ub-spin. align-items is
    flex-start so it sits with the first line rather than the middle of a
    paragraph that wraps to three. */
.ub-settlenote .ub-spin { margin-top: .125rem; }

/* Token panel trimmed to match (2026-07-31) — it was the tallest thing on
   the success screen for no good reason. */
.ub-lcd          { padding: 1rem .875rem; }
/*  Font-size is left to the clamp() at the base rule — it already answers
    "how narrow is the screen"; only the margin needed its own mobile value.
    See the note there for why size no longer has a separate mobile figure. */
.ub-lcd__cap     { margin-bottom: .5rem; }
.ub-lcd__sub     { margin-top: .625rem; gap: 1.5rem; }
.ub-lcd__sub b   { font-size: 1rem; }
.ub-lcd__sub small { font-size: .625rem; }

/* Chart hover / tap readout (2026-07-31).
   A bar chart shows the shape; the customer wants the figure for a month. */
.ub-tip {
    position: absolute; bottom: 100%; margin-bottom: .375rem;
    width: 110px; padding: .4375rem .5rem;
    background: var(--surface); border: 1px solid var(--line-hi);
    border-radius: 8px; box-shadow: var(--shadow);
    pointer-events: none; opacity: 0; transition: opacity .12s;
    text-align: center; z-index: 5;
}
.ub-tip.on   { opacity: 1; }
.ub-tip__p   { font-size: .625rem; color: var(--text-3); letter-spacing: .06em; text-transform: uppercase; }
.ub-tip__v   { font-size: .9375rem; font-weight: 750; font-variant-numeric: tabular-nums; margin: .0625rem 0; }
.ub-tip__n   { font-size: .625rem; color: var(--text-3); }

/* Estimated units are marked, never passed off as measured. */
.ub-tx__u.est { color: var(--text-3); }

/* The transaction just purchased, when arriving from the token screen. */
.ub-tx--new {
    background: rgba(96,165,250,.06);
    box-shadow: inset 3px 0 0 var(--lcd);
    padding-left: .625rem; border-radius: 6px;
}

/* Secondary note under a stat figure. */
.ub-stat__n { font-size: .625rem; color: var(--text-3); margin-top: .125rem; }

/* ==========================================================================
   Brand lockup (2026-07-31)
   Mark plus wordmark, drawn inline in _UBoxLogo.cshtml. "Utility" takes the
   page text colour and "Box" the brand purple, matching the on-dark lockup
   from the brand sheet.
   ========================================================================== */
.ub-logo { display: inline-flex; align-items: center; gap: .5rem; }

/*  ⚠ THE MARK IS AN IMAGE NOW, AND IT IS NOT SQUARE.

    ubox-mark.png is the supplied artwork at 392x448 (12 Aug 2026 re-extraction).
    The old rule set width
    AND height to 30px, which was right for a square inline SVG and would
    squash the real file. Height drives it; width follows. */
.ub-logo__markwrap {
    position: relative; flex: none; display: inline-flex; line-height: 0;
}
/*  36px, up from 30. Asked for on 7 Aug: "a bit bigger, not too massive."
    The supplied mark is taller than it is wide, so at the old 30px it read
    smaller than the square redraw it replaced even though it was the same
    height. */
.ub-logo__mark {
    height: 36px; width: auto; flex: none; display: block;
}

/*  Seasonal dressing sits over the supplied artwork rather than being drawn
    into it. Square, matching the old 44x44 viewBox the decorations were
    positioned against, and anchored to the mark's top-left where they were
    designed to sit. */
.ub-logo__dress {
    position: absolute; left: 0; top: 0;
    height: 36px; width: 36px; pointer-events: none;
}
.ub-logo__word {
    font-weight: 750; letter-spacing: -.025em; font-size: 1.0625rem;
    color: var(--text); white-space: nowrap;
}
.ub-logo__word span { color: var(--brand); }

/*  ---- ADMIN-LINKED ACCOUNT BADGE, CUSTOMER NAV ----

    Shown when the signed-in customer's email is on the SAME allowlist that
    decides who may hold an /admin account (Services/AdminUsers.cs,
    checked in _UBoxV2.cshtml) - purely a visual cue, grants nothing.

    .uadm-badge is copied character-for-character from the admin layout's own
    "admin" badge (ubox-admin.css) rather than redesigned, per the owner's
    own instruction to match that style. It reads correctly in both themes
    because --brand-hi / --brand-dim / --line-hi are already themed tokens
    (see the :root blocks further down this file), the same as everywhere
    else that trio is used.

    Stacked under the wordmark, same as admin: it costs no extra horizontal
    space, so the nav does not get tighter on a phone just because one
    account happens to be admin-linked. .ub-nav__in .ub-brand.is-admin is
    written at (0,3,0) specifically so it beats the mobile clamp() rule
    for .ub-nav__in .ub-brand further down this file, which shares the
    property but not the flex-direction. */
.ub-brand.is-admin { flex-direction: column; align-items: flex-start; gap: .125rem; }
.ub-nav__in .ub-brand.is-admin { gap: .125rem; }
.ub-brand.is-admin .uadm-badge { margin-inline-start: calc(36px + .5rem); }
.uadm-badge {
    font-size: .6875rem;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--brand-hi);
    background: var(--brand-dim);
    border: 1px solid var(--line-hi);
    border-radius: 999px;
    padding: .15rem .5rem;
    white-space: nowrap;
}

/* Hero headline picks up the brand gradient rather than the action colour —
   the accent is reserved for things you press. */
.ub-hero h1 em {
    background: linear-gradient(100deg, var(--brand) 0%, #6366F1 45%, var(--brand-2) 100%);
    -webkit-background-clip: text; background-clip: text; color: transparent;
}

/* Focus rings follow the brand, not the action colour. */
:focus-visible { outline-color: var(--brand); }

@media (max-width: 420px) {
    .ub-logo__word { font-size: .9375rem; }
    .ub-logo__mark  { height: 31px; width: auto; }
    .ub-logo__dress { height: 31px; width: 31px; }
}

/* ==========================================================================
   Purple leads, white speaks (2026-07-31)

   Reworked after the first pass made orange the loudest thing on screen,
   when on the mark it is six small dots. Actions now carry the brand
   gradient with white text — the same two colours that dominate the logo.
   The orange and red survive only as --spark, at dot scale.
   ========================================================================== */

/*  Primary action: the mark's own gradient, white label.

    ⚠ REPORTED 2026-08-11: "the continue button, review purchase — they need
    to be bright... maybe glowing... some of the continue buttons look
    grayed out." These already carried the brand gradient, but the rest-state
    shadow was a faint 20px/.32-alpha smudge — barely visible on either
    theme's ground, so an ENABLED button and the .is-blocked/.4-opacity one
    right next to it (above, .ub-btn.is-blocked) read as two fades of the
    same purple rather than "off" vs. "press this". The glow now shows at
    rest, not only on :hover, and is wide/bright enough to be the loudest
    thing on the card — which is what tells a customer this is the one
    control they're meant to press. The blocked/disabled states are
    deliberately NOT touched here: they lose the glow entirely (see above),
    so brightening this can only widen the gap, never blur it.

    `:not(.is-blocked)` on the hover rule matters, not decoration — without
    it, `.ub-btn--go:hover` and `.ub-btn.is-blocked:hover` have equal
    specificity and this one, being later in the file, would win and put
    the glow back on a hovered blocked button.

    ⚠ RE-REPORTED SAME DAY as still gray — see the long note on
    .ub-btn.is-blocked, above, for the full re-investigation. Short version:
    this glow is fine, verified by injecting these exact rules over the live
    site and screenshotting a real halo. The box the owner was looking at
    was still running the box-shadow this replaced, because it had never
    been rebuilt since d5c20b1 shipped. */
.ub-btn--go {
    background: linear-gradient(135deg, var(--brand) 0%, var(--brand-2) 55%, var(--brand-3) 100%);
    color: var(--text);
    box-shadow: 0 0 0 1px rgba(255, 255, 255, .12) inset,
                0 8px 22px rgba(139, 92, 246, .48),
                0 0 26px rgba(139, 92, 246, .42);
}
.ub-btn--go:hover:not(:disabled):not(.is-blocked) {
    box-shadow: 0 0 0 1px rgba(255, 255, 255, .16) inset,
                0 10px 28px rgba(139, 92, 246, .6),
                0 0 34px rgba(139, 92, 246, .55);
    filter: brightness(1.08);
}

/* Payment button is the same family — a different colour there would read
   as a different system taking the money. */
.ub-btn--pay {
    background: linear-gradient(135deg, var(--brand) 0%, var(--brand-2) 55%, var(--brand-3) 100%);
    color: var(--text);
    box-shadow: 0 0 0 1px rgba(255, 255, 255, .12) inset,
                0 8px 22px rgba(139, 92, 246, .48),
                0 0 26px rgba(139, 92, 246, .42);
}
.ub-btn--pay:hover:not(:disabled):not(.is-blocked) {
    box-shadow: 0 0 0 1px rgba(255, 255, 255, .16) inset,
                0 10px 28px rgba(139, 92, 246, .6),
                0 0 34px rgba(139, 92, 246, .55);
    filter: brightness(1.08);
}

/* Selected states follow the brand, with white on top. */
.ub-seg input:checked + label {
    background: linear-gradient(135deg, var(--brand), var(--brand-3));
    color: var(--text);
    box-shadow: 0 4px 14px rgba(139, 92, 246, .3);
}
.ub-chip.on {
    background: rgba(139, 92, 246, .18);
    border-color: var(--brand);
    color: var(--brand-hi);
}
.ub-switch button.on {
    background: linear-gradient(135deg, var(--brand), var(--brand-3));
    color: var(--text);
}
.ub-skip { background: var(--brand); color: #fff; }

/* Nav: white for the current page, not a colour shout. */
.ub-nav__links a[aria-current="page"] {
    color: var(--text);
    background: rgba(139, 92, 246, .16);
}

/* Figures that matter are white; the accent class tints to brand purple
   rather than orange. */
.ub-stat__v.acc,
.ub-rewards__v.acc,
.ub-line--tot dd { color: var(--brand-hi); }

/* Chart bars in the brand gradient. */
.ub-chart__meter { color: var(--brand-hi); }

/* --- spark: dot-scale only ------------------------------------------------
   The single place orange earns its keep — a status dot, the size it is on
   the mark. */
.ub-dot--live { background: var(--spark); box-shadow: 0 0 8px rgba(249, 115, 22, .6); }
.ub-stat__n   { color: var(--text-3); }
.ub-tx--new   { box-shadow: inset 3px 0 0 var(--spark); background: rgba(249, 115, 22, .05); }
/*  ⚠ THEMED, NOT THE RAW SPARK LITERALS — 2026-08-16 both-themes audit.
    These two overrides put --spark-2 (#EF4444) and --spark (#F97316) on as
    pill TEXT: red at roughly 3:1 on a dark card, orange at roughly 2.8:1
    on a white one — each override legible only in the theme it was written
    against, which is exactly the class of bug the audit exists to catch.
    The spark colours keep their stated job ("a status dot, the size it is
    on the mark", above) — a pill's worth of text is not a dot. Failed now
    simply falls through to its base rule (the themed --danger pair, the
    same tokens .ub-err and the down-DisCo marking read); pending keeps an
    orange identity — it must not read as postpaid's purple — through the
    --warn pair, defined per theme next to --danger. */
.ub-pill--pending { background: var(--warn-bg); color: var(--warn); }

/* Meter the chart is scoped to — boxed, so it reads as a label on the panel
   rather than loose text beside the heading (2026-07-31). */
.ub-chart__meter {
    font-family: var(--mono); font-size: .6875rem;
    color: var(--brand-hi); letter-spacing: .06em;
    margin-left: auto;
    background: rgba(139, 92, 246, .12);
    border: 1px solid rgba(139, 92, 246, .3);
    border-radius: 7px; padding: .25rem .5rem;
    white-space: nowrap;
}

/* ==========================================================================
   Legibility pass (2026-07-31)

   Reported as too dark to read comfortably. The secondary and tertiary text
   were purple-tinted greys sitting on a purple ground — too little
   separation. Body copy is now near-white, supporting text a light lilac
   grey, and only genuinely incidental text stays dim.

   Ground surfaces lift slightly too, so cards separate from the page.
   ========================================================================== */
:root {
    --text:   #FFFFFF;
    --text-2: #CFC7E8;   /* was #A99FC7 — supporting copy, now clearly legible */
    --text-3: #9A90BE;   /* was #6F6491 — captions and labels */

    --surface:   #1C1440;
    --surface-2: #251B52;
    --line:      #322668;
    --line-hi:   #4A3890;
}

/* Body copy and descriptions read white-ish rather than muted. */
.ub-lede,
.ub-sec__head p,
.ub-feat p,
.ub-step p,
.ub-facts dd,
.ub-mrow__body dd,
.ub-mdetail dd,
.ub-confirm small  { color: var(--text-2); }

/* Headings, figures and anything a customer reads to make a decision. */
.ub-hero h1,
.ub-sec__head h2,
.ub-panel__h h2,
.ub-feat h3,
.ub-step h3,
.ub-stat__v,
.ub-chart__tot,
.ub-tx__a,
.ub-tx__when,
.ub-rewards__v,
.ub-summary__row dd,
.ub-line dd        { color: var(--text); }

/* Field labels and values — these were the hardest to read. */
.ub-lab            { color: var(--text); }
.ub-lab span       { color: var(--text-3); }
.ub-in, .ub-sel    { color: var(--text); }
.ub-in::placeholder { color: var(--text-3); opacity: 1; }

/* Supported DisCos: acronym in brand purple, coverage in white — the
   coverage is the part people actually scan for their own state. */

/* Ghost buttons and nav links legible at rest, not only on hover. */
.ub-btn--ghost      { color: var(--text); border-color: var(--line-hi); }
.ub-nav__links a    { color: var(--text-2); }
.ub-nav__links a:hover { color: var(--text); }

/* Transaction meter numbers were nearly invisible against the row. */
.ub-tx__m   { color: var(--text-2); }
.ub-tx__edc { color: var(--text-3); }

/* ==========================================================================
   THEMES (2026-07-31)

   Two themes, both built from the brand mark rather than one being a naive
   inversion of the other. The purple is the constant; what changes is the
   ground it sits on and how much light the page carries.

   Resolution order:
     1. an explicit choice, stored and stamped as data-theme on <html>
     2. otherwise the operating system preference
     3. otherwise light

   Tokens only — no component below this point knows which theme is active.
   ========================================================================== */

/* ---------- LIGHT (default) ------------------------------------------- */
:root,
:root[data-theme="light"] {
    --ink:       #F6F4FD;   /* page — a lilac white, not a grey */
    --ink-2:     #FFFFFF;   /* inputs and wells */
    --surface:   #FFFFFF;   /* cards */
    --surface-2: #F1EDFB;
    --line:      #E4DEF6;
    --line-hi:   #CFC4EE;

    --text:   #16103A;      /* near-black with a violet bias */
    --text-2: #4B4276;
    --text-3: #7A719F;

    --brand:     #7C3AED;
    --brand-2:   #6366F1;
    --brand-3:   #3B82F6;
    --brand-hi:  #6D28D9;   /* darker on light, so it stays readable */
    --brand-dim: #EDE7FD;

    --amber:     #7C3AED;
    --amber-hi:  #6D28D9;
    --amber-dim: #C4B5FD;

    --lcd:       #1D4ED8;
    --lcd-dim:   #E4EDFE;
    --lcd-glow:  rgba(29, 78, 216, .18);

    --danger:    #C2264B;
    --danger-bg: #FDECF0;

    /*  Amber-brown, not the spark orange: #F97316 as text on a white card
        measured ~2.8:1 (2026-08-16 audit). This is ~6:1 and still plainly
        orange next to postpaid's purple. */
    --warn:      #92400E;
    --warn-bg:   #FDF0E0;

    /*  Success green with enough ink to read on white. #22C55E — the admin
        green, kept for dark below — measured ~2.1:1 as text on a light
        card (2026-08-16 audit). Green stays green in both themes; light
        just carries a darker shade, exactly the treatment --danger gets. */
    --good:      #15803D;

    /*  ⚠ color-scheme TELLS THE BROWSER, NOT OUR CSS. Everything the UA
        draws natively — the <select> popup where "— 🔴 Not responding" is
        actually read, scrollbars, autofill fills — follows this, not the
        tokens. It was never declared, so the UA rendered those pieces on
        its own light assumptions even while the page was dark. Declared
        per theme right here with the tokens, so the two can never
        disagree. */
    color-scheme: light;

    --shadow: 0 1px 2px rgba(22, 16, 58, .06), 0 8px 28px rgba(22, 16, 58, .09);
}

/* ---------- DARK ------------------------------------------------------- */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --ink:       #150E33;   /* lifted from near-black — it read as heavy */
        --ink-2:     #1B1240;
        --surface:   #221750;
        --surface-2: #2B1E63;
        --line:      #392878;
        --line-hi:   #513A9E;

        --text:   #FFFFFF;
        --text-2: #D6CEF2;
        --text-3: #A79CD0;

        --brand:     #8B5CF6;
        --brand-2:   #6366F1;
        --brand-3:   #3B82F6;
        --brand-hi:  #C4B5FD;
        --brand-dim: #33246E;

        --amber:     #8B5CF6;
        --amber-hi:  #C4B5FD;
        --amber-dim: #5B45B0;

        --lcd:       #7DD3FC;
        --lcd-dim:   #14284D;
        --lcd-glow:  rgba(125, 211, 252, .35);

        --danger:    #FF8095;
        --danger-bg: #3A1730;

        --warn:      #FDBA74;
        --warn-bg:   #3B2A14;

        --good:      #22C55E;

        color-scheme: dark;

        --shadow: 0 2px 8px rgba(0,0,0,.45), 0 18px 44px rgba(0,0,0,.4);
    }
}

:root[data-theme="dark"] {
    --ink:       #150E33;
    --ink-2:     #1B1240;
    --surface:   #221750;
    --surface-2: #2B1E63;
    --line:      #392878;
    --line-hi:   #513A9E;

    --text:   #FFFFFF;
    --text-2: #D6CEF2;
    --text-3: #A79CD0;

    --brand:     #8B5CF6;
    --brand-2:   #6366F1;
    --brand-3:   #3B82F6;
    --brand-hi:  #C4B5FD;
    --brand-dim: #33246E;

    --amber:     #8B5CF6;
    --amber-hi:  #C4B5FD;
    --amber-dim: #5B45B0;

    --lcd:       #7DD3FC;
    --lcd-dim:   #14284D;
    --lcd-glow:  rgba(125, 211, 252, .35);

    --danger:    #FF8095;
    --danger-bg: #3A1730;

    --warn:      #FDBA74;
    --warn-bg:   #3B2A14;

    --good:      #22C55E;

    color-scheme: dark;

    --shadow: 0 2px 8px rgba(0,0,0,.45), 0 18px 44px rgba(0,0,0,.4);
}

/* ---------- pieces that were tuned for a dark ground ------------------- */

/* Sticky nav and overlays: derive from the page colour instead of a
   hardcoded dark rgba, so they frost correctly in both themes.

   ⚠ .ub-confirmwrap WAS MISSING FROM THIS LIST. Its own comment at the rule
   above says it shares .ub-veil's ground "deliberately", but the value it
   actually shipped with, rgba(11, 7, 22, .86), is .ub-veil's OLD hardcoded
   colour from before this fix existed - copied once, then never migrated
   when .ub-veil itself was made theme-aware. The result: opening the
   confirm-and-pay sheet in light theme dropped a near-black scrim behind it
   while every other overlay on the page had gone light. Given the same
   88% figure as .ub-veil, since the two are meant to read as one system. */
.ub-nav         { background: color-mix(in srgb, var(--ink) 82%, transparent); }
.ub-veil        { background: color-mix(in srgb, var(--ink) 88%, transparent); }
.ub-confirmwrap { background: color-mix(in srgb, var(--ink) 88%, transparent); }
.ub-payback     { background: color-mix(in srgb, var(--ink) 94%, transparent); }
@supports not (background: color-mix(in srgb, red 50%, blue)) {
    .ub-nav         { background: var(--ink); }
    .ub-veil        { background: var(--ink); }
    .ub-confirmwrap { background: var(--ink); }
    .ub-payback     { background: var(--ink); }
}

/* Text sitting on the brand gradient is always white, in both themes. */
.ub-btn--go, .ub-btn--pay,
.ub-seg input:checked + label,
.ub-switch button.on,
.ub-skip { color: #fff; }

/*  ⚠ THESE PINNED THE TOKEN PANEL DARK IN BOTH THEMES, AND ARE GONE.
    The argument was that a meter readout is not white. The consequence was a
    LIGHT page with a black panel on it, and every label and button inside it
    forced light to stay legible - which is exactly the "light theme has dark
    boxes I cannot read" problem, arrived at on purpose.

    One rule now: light theme is light throughout, dark theme is dark
    throughout, and panels, fields and buttons all obey it. The token keeps
    its monospace face and its blue accent; it just sits on the page's own
    surface like everything else.

    Kept as explicit resets rather than deleted, so a stylesheet still in a
    browser cache cannot leave half the old pinning behind mid-deploy. */
.ub-lcd__val   { color: var(--brand-hi); text-shadow: none; }
.ub-lcd__cap   { color: var(--text-3); }
.ub-lcd__sub b { color: var(--text); }
.ub-lcd__sub small { color: var(--text-3); }

/*  The token in a history row is the same idea as the readout above - a dark
    face carrying a lit token - and it was missed when that decision was made.
    Its ground is hardcoded dark, but its text was var(--lcd), which on a light
    page is #1D4ED8: a dark blue on a near-black ground, unreadable.

    Fixed the way the readout was, rather than by lightening the ground: the
    face stays dark in both themes and the text is the same lit blue in both,
    so a token looks like a token wherever it appears. */
/*  ⚠ THE TOKEN ROW FOLLOWS THE THEME TOO. It was a hardcoded #071612 face
    with the token pinned light on top, and the COPY BUTTON beside it left
    following the theme - so in light mode that button was near-black text on
    a near-black box, which is what "I cannot see the total or the copy
    button" was.

    Both the face and everything on it now use tokens. Nothing here is pinned,
    so nothing can invert against anything else. */
.ub-tx__t { color: var(--brand-hi); }

/* Ambient canvas is decoration; on light it would be noise. */
:root[data-theme="light"] #ub-current,
:root:not([data-theme="dark"]) #ub-current { opacity: .22; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) #ub-current { opacity: .5; }
}
:root[data-theme="dark"] #ub-current { opacity: .5; }

/* ---------- theme toggle ---------------------------------------------- */
/*  ⚠ position: sticky; right: … IS A SAFETY CLAMP, NOT A LAYOUT CHOICE.
    Reported from a real iPhone: the circle sat past the true right edge of
    the screen, clipped - while every desktop/emulator check of the same
    width showed it sitting comfortably inside .wrap's padding. That gap
    between "measured fine" and "photographed clipped" points at real-device
    text metrics (system font vs whatever the checking machine substitutes)
    nudging the nav row's total content width past what this checking
    environment ever reproduces - a few px more than "Support"/"Sign in"
    measured here, enough to push the last nowrap flex item outside the
    viewport on the device that actually hit it.

    `right` alone (without top/bottom) makes this ONLY a horizontal clamp:
    the toggle still scrolls normally with the page and still occupies its
    normal place in the flex row - this does not change spacing for any
    sibling. It just refuses to ever render closer to the true edge than
    `right` allows, however that row's content happens to measure on the
    device someone is actually holding. */
.ub-theme {
    display: inline-grid; place-items: center;
    width: 34px; height: 34px; flex: none;
    background: transparent; border: 1px solid var(--line-hi);
    border-radius: 50%; cursor: pointer; color: var(--text-2);
    transition: color .15s, border-color .15s, background .15s;
    position: sticky; right: 1.5rem;
}
.ub-theme:hover { color: var(--text); border-color: var(--brand); background: var(--surface); }
.ub-theme svg { width: 16px; height: 16px; }
.ub-theme .ub-theme__sun  { display: none; }
.ub-theme .ub-theme__moon { display: block; }
:root[data-theme="dark"] .ub-theme .ub-theme__sun  { display: block; }
:root[data-theme="dark"] .ub-theme .ub-theme__moon { display: none; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .ub-theme .ub-theme__sun  { display: block; }
    :root:not([data-theme="light"]) .ub-theme .ub-theme__moon { display: none; }
}

/* kWh larger and set clearly apart from the price, so a row reads
   "how much power" then "what it cost" (2026-07-31). */
.ub-tx__row { gap: 1.25rem; }
.ub-tx__u   { font-size: .875rem; font-weight: 650; color: var(--lcd); }
.ub-tx__a   { font-size: 1rem; }

/* ==========================================================================
   Quick Buy legibility (2026-07-31)

   The buy card is the one thing on the site that has to be readable at a
   glance, often on a phone in poor light. Its labels were the smallest and
   dimmest type on the page — the opposite of what they should be.

   Field names now take the full text colour at a proper size, and the card
   is allowed to grow to fit rather than everything being squeezed.
   ========================================================================== */
.ub-buy { padding: 1.125rem 1.125rem 1.25rem; }

/* Field names: full contrast, readable size. */
.ub-buy .ub-lab {
    color: var(--text);
    font-size: .9375rem;
    font-weight: 650;
    margin-bottom: .5rem;
}
/* The explanatory line under a field name stays secondary, but no longer
   microscopic. */
.ub-buy .ub-lab span {
    color: var(--text-2);
    font-size: .8125rem;
    font-weight: 450;
    margin-top: .1875rem;
}

/*  ---- ⚠ EVERY CONTROL STACKED IN A CARD IS THE SAME WIDTH ----

    THE RULE, and it holds for every card on this site, not only this one:

        If a control sits in a vertical stack with other controls or
        buttons, it is the FULL WIDTH OF THE CARD. It does not get its own
        measure. One left edge, one right edge, all the way down.

    ⚠ THERE WERE THREE DIFFERENT CAPS IN THIS ONE CARD, and you could see
    every one of them:

        .ub-in--meter          22ch     the meter box, shortest
        #ub-amount             14ch     the amount box
        .ub-btn                34ch     Continue, and the DisCo select
                                        inherited none of these, so it ran
                                        to the full card width

    Four controls in one column, four different right edges. That is what
    "the meter number, the DisCo and Continue are all different lengths"
    was, and it was as bad as it sounds.

    ⚠ THE CAPS ARE NOT WRONG IN THEMSELVES, which is why they are overridden
    here and not deleted. They come from #116, where inputs stretched across
    a full desktop measure looked absurd for thirteen digits. That reasoning
    holds for a lone input in open space. It is simply void inside a card,
    because the card already IS the measure.

    ⚠ max-width: none IS THE LOAD-BEARING DECLARATION. `width: 100%` alone
    does not beat an inherited max-width — the box would still stop dead at
    22ch and merely be 100% of nothing wider. Both are needed, together.

    ⚠ AND THE :not() IS NOT DECORATION. Two button variants are deliberately
    narrow and must stay that way:

        .ub-btn--sm     small inline actions
        .ub-btn--auto   shrink-to-fit

    Both set `width: auto` at one class of specificity. This selector has
    two, so without the exclusion it would silently stretch every one of
    them across its card. The Back/Continue pair is safe without help —
    `.ub-btnrow .ub-btn { flex: 1 }` makes flex, not width, the authority
    there, and the two share the row evenly. */
/*  ⚠ THE BUTTON CAME OUT OF THIS SELECTOR TOO - see the matching note on
    .ub-auth above. The exclusion list it used to carry
    (:not(.ub-btn--sm):not(.ub-btn--auto)) existed only to let two modifiers
    escape a stretch that now applies to no button at all. */
.ub-buy .ub-in,
.ub-buy .ub-sel {
    width: 100%;
    max-width: none;
}
.ub-buy .ub-in,
.ub-buy .ub-sel {
    min-height: 50px;
    font-size: 1.0625rem;
    padding: .625rem .875rem;
}
.ub-buy .ub-in--meter { font-size: 1.25rem; letter-spacing: .1em; }
.ub-buy .ub-sel { padding-right: 2.75rem; }
/* The naira sign is absolutely positioned, so the amount field reserves room
   for it with padding-left. The `padding` shorthand three rules up resets
   that to the card's own inset and the digits land on top of the sign, so
   the reserve has to be restated here — at a specificity that outranks both
   `.ub-buy .ub-in` and the density-pass `.ub-amt .ub-in`. The sign moves to
   the buy card's .875rem inset so it lines up with the text in every other
   field on the card. */
.ub-buy .ub-amt__ng { left: .875rem; }
.ub-buy .ub-amt .ub-in { font-size: 1.375rem; padding-left: 2.25rem; }

/* Prepaid / postpaid reads as a real choice, not a caption. */
.ub-buy .ub-seg label { min-height: 44px; font-size: 1rem; }

/* Primary action matches the fields it sits under. */
.ub-buy .ub-btn { min-height: 50px; font-size: 1rem; }
.ub-buy .ub-btn--sm { min-height: 40px; font-size: .875rem; }

/* Breathing room between fields now that they carry more weight. */
.ub-buy .ub-f { margin-bottom: 1.125rem; }

/* Step heading and counter scale with the rest. */
.ub-buy__head { margin-bottom: .875rem; }
.ub-buy__step { font-size: .8125rem; }

/* Summary lines inside the card — the figures a customer checks before
   paying should not be the smallest thing in it. */
.ub-buy .ub-line    { font-size: 1rem; padding: .4375rem 0; }
.ub-buy .ub-line dt { color: var(--text-2); }
.ub-buy .ub-line--tot { font-size: 1.1875rem; }

/* Reassurance row under the button. */
.ub-buy .ub-assure { font-size: .75rem; }

/* The confirmed name and address before payment — the strongest trust
   signal in the flow, so it reads at full size. */
.ub-confirm b     { font-size: 1rem; }
.ub-confirm small { font-size: .875rem; color: var(--text-2); }

/* ?debug=1 panel — shows one raw API row so field-name questions can be
   settled by looking rather than guessing. Not linked from anywhere. */
.ub-debug {
    background: var(--ink-2); border: 1px solid var(--brand);
    border-radius: var(--r); padding: .75rem .875rem; margin-bottom: 1rem;
    font-family: var(--mono); font-size: .6875rem; color: var(--text-2);
    white-space: pre-wrap; word-break: break-word; overflow-x: auto;
}

/* ==========================================================================
   Hero stats — made prominent (2026-07-31)
   They were a muted footnote under the lede. Boxed and enlarged so they read
   at a glance, which is the only reason to have them.
   ========================================================================== */
.ub-proof {
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: .5rem; margin-top: 1.25rem;
}
.ub-proof > div {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r);
    padding: .75rem .625rem;
    text-align: center;
}
.ub-proof__n {
    font-size: 1.375rem; font-weight: 750; letter-spacing: -.02em;
    color: var(--text); font-variant-numeric: tabular-nums; line-height: 1.15;
}
.ub-proof__l {
    font-size: .6875rem; color: var(--text-3); margin-top: .1875rem;
    line-height: 1.3;
}

/* A customer's own figures get the brand tint — they are about them, and
   they are real rather than a marketing claim. */
.ub-proof--mine > div {
    background: linear-gradient(140deg,
        color-mix(in srgb, var(--brand) 14%, var(--surface)),
        var(--surface));
    border-color: var(--line-hi);
}
@supports not (background: color-mix(in srgb, red 50%, blue)) {
    .ub-proof--mine > div { background: var(--surface-2); }
}
.ub-proof--mine .ub-proof__n { color: var(--brand-hi); }

@media (max-width: 420px) {
    .ub-proof__n { font-size: 1.125rem; }
    .ub-proof > div { padding: .625rem .375rem; }
}

/* ==========================================================================
   Ground re-tint (2026-08-01) — "too purple everywhere"

   The accents fixed monotony but not the cause: both themes were built on a
   SATURATED violet, so purple was in the walls rather than only the
   furniture. Every surface, border and page background carried it, and any
   accent laid on top read as one more purple thing.

   The grounds move to a near-neutral indigo-slate — still cool, still a
   family the mark sits in, but far less chroma. The brand purple is
   unchanged, so the logo, buttons and focus rings are now the most saturated
   things on screen, which is what makes them read as brand rather than as
   background.

   Only ground, surface, line and page-background tokens change here. No
   brand, text or status colour is touched.
   ========================================================================== */
/*  ---- THE DISCO CHIP EDGE, AND WHY IT IS A TOKEN ----

    The chips are white and stay white: they carry other companies' marks,
    drawn for light backgrounds, and recolouring somebody else's trademark is
    not an option. See the note above `.ub-disco`.

    That is fine on a dark page and INVISIBLE on a light one. In light theme
    the page is #F7F8FC and cards are #FFFFFF, so a white chip with a
    six-percent black border is a white box on a white box - the logos float
    with no chip around them, and the one that is white artwork disappears
    completely.

    So the edge cannot be a fixed colour. It is a token, set per theme like
    everything else here, and `.ub-disco` never learns which theme is on.

    ⚠ THE BARE `:root` HERE CARRIES THE LIGHT VALUES, AND IT MUST.

    An earlier version of this block said "it went unnoticed because the
    default theme is dark" and set dark values on a bare `:root`. The default
    theme is NOT dark. The resolution block above declares
    `:root, :root[data-theme="light"]`, which makes LIGHT the default and lets
    the OS preference and an explicit choice override it.

    So a bare `:root` down here is the same specificity as that one and later
    in the file, and it won - but only for the tokens it names. It took --ink
    and --surface dark and left --text on the light value, giving #16103A text
    on a #0C0B14 ground: a contrast ratio of 1.02 to 1 on every page, for
    every first-time visitor whose machine is set to light. The form was
    invisible.

    This is the third time this file has been bitten by a later rule of equal
    specificity quietly winning - see the doubled `.ub-hero h1.ub-hero__h1`
    selector, and the `padding` shorthand that reset the amount field's
    padding-left. THE RULE: a bare `:root` sets the DEFAULT, and the default
    is light. Anything dark belongs behind `[data-theme="dark"]` or the
    prefers-dark media query, never on a bare `:root`. */
:root,
:root[data-theme="light"] {
    --ink:       #F7F8FC;
    --ink-2:     #FFFFFF;
    --surface:   #FFFFFF;
    --surface-2: #F2F4FA;
    --line:      #E1E4EF;
    --line-hi:   #CBD0E2;

    /*  Here the edge is the ONLY thing separating a white chip from a white
        card, so it has to be a real line rather than a hint of one - the same
        weight as --line-hi, which is what every other boundary on a light
        page uses. The shadow lifts it off the card as well, because a border
        alone on white reads as a table cell. */
    --disco-edge:         #CBD0E2;
    --disco-shadow:       0 1px 2px rgba(22, 16, 58, .08), 0 2px 8px rgba(22, 16, 58, .06);
    --disco-shadow-hover: 0 4px 12px rgba(22, 16, 58, .13);
}

:root[data-theme="dark"] {
    --ink:       #14131F;
    --ink-2:     #1A1928;
    --surface:   #222134;
    --surface-2: #2C2B42;
    --line:      #363551;
    --line-hi:   #4A4870;

    --disco-edge:         rgba(0, 0, 0, .06);
    --disco-shadow:       0 1px 2px rgba(0, 0, 0, .18);
    --disco-shadow-hover: 0 3px 8px rgba(0, 0, 0, .24);
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --ink:       #14131F;
        --ink-2:     #1A1928;
        --surface:   #222134;
        --surface-2: #2C2B42;
        --line:      #363551;
        --line-hi:   #4A4870;

        --disco-edge:         rgba(0, 0, 0, .06);
        --disco-shadow:       0 1px 2px rgba(0, 0, 0, .18);
        --disco-shadow-hover: 0 3px 8px rgba(0, 0, 0, .24);
    }
}

/*  Light theme was a lilac wash on white cards. The page goes to a cool
    near-white with a hint of blue rather than violet, so the purple in the
    logo and the buttons is the only strong colour on screen.

    Those values now live on the `:root, :root[data-theme="light"]` block
    above, next to the reason they have to be the default. Two blocks setting
    the same six tokens is how they drift apart. */

/*  The three steps, in the hero.

    Numbered marks in the mark's own colours — the first concrete use of the
    dot cluster on the landing page, and a place where colour carries the
    sequence rather than decorating it. */
/*  No top margin: the DisCo row now sits between these and the headline and
    carries its own gap underneath. Adding one here on top of that put three
    clear lines between the marks and step 1 and broke the column into pieces. */
.ub-hero__steps {
    list-style: none; margin: 0 0 1.25rem; padding: 0;
    display: flex; flex-direction: column; gap: .625rem;
    counter-reset: ubstep;
}
.ub-hero__steps li {
    display: flex; align-items: center; gap: .625rem;
    font-size: .9375rem; font-weight: 500; color: var(--text-2);
    /*  500, not the inherited 400 (2026-08-12) — each of these three lines
        sat next to a 700-weight numbered marker and read visibly lighter
        than the badge beside it. Matches the .ub-lede bump just above in
        this file, same reasoning. */
}
.ub-hero__steps li::before {
    counter-increment: ubstep; content: counter(ubstep);
    flex: 0 0 auto; width: 1.5rem; height: 1.5rem; border-radius: 50%;
    display: grid; place-items: center;
    font-size: .75rem; font-weight: 700; color: #fff;
    background: var(--dot, var(--brand));
}
.ub-hero__steps li:nth-child(1) { --dot: var(--dot-1); }
.ub-hero__steps li:nth-child(2) { --dot: var(--dot-2); }
.ub-hero__steps li:nth-child(3) { --dot: var(--dot-3); }

/*  Quidly sheet styling REMOVED (2026-08-01) — I broke it.

    Setting background-color on descendant DIVs stripped the panel out from
    under their sheet, so the fields, the total and the Make Payment button
    were left floating on the page's dark ground with no card behind them and
    no way to tell where to type. The pay button lost its fill entirely,
    because it is not type=submit and so never got the replacement gradient.

    The sheet renders exactly as Quidly ships it again. Do not restyle it
    through descendant selectors: their markup is a Vue build with no stable
    hooks, and the failure mode is an unusable payment form.
    ========================================================================== */

/*  Sized to the CONTENT, not to the column.

    grid columns of 1fr stretched the DisCo tile to whatever was left over,
    so the box ran on past "PHED" with nothing in it. inline-grid with auto
    columns means each tile is exactly as wide as what it holds and the pair
    stops where the text stops. */
.ub-proof { display: inline-grid; grid-template-columns: auto auto; }
.ub-proof > div { text-align: left; padding: .625rem .75rem; }
.ub-proof__hours { text-align: center; }
.ub-proof__list {
    margin-top: .1875rem;
    font-size: .8125rem; font-weight: 650; letter-spacing: .01em;
    line-height: 1.45; color: var(--text-2);
}
.ub-proof__discos .ub-proof__l { font-size: .6875rem; text-transform: uppercase; letter-spacing: .06em; }

/* Below 560px the two sit one above the other rather than squeezing six
   acronyms into half a phone. */
@media (max-width: 560px) {
    .ub-proof { grid-template-columns: auto; }
    .ub-proof__hours { text-align: left; }
}

/*  Compact history rows.

    Each row carried a full-width token panel — dark face, dashed border,
    generous padding — so ten rows ran the length of the page even though the
    list itself was already capped at ten. The token still shows in full and
    still copies; it is just no longer the tallest thing in every row.

    The dark readout face is kept, because that is the meter-display motif and
    the token is the one thing on the page people read digit by digit. What
    goes is the padding around it. */
.ub-dash .ub-tx__t {
    margin-top: .3125rem;
    padding: .3125rem .5rem;
    font-size: .8125rem;
    letter-spacing: .04em;
    gap: .5rem;
}
.ub-dash .ub-tx__t .ub-btn { min-height: 26px; padding: 0 .625rem; font-size: .75rem; }
.ub-dash .ub-tx__when { font-size: .8125rem; }
.ub-dash .ub-tx__m { font-size: .8125rem; }

/*  A derived unit figure is marked, never presented as measured. */
.ub-tx__u--est { opacity: .85; }

/* ==========================================================================
   Step colour, carried through the flow (2026-08-01)

   The hero lists three numbered steps in the mark's colours: 1 purple,
   2 blue, 3 orange. Those numbers now follow you into the form — the same
   colour appears on the card that performs that step, and on the segment of
   the progress rail that represents it.

   Someone who read "enter your meter number and pick your DisCo" beside a
   purple 1 meets a purple 1 on the form asking for exactly that. The colour
   is the thread through the flow rather than decoration applied three times.
   ========================================================================== */
.ub-buy__head { align-items: center; }

/*  ---- WHY THIS IS SO SPECIFIC ----

    The buy card sits INSIDE .ub-hero, so the density rule
    `.ub-hero h1 { font-size: clamp(...); margin-bottom: .625rem }`
    was landing on the card's step heading as well as on the page headline.
    Both that rule and `.ub-buy__head h1` are (0,1,1), and the density one
    comes later in the file, so it won.

    The margin-bottom was the visible damage: it made the heading taller than
    its text, so `align-items: center` centred a box with dead space under it
    and the words sat ABOVE the numbered badge instead of level with it. Most
    obvious on the longer headings — "Pay for TV", "Buy exam pins".

    `.ub-buy .ub-buy__head h1` is (0,2,1) and beats both. Do not simplify it
    back down to one class; it will silently stop working. */
.ub-buy .ub-buy__head h1,
.ub-buy .ub-buy__head h2 {
    flex: 1;
    font-size: 1.25rem;
    margin: 0;
    line-height: 1.75rem;   /* matches the badge, so the two centre on the same axis */
}

.ub-buy__num {
    flex: 0 0 auto;
    width: 1.75rem; height: 1.75rem; border-radius: 50%;
    display: grid; place-items: center;
    font-size: .875rem; font-weight: 750; color: #fff;
    font-variant-numeric: tabular-nums;
    background: var(--brand);
    transition: background .35s;
}
.ub-buy__num--1 { background: var(--dot-1); }
.ub-buy__num--2 { background: var(--dot-2); }
.ub-buy__num--3 { background: var(--dot-3); }

/*  Each rail segment takes its own step's colour once reached, so the rail
    reads as the same three stages rather than one undifferentiated bar. */
.ub-rail i.on              { box-shadow: none; }
.ub-rail i.ub-rail--1.on   { background: var(--dot-1); }
.ub-rail i.ub-rail--2.on   { background: var(--dot-2); }
.ub-rail i.ub-rail--3.on   { background: var(--dot-3); }

/* ==========================================================================
   DisCo strip — across the top of the page (2026-08-02, twelve DisCos since
   2026-08-10)

   The first thing on the page: a quiet label and the marks, level with
   where the signed-out notice appears. "Do you cover me?" gets answered
   before anything else is read.

   Real logos and plain initials can share the row — every DisCo has a real
   mark as of 2026-08-10, but a DisCo added later without one falls back to
   text, and each cell is the same height so it would still sit level with
   the rest and the row would not change shape when the logo arrives.
   ========================================================================== */
/*  Last thing in the text column, on the landing page only, sitting on the
    floor of the hero so it lines up with the foot of the buy card beside it.

    margin-top:auto does the pushing, which needs the column to be a flex
    column AND to be as tall as the row - see .ub-hero__intro below. Padding
    alone cannot do this: the gap that lands the marks on the bottom depends on
    how tall the card is, and that changes with the step, the viewport and
    whether an error is showing. */
/*  A GRID, and the same grid as the row above it.

    This was a wrapping flex row of chips that each sized to their own logo,
    so AEDC's chip was one width, PHED's another, and the strip ended ragged
    and visibly out of line with the statistics row above — which is a grid of
    equal columns spanning the full measure.

    Six equal columns, matching .ub-trust's construction, so both rows start
    at the same left edge, finish at the same right edge, and read as one
    system rather than two things that happen to be near each other. This is
    a column count, not an item count: it does not change with how many
    DisCos are in UBox.EDCS. Twelve items wrap to two full rows of six
    rather than breaking anything - see the note on .ub-discos above.

    minmax(0, 1fr) rather than 1fr: without the zero minimum a grid track
    refuses to shrink below its content, and a wide logo would push the row
    past the container. */
.ub-discos {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: .5rem;
    align-items: stretch;
    margin-top: auto;
    padding: clamp(1.75rem, 4vw, 2.75rem) 0 0;
}

/*  White cells, always. These are other companies' marks, drawn for light
    backgrounds — most are dark type with a coloured device, and on our
    ground they would either disappear or need recolouring, which is not
    something to do to somebody else's trademark. A white chip keeps every
    one of them exactly as its owner drew it. */
/*  The DEFAULT chip: sized to its own logo.

    Used inline all over the site — beside a meter in the account detail, on
    the payment backdrop, in the history rows. These must stay compact and
    hug their artwork.

    The equal-width treatment below is scoped to `.ub-discos .ub-disco`, the
    landing-page strip, and nowhere else. It was briefly applied to
    `.ub-disco` itself, which stretched every inline chip on every page to
    full width — AEDC's mark ended up absurdly wide in the account card. */
.ub-disco {
    position: relative;
    display: inline-flex; align-items: center; justify-content: center;
    height: 56px; min-width: 84px; padding: 0 .875rem;
    background: #fff;
    /*  Edge and shadow come from theme tokens, because a white chip needs a
        faint edge on a dark page and a real one on a light page or it
        vanishes into the card. Defined with the other tokens - search
        --disco-edge for the full explanation. */
    border: 1px solid var(--disco-edge, rgba(0, 0, 0, .06));
    border-radius: 9px;
    box-shadow: var(--disco-shadow, 0 1px 2px rgba(0, 0, 0, .18));
    font-size: .8125rem; font-weight: 750; letter-spacing: .04em;
    color: #2C2B42;
    transition: transform .15s ease, box-shadow .15s ease;
}
.ub-disco:hover {
    transform: translateY(-1px);
    box-shadow: var(--disco-shadow-hover, 0 3px 8px rgba(0, 0, 0, .24));
}

.ub-disco img {
    max-height: 40px; max-width: 126px;
    width: auto; height: auto; display: block;
    object-fit: contain;
}

/*  ---- DOWN, PER THE EDC HEALTH PROBE OR VTPASS'S OWN CATALOGUE ----

    Owner's own words, 2026-08-11: "the logo turns red... and then it grays
    out in the picker... when it comes back, it can go white again — doesn't
    have to go green, but red at least to show something is down." Set by
    Index.cshtml's UBox.refreshMinimums callback, never by CSS alone.

    ⚠ CHANGED 2026-08-14, OWNER'S OWN WORDS: "the whole box needs to be
    red... the outline red is not really [enough]... I need more visual
    aids." The border-only ring below used to be the whole red signal; a
    thin --danger outline on a still-white chip read as barely tinted from
    across a room. Two independent signals remain, deliberately, not one
    relying on the other, but the first one is now the WHOLE tile rather
    than its edge:

    1. `::before` lays a var(--danger) wash across the entire tile, ON TOP
       of the logo art (an absolutely-positioned pseudo-element paints
       after a plain, non-positioned <img> regardless of source order —
       see the stacking note this rule relies on). `mix-blend-mode:
       multiply` is what makes this work on EVERY logo file, not just the
       transparent-background PNGs: two of these are opaque JPGs (ABEDC,
       YEDC) baked onto their own white rectangle, and a plain translucent
       red layer sitting UNDER an opaque JPG would never be seen. Multiply
       darkens whatever is beneath it toward red instead of composing over
       it, so the opaque white JPGs go visibly red-tinted too, not just the
       PNGs with real alpha.
    2. `filter` still desaturates and dims the logo itself, so a customer
       who cannot see red still gets a visibly greyed mark under the wash.

    ⚠ THE CHIP'S OWN COLOURS ARE PINNED, NOT THEMED — 2026-08-16, reported
    as unreadable in dark mode. The first version of this used the themed
    --danger/--danger-bg pair on the argument that themed tokens are
    validated per ground — which is true for text on the PAGE, and exactly
    wrong here, because this ground does not flip with the theme: the chip
    is pinned white in both themes (see "White cells, always" above). In
    dark theme --danger-bg is #3A1730, so the chip's ground went
    near-black, the greyed logo vanished into it, and the multiply wash —
    which can only ever darken — finished the job: a black-red blob where
    a company's mark should still be legible. So the ground pins to the
    LIGHT danger tint and the wash to the LIGHT danger red, for the same
    reason the chip itself pins white: the state must stay readable on the
    artwork it marks, whatever the page around it is doing. The BORDER and
    the corner dot stay on var(--danger): they read against the page, not
    the chip, and the page is what flips. */
.ub-disco.is-down {
    background: #FDECF0;   /* the light-theme --danger-bg, pinned — see above */
    border-color: var(--danger);
    box-shadow: none;
}
/*  A dark-faced chip (white artwork — PHED, see .ub-disco--dark) cannot
    take the light tint: its logo is white and would vanish on it. It pins
    to the DARK danger tint instead, and the border, dot and greyed logo
    carry the state. Same specificity as .ub-disco.is-down, later in
    source, so it wins the tie. */
.ub-disco--dark.is-down {
    background: #3A1730;   /* the dark-theme --danger-bg, pinned */
}
.ub-disco.is-down:hover {
    box-shadow: none;
    transform: none;
}
.ub-disco.is-down img {
    filter: grayscale(1) opacity(.7);
}
/*  The full-tile red wash — see the header comment above for why this is
    ::before (painted OVER the <img>, not under it) and why multiply matters
    for the two opaque JPG logos. inset:0 + border-radius:inherit keeps it
    exactly the tile's own shape, corners included. */
.ub-disco.is-down::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: #C2264B;   /* the light-theme --danger, pinned with the chip
                              ground above — the wash lives ON the chip, and
                              the chip does not flip with the theme */
    opacity: .55;
    mix-blend-mode: multiply;
    pointer-events: none;
}
/*  The dot. Ringed in the PAGE ground (--ink), not the chip's own white, so
    it reads as sitting ON the chip rather than merging into it on a dark
    page where the chip would otherwise be the one light shape around.
    Kept on top of the ::before wash (see stacking note above: later
    pseudo-element among same-z-index positioned siblings paints last). */
.ub-disco.is-down::after {
    content: '';
    position: absolute;
    top: -5px; right: -5px;
    width: 11px; height: 11px;
    border-radius: 50%;
    background: var(--danger);
    border: 2px solid var(--ink);
}

/*  ---- LANDING-PAGE STRIP ONLY ----

    Equal columns here and nowhere else. Every chip the same width so the row
    lines up with the statistics grid above it; `min-width: 0` lets the grid
    track govern the width instead of the widest logo doing it.

    Scoped to `.ub-discos` deliberately. Applying this to `.ub-disco` itself
    stretched the inline chips on the account page and the payment backdrop to
    full width, which looked ridiculous — they are meant to hug their mark. */
.ub-discos .ub-disco {
    display: flex;
    width: 100%;
    min-width: 0;
    height: 68px;
    padding: 0 .75rem;
}

.ub-discos .ub-disco img {
    max-height: 48px;
    max-width: 100%;
}

/*  Rows of three rather than six slivers. Six equal columns on a phone gives
    each logo about 50px, which is narrower than the artwork is legible at.
    Three keeps them readable and the rows still line up - four rows for
    twelve items rather than the two this gave when there were only six,
    which is still fine, just taller. */
@media (max-width: 860px) {
    .ub-discos { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 420px) {
    .ub-discos { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .ub-discos .ub-disco { height: 64px; padding: 0 .625rem; font-size: .75rem; }
    .ub-discos .ub-disco img { max-height: 44px; }
}


/* ==========================================================================
   Seasonal accents (2026-08-01)

   Driven by data-season on <html>, set in ubox-current.js. Each season shifts
   --brand and the step/accent colours and adds one small emblem beside the
   wordmark. Nothing else moves.

   What this deliberately does NOT do: overlay the page, animate anything,
   recolour a form field, or touch the payment sheet. Someone typing a meter
   number or a card number should see a calm page. A seasonal touch is worth
   one accent colour and one emoji, and not one pixel more.

   Preview any of them with ?season=christmas (or ramadan, eid, easter,
   independence, democracy, newyear).
   ========================================================================== */
/*  ---- THE DRESSING GOES ON THE LOGO ----

    This was an emoji absolutely positioned in the right-hand corner of the
    navigation bar. Nobody reads a 🎄 floating beside "Sign in" as festive
    branding; it reads as a stray character.

    What retailers actually do — and what was asked for — is put a Santa hat
    on the mark. So the decorations are now drawn inside the logo's own SVG
    (see _UBoxLogo.cshtml), which means they scale with it, sit at a fixed
    position relative to the U, and cannot drift.

    All hidden by default. One group is revealed per season and no more. */
.ub-logo__season { display: none; }

[data-season="christmas"]    .ub-logo__season--christmas,
[data-season="ramadan"]      .ub-logo__season--crescent,
[data-season="eid"]          .ub-logo__season--crescent,
[data-season="independence"] .ub-logo__season--flag,
[data-season="democracy"]    .ub-logo__season--flag,
[data-season="newyear"]      .ub-logo__season--spark,
[data-season="easter"]       .ub-logo__season--egg { display: block; }

/* Christmas — deep red against the brand purple, which sit well together. */
[data-season="christmas"] { --dot-1: #C42B3A; --dot-3: #1E8449; }

/* New Year. */
[data-season="newyear"]   { --dot-1: #D4AF37; --dot-3: #8B5CF6; }

/*  Independence Day — the flag, and the only season that takes the hero
    headline as well. Green, white, green: the white is the page, so the rule
    under the headline carries the two greens with a gap between them. */
[data-season="independence"] { --dot-1: #008751; --dot-2: #FFFFFF; --dot-3: #008751; }
[data-season="independence"] .ub-hero__h1 {
    padding-bottom: .5rem;
    border-bottom: 4px solid;
    border-image: linear-gradient(90deg,
        #008751 0 33.3%, transparent 33.3% 66.6%, #008751 66.6%) 1;
}

/* Democracy Day — the same greens, without the flag rule. */
[data-season="democracy"] { --dot-1: #008751; --dot-3: #008751; }

/*  Ramadan and Eid — deep green and gold, which is the conventional pairing
    and reads as respectful rather than festive. No emblem for Ramadan: it is
    a month of fasting, not a party, and a decoration would land badly. */
[data-season="ramadan"] { --dot-1: #0B6E4F; --dot-3: #D4AF37; }
[data-season="eid"]     { --dot-1: #0B6E4F; --dot-3: #D4AF37; }

/* Easter. */
[data-season="easter"] { --dot-1: #7D3C98; --dot-3: #F5B7B1; }

.ub-nav { position: relative; }

/* Nothing seasonal reaches the payment sheet or the token. */
[data-season] #quidly-pay-modal::after,
[data-season] #quidly-overlay-root::after { content: none !important; }


/*  A cell for LIGHT artwork.

    Some DisCos publish only a white logo, drawn for a dark header — PHED is
    one. On the white cell every other mark needs, a white logo is invisible,
    which is exactly how PHED's chip came out blank. Recolouring another
    company's trademark is not something to do, so the cell changes instead:
    a dark face, and their mark stays untouched.

    Check before adding one: a logo whose opaque pixels are overwhelmingly
    light needs this class. */
.ub-disco--dark {
    background: #232236;
    border-color: rgba(255, 255, 255, .14);
    color: #F0EDFA;
}

/*  Breathing room between the wordmark and the DisCo strip, and a bigger
    wordmark. They were butted together, which read as one run of logos
    rather than "us, then who we vend for". */
.ub-nav__in .ub-brand { font-size: 1.3125rem; margin-right: 1.25rem; }
.ub-nav__in .ub-brand svg { height: 34px; width: auto; }

/*  The selected meter's details, inside the buy card.

    Folded in from the separate "Meters" panel that used to sit below it with
    its own duplicate picker. Two columns of small label/value pairs, tucked
    under the picker so it reads as belonging to the meter you just chose
    rather than as another section.
*/
.ub-mdetail--inline {
    display: grid; grid-template-columns: 1fr 1fr;
    gap: .5rem .875rem;
    margin: .75rem 0 0;
    padding: .75rem .875rem;
    background: var(--ink-2);
    border: 1px solid var(--line);
    border-radius: var(--r);
}
.ub-mdetail--inline > div { min-width: 0; }
.ub-mdetail--inline dt {
    font-size: .625rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: .07em; color: var(--text-3); margin-bottom: .0625rem;
}
.ub-mdetail--inline dd {
    margin: 0; font-size: .8125rem; font-weight: 600; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ==========================================================================
   Tiled watermark (2026-08-02)

   The SUPPLIED mark, repeated faintly behind every page.

   ⚠ THIS WAS A REDRAW OF A REDRAW AND IT SHOWED. The data URI here drew the
   U freehand and reduced the dot cluster to ONE ROW OF THREE. The real mark
   has two rows of six dots in four colours. So the watermark, the header
   logo and the admin backdrop were three different logos.

   It is ubox-mark.png now — the file the brand actually supplied. The tile is
   392x448 (12 Aug 2026 re-extraction), so the box is sized to that ratio rather than forced square, which
   is what made the old redraw look squashed as well as wrong.

   Very low opacity on purpose. A watermark that competes with the content is
   not a watermark, and this sits behind a form people type card-adjacent
   details into. It is fixed rather than scrolling so it reads as the surface
   the page is printed on.
   ========================================================================== */
.ub-page::before {
    content: '';
    position: fixed; inset: 0;
    z-index: 0; pointer-events: none;
    /*  ⚠ THE WHOLE LOCKUP: THE U AND THE WORD, exactly as it sits top-left on
        every page. Not the bare mark, and not ubox-logo-01.png, which is a
        different lockup altogether and was a misreading.

        ubox-lockup-*.svg embeds ubox-mark.png byte for byte and sets the word
        in the site's own font stack at the same weight and colours as
        .ub-logo__word. Nothing is redrawn. It has to be embedded rather than
        referenced because an SVG used as a CSS background renders in secure
        static mode, where external images do not load.

        ⚠ THE PADDING IS INSIDE THE TILE, AND THAT IS THE TRICK. The canvas is
        320x170 for a logo about 160 wide. Scaling the tile down therefore
        makes the LOGO smaller while keeping the tiles apart, which is what was
        wanted: the marks were too big, and there were too many of them. Making
        the tile smaller alone would only have crowded them.  */
    /*  ⚠ THE U ALONE, SMALL, AND PLENTY OF THEM.

        The lockup with the wordmark was tried and dropped: at a size where
        the word was legible the tile had to be 260px, which left too few
        marks on screen. Asked for instead — keep the small U from that tile,
        lose the word, and have more of them.

        The padding is inside the canvas, so the U renders at 40px while the
        tiles repeat every 140px. About eleven across, against six before.

        One file for both themes. The mark is a purple-to-blue gradient and
        reads on either ground, which is why there is no light and dark pair
        here any more and no filter. */
    background-image: url("/images/ubox-tile-u.svg");
    background-repeat: repeat;
    /*  140px left the marks too far apart and too few of them. 100px puts
        very close to twice as many on screen for a given area, which is the
        step asked for — noticeably denser, still wallpaper rather than
        pattern.

        ⚠ THE WHOLE TILE SCALES, NOT THE SPACING ALONE. The padding is baked
        into the SVG canvas, so this shrinks the U with it, from 40px to about
        29px. That is the trade and it is the right way round: a smaller mark
        at this opacity reads as texture, whereas keeping it at 40px and
        closing the gaps would have the Us almost touching.

        Keep this the same in ubox-admin.css. The two are separate stylesheets
        and the wallpaper is the one thing that must not drift between them. */
    background-size: 100px 100px;
    opacity: .05;
}

/*  Light is the DEFAULT here — the palette is declared on
    `:root, :root[data-theme="light"]` and dark arrives inside
    @media (prefers-color-scheme: dark). A bare [data-theme="light"] selector
    misses everyone who has not touched the switch, because data-theme is null
    until they do. It just needs a touch more alpha on a light ground. */
:root[data-theme="light"] .ub-page::before { opacity: .06; }

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .ub-page::before { opacity: .05; }
}

/* Everything sits above it. */
.ub-page > * { position: relative; z-index: 1; }

:root[data-theme="light"] .ub-page::before { opacity: .05; }

/* Never behind the payment sheet or the token readout. */
#quidly-overlay-root, #quidly-pay-modal { z-index: 2147483647; }

/*  THE "STILL WAITING" LINE, AND WHY IT CAN BE SEEN AT ALL.
    ---------------------------------------------------------------------
    ⚠ THE Z-INDEX ABOVE IS THE MAXIMUM A 32-BIT INT HOLDS. There is no
    number that outranks Quidly's overlay, so this does not try: it takes
    the SAME z-index and relies on paint order instead - among positioned
    siblings with equal z-index, the one later in the DOM paints on top.
    payBackdrop.showWaitingNotice appends this to <body> only when the
    90-second mark is reached, which is minutes after their root mounted,
    so it is always the later sibling.

    Do not "fix" this by raising the number. There is nothing to raise it
    to, and moving the element earlier in the DOM would silently put it
    back behind their sheet - which is exactly the failure it exists to
    solve, and it would look fine in every test that does not have a real
    Quidly sheet open on top of it.

    Added 2026-08-18: a customer sat watching their spinner with nothing
    from us, because everything we render lives inside our card, and their
    sheet covers the whole card.                                        */
#ub-paywait-note {
    position: fixed;
    z-index: 2147483647;
    left: 50%;
    transform: translateX(-50%) translateY(-140%);
    bottom: max(1.25rem, env(safe-area-inset-bottom, 0px));
    width: min(30rem, calc(100vw - 2rem));
    box-sizing: border-box;
    padding: .7rem 1rem;
    border-radius: .55rem;
    background: var(--ub-card, #131A2B);
    color: var(--ub-fg, #E8EEF9);
    border: 1px solid var(--lcd, #60A5FA);
    box-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .45);
    font-size: .9rem;
    line-height: 1.4;
    text-align: center;
    opacity: 0;
    pointer-events: none;      /* never in the way of their own buttons */
    transition: opacity .25s ease, transform .25s ease;
}

#ub-paywait-note.on {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

@media (prefers-reduced-motion: reduce) {
    #ub-paywait-note { transition: none; }
}

/*  A BIGGER SHEET. Container only — nothing inside it is touched.
    ---------------------------------------------------------------------

    Reported 2026-08-02 with a photograph: "Card has expired" rendered as a
    thin red line about eight pixels tall, wedged between the expiry field
    and the Make Payment button, on a sheet narrow enough that the message
    had nowhere to go. A customer who mistypes an expiry date cannot see why
    the payment is refusing.

    This widens and heightens the sheet so their own layout has room to
    breathe — the error text then sits on its own line at a readable size
    rather than being crushed.

    WHY THIS IS SAFE WHERE RESTYLING THE FORM WAS NOT. These are width and
    min-height on Quidly's OUTERMOST container, the element they position
    themselves. Their fields, labels and buttons are untouched, so nothing
    here can make a field unusable — the failure mode if they rename the
    container is that the sheet stays its old size, which is where we are
    today anyway.

    What this canNOT fix is the size and colour of their error text. That
    lives inside their form and would mean styling `qw-*` internals, which
    has broken card entry once already. It is written up for Quidly in
    docs/PAYMENT-FEES-QUESTION.md instead — it is their bug to fix.  */
/*  CENTRED IN THE VIEWPORT, and considerably bigger.

    Asked for after seeing the 3-D Secure step, which takes the whole page
    and reads well for it. Worth being clear about what that page was:
    centinelapi.cardinalcommerce.com, the card scheme's own OTP step, on the
    bank's domain. We have no control over it whatsoever and never will —
    it is not Quidly's and it is not ours.

    Quidly's sheet IS ours to size, because it mounts into our document.
    So: centred both ways in the viewport, and given real room.

    Not literally 100vw. A card form stretched across a 1400px desktop is
    worse than a generous one, not better — the fields would be a metre wide
    and the eye has nowhere to rest. 640px is about as wide as a payment form
    should ever be. On a phone it does fill the screen, which is where "full
    page" actually helps. */
/*  NEVER 100vw, AND NEVER vh, ON A PHONE.

    The first version used `width: 100vw` and `min-height: 100vh` on mobile.
    On iOS `100vw` INCLUDES the scrollbar gutter and ignores the safe-area
    insets, so the sheet came out fractionally wider than the visual viewport.
    That produced horizontal overflow: the page slid left, lost its centring,
    and — because the document had genuinely become wider than the screen —
    stayed that way after the sheet closed. Reported from an iPhone, exactly
    as described.

    `100%` is measured against the containing block, not the layout viewport,
    so it cannot overflow. `dvh` follows the dynamic viewport as the browser
    chrome slides away, which `vh` does not.

    The overlay is pinned with position: fixed and inset: 0 so it is taken out
    of document flow entirely — nothing it does can change the width of the
    page underneath it, which is what made the breakage persist. */
#quidly-overlay-root {
    position: fixed !important;
    inset: 0 !important;
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    padding: env(safe-area-inset-top) env(safe-area-inset-right)
             env(safe-area-inset-bottom) env(safe-area-inset-left);
    overflow: auto !important;
    box-sizing: border-box !important;
}

/*  Enlarged 2026-09-07, owner's request: the sheet read small against all the
    page's spare real estate. This sizes the OUTER shell Quidly mounts into OUR
    document (no iframe — #quidly-pay-modal is a real node in our page), so we
    can give it more room; the FORM INTERNALS are Quidly's own layout, so this
    widens the panel and its padding, and their fields fill it as far as their
    own CSS allows. Bumped 640 -> 760 on width and the desktop min-height up a
    little; phone stays full-bleed below. */
#quidly-pay-modal {
    width: 100% !important;
    max-width: 820px !important;
    max-height: 100% !important;
    overflow-y: auto !important;
    margin: auto !important;
    box-sizing: border-box !important;
}

/*  Room to breathe on a desktop, where there is room to give. */
@media (min-width: 641px) {
    #quidly-pay-modal {
        min-height: min(90dvh, 880px) !important;
        /*  Enlarge the CONTENT, not just the shell — owner's follow-up:
            "increase the size of the information within." Quidly mount their
            widget into OUR DOM (no iframe), so their text and fields are ours
            to scale. `zoom` scales everything inside proportionally - text,
            inputs, buttons - WITHOUT naming any of their minified `qw-` class
            internals, which is the fragile edit the code has always avoided
            (a rule aimed at one of their fields breaks when they redeploy
            their bundle; a blanket zoom does not). Desktop only: the phone
            layout is already full-bleed and a zoom there would push content
            off-screen. 1.12 is a gentle bump; raise toward ~1.2 if the owner
            wants it larger still. Bumped 1.12 -> 1.22 on 2026-09-07: 12% was
            too subtle to read as bigger on the owner's tablet. */
        zoom: 1.22;
    }
}

/*  Full-bleed on a phone — but by filling its fixed parent, not by claiming
    a viewport width it cannot know. */
@media (max-width: 640px) {
    #quidly-overlay-root { padding: 0; }
    #quidly-pay-modal {
        max-width: 100% !important;
        min-height: 100% !important;
        border-radius: 0 !important;
    }
}

/*  Belt and braces: nothing may make the document scroll sideways. A payment
    sheet that leaves the site off-centre after it closes is worse than one
    that never looked quite right. */
html, body { overflow-x: hidden; max-width: 100%; }

/*  ⚠ QUIDLY'S OWN TOAST, HIDDEN — THE SECOND SOURCE OF THE DUPLICATE
    "PAYMENT SUCCESSFUL", DISTINCT FROM THE ONE 2155e48 ALREADY FIXED.
    ---------------------------------------------------------------------

    2155e48 tore down #quidly-pay-modal/#quidly-overlay-root on hide(), for
    "Payment successful" shown centred INSIDE their sheet. Reported again
    2026-08-12, same night, in the owner's own words: a green "Payment
    successful" in the MIDDLE of the screen at the same time as ours in the
    card. Read directly against their live SDK (quidly-pay.js, fetched from
    securepay.quidly.ng and inspected 2026-08-12): they bundle vue-toast-
    ification, configured

        { position: "top-center", timeout: 3000, toastClassName: "quidly-toast",
          bodyClassName: "quidly-toast-body" }

    and mount its container straight onto document.body — a THIRD subtree,
    a sibling of both #quidly-pay-modal and #quidly-overlay-root, not a
    child of either. payBackdrop.hide() in ubox-v2.js only ever knew about
    those two IDs, so a toast fired at any point — on their own success
    message, which position:top-center puts exactly where "the middle of
    the screen" reads from a phone — was never touched by that removal, and
    is not something JS teardown can race reliably anyway: their SDK can
    fire it from a postMessage handler that keeps listening after callback
    has already returned control to us.

    Hiding it in CSS sidesteps the timing question entirely — it is never
    visible, whenever their SDK decides to mount it, rather than removed a
    beat after it already was. vue-toastification's own container carries
    ITS class, not one of ours; that class name is standard library output
    (github.com/Maronato/vue-toastification), not a private `qw-*` internal,
    so this is no more fragile than the sizing rules just above. The
    `quidly-toast`/`quidly-toast-body` classes from their config are matched
    too, belt and braces, in case a future version of their widget changes
    the container selector but keeps the classes they explicitly named.

    Their PAY BUTTON and FORM remain completely untouched — same rule as
    always. This hides a redundant notification after the fact, exactly
    like removing #quidly-pay-modal on hide() already did; it does not
    touch anything Quidly needs the customer to interact with. */
.Vue-Toastification__container,
[class*="quidly-toast"] {
    display: none !important;
}

/* A DisCo chip at detail size, inside the buy card's meter summary. */
.ub-disco--sm { height: 26px; min-width: 40px; padding: 0 .375rem; border-radius: 7px; }
.ub-disco--sm img { max-height: 16px; max-width: 56px; }

/*  PHED's mark is white artwork, so on the white chip every other DisCo
    needs it would vanish. Rather than give that one chip a different colour
    — the strip should read as one row — the IMAGE carries its own dark
    backing. Their trademark is untouched; only what sits behind it changes. */
.ub-disco__onlight {
    background: #232236;
    border-radius: 5px;
    padding: 3px 5px;
}


/*  Label and its note, note right-aligned on the same line when there is
    room for it.

    The note used to carry white-space: nowrap with no shrink allowance on
    either flex child, so a long note (the Airtime/Bundles "detected from
    the number" hint) rendered at its full one-line width and hung off the
    right edge of the row, silently sliced by the page's overflow-x: hidden
    instead of wrapping. The field name never needs more than one short
    word, so it is pinned to its own width; the note gets the rest of the
    row and wraps within it once that space runs out. */
.ub-lab--row { display: flex; align-items: baseline; justify-content: space-between; gap: .75rem; }
.ub-lab--row > span:first-child { flex-shrink: 0; }
.ub-lab--row > span:last-child {
    font-weight: 400; font-size: .6875rem; color: var(--text-3);
    text-align: right; white-space: normal;
    min-width: 0; flex: 1 1 auto;
}

/*  The pitch sections belong to the landing page at step 1.

    "Three steps. That's the whole thing." and the feature grid sat below the
    buy card at every step, so they were still there while a token was being
    read. They are outside the Knockout root, so the model toggles this class
    on <html> instead of binding them directly. */
.is-buying .ub-sec--pitch,
/*  Shown when signed in too, for the same reason as .ub-hero__intro above:
    "how it works" and the feature band ARE the home page. Hiding them left a
    signed-in customer with a form and a footer. */
.is-signedin .ub-sec--pitch { display: block; }

/* ==========================================================================
   Long-form legal / support copy — supporting components (2026-08-02)

   ⚠ THE CARD SHELL AND BASE TYPOGRAPHY LIVE ON .ub-prose NOW, NOT HERE.
   Privacy and Support used to sit in their own .ub-panel.ub-legal box — flat
   background, no shadow, full page width — while Terms.cshtml got a
   dedicated card treatment (.ub-prose, further down this file): centred
   reading column, gradient surface, shadow, the works. The owner's reaction
   to Terms was "I love the way you've done that card... every other card,
   Privacy, Support should resemble this... laid out exactly the same way" —
   so all three pages now share the one .ub-prose shell (see Terms.cshtml,
   Privacy.cshtml, Support.cshtml) instead of Privacy/Support keeping a
   second, older card component that drifted from it.

   What is left in THIS block is the stuff that is genuinely specific to
   these two pages' content and has no equivalent on Terms — the short-version
   callout and the lawful-basis table on Privacy. Both are plain classes, not
   nested under a parent .ub-legal wrapper, so they work standalone inside
   .ub-prose exactly the way .ub-warnbox already does. */

/*  Sits under a rule rather than beside the text: it is a footer, not a
    clause. Scoped as ".ub-prose .ub-legal__foot" (two classes), not bare
    ".ub-legal__foot" (one) — this is itself a <p>, and .ub-prose p (below)
    is a class+tag selector at equal-or-higher specificity than a single
    class. Without the extra scoping the LATER rule in the file wins ties,
    which is .ub-prose p, and this footer line loses its own margin and
    colour to the ordinary paragraph rule. */
.ub-prose .ub-legal__foot {
    margin: 1.75rem 0 0; padding-top: 1rem;
    border-top: 1px solid var(--line);
    font-size: .8125rem; color: var(--text-3);
}

.ub-legal__tldr {
    background: var(--ink-2);
    border: 1px solid var(--line);
    border-left: 3px solid var(--dot-2);
    border-radius: var(--r);
    padding: 1.125rem 1.25rem .875rem;
    margin-bottom: 1.75rem;
}
/*  Both scoped under .ub-prose for the same reason as .ub-legal__foot above:
    .ub-prose h2 / .ub-prose ul (below) are equal-specificity class+tag
    selectors that would otherwise win the tie by appearing later in the
    file, flattening this box's own "smaller, no top margin" heading and
    "no gap after the last bullet" list back to the ordinary prose values. */
.ub-prose .ub-legal__tldr h2 { margin-top: 0; font-size: .9375rem; letter-spacing: .02em; }
.ub-prose .ub-legal__tldr ul { margin-bottom: 0; }

/*  Two columns of short phrases. A real <table> would be right, but the rest
    of the page is CSS grid and a table here would need its own reset to stop
    looking like a spreadsheet, so this carries table roles instead and the
    reader gets the same semantics. */
.ub-legal__table {
    border: 1px solid var(--line); border-radius: var(--r);
    overflow: hidden; margin: .75rem 0 1rem;
}
.ub-legal__row {
    display: grid; grid-template-columns: 1fr 12.5rem; gap: 1rem;
    padding: .625rem .875rem;
    border-top: 1px solid var(--line);
    font-size: .875rem; color: var(--text-2); line-height: 1.5;
}
.ub-legal__row:first-child { border-top: 0; }
.ub-legal__row--h {
    background: var(--ink-2);
    font-size: .75rem; font-weight: 650; letter-spacing: .06em;
    text-transform: uppercase; color: var(--text-3);
}
/* One column on a phone: 12.5rem of lawful basis beside a wrapped purpose is
   two words per line in both halves. */
@media (max-width: 640px) {
    .ub-legal__row { grid-template-columns: 1fr; gap: .1875rem; }
    .ub-legal__row--h { display: none; }
    .ub-legal__row > span:last-child { color: var(--text-3); font-size: .8125rem; }
}

/* ==========================================================================
   Admin (2026-08-02)

   Dense on purpose. This is the one screen where the job is reading numbers
   off a table, so the type is smaller than anywhere else on the site and the
   rows are tight. Every table scrolls inside its own box rather than widening
   the page - eight columns of transaction data will not fit a laptop, and a
   page that scrolls sideways loses its own row headings.
   ========================================================================== */
.ub-admin__kpis {
    display: grid; gap: .75rem; margin-bottom: 1.25rem;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}

/*  The two figures nobody wants to look at get the warm accent. Not red: red
    reads as "broken right now", and these are a backlog. Colour is not the
    only signal either - the labels say what they are. */
.ub-stat--warn { border-color: rgba(245,158,11,.35); }
.ub-stat--warn .ub-stat__v { color: var(--dot-3); }

.ub-admin__note { font-size: .8125rem; color: var(--text-2); margin: -.375rem 0 .875rem; line-height: 1.55; }
.ub-admin__count { font-size: .75rem; color: var(--text-3); }
.ub-admin__sub { display: block; font-size: .6875rem; color: var(--text-3); }

.ub-admin__scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.ub-admin__t { width: 100%; border-collapse: collapse; font-size: .8125rem; }
.ub-admin__t th {
    text-align: left; font-size: .6875rem; font-weight: 650;
    letter-spacing: .06em; text-transform: uppercase; color: var(--text-3);
    padding: .5rem .625rem; border-bottom: 1px solid var(--line-hi);
    white-space: nowrap;
}
.ub-admin__t td {
    padding: .5rem .625rem; border-bottom: 1px solid var(--line);
    color: var(--text-2); vertical-align: top;
}
.ub-admin__t tr:last-child td { border-bottom: 0; }
.ub-admin__t .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.ub-admin__t .mono { font-family: var(--mono); font-size: .75rem; color: var(--text); white-space: nowrap; } /* was var(--text-1), never defined */

/* A failed row is dimmed rather than reddened: most of them are ordinary
   abandoned checkouts, not incidents. */
.ub-admin__t tr.is-failed td { opacity: .55; }

/*  Platform totals under the three steps (2026-08-02).

    Four counts, between the steps and the DisCo marks - the "is this real"
    part of the column: what we have done, and who we can do it for.
    Four cards in a row of their own, with clear air above them. Set loose as
    bare figures they ran straight on from step 3 and read as a fourth step.
    A boxed card says "this is a different kind of thing" without a heading
    having to say it.

    ONE ROW. auto-fit was wrapping the fifth card onto a line of its own, which
    read as an afterthought rather than a fifth fact. Equal fractions keep all
    five on one line at every desktop width; the type inside is small enough to
    survive the narrow column that buys. Two rows of small cards would be fine
    at eight of them - it is not fine at five. */
.ub-trust {
    display: grid; gap: .5rem;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    margin-top: clamp(1.75rem, 3.5vw, 2.5rem);
}
.ub-trust__i {
    display: flex; flex-direction: column; gap: .1875rem;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r);
    padding: .875rem .9375rem;
}
.ub-trust__v {
    font-size: clamp(1rem, 1.5vw, 1.3125rem); font-weight: 750; letter-spacing: -.02em;
    font-variant-numeric: tabular-nums; color: var(--text); /* was var(--text-1), never defined */
    line-height: 1.1;
}
.ub-trust__l { font-size: .6875rem; color: var(--text-3); line-height: 1.3; }

/*  Each card takes one of the mark's four accents as a top rule - the same
    device the account summary tiles use, so the two read as one system. */
.ub-trust__i { border-top: 3px solid var(--line-hi); }
.ub-trust__i:nth-child(1) { border-top-color: var(--dot-1); }
.ub-trust__i:nth-child(2) { border-top-color: var(--dot-2); }
.ub-trust__i:nth-child(3) { border-top-color: var(--dot-3); }
.ub-trust__i:nth-child(4) { border-top-color: var(--dot-4); }

@media (max-width: 640px) {
    .ub-trust { grid-template-columns: repeat(2, 1fr); gap: .5rem; }
    .ub-trust__v { font-size: 1.25rem; }
    .ub-trust__i { padding: .75rem .8125rem; }
}

/* ==========================================================================
   RESPONSIVE NAVIGATION — DELIBERATELY LAST IN THIS FILE.

   These rules were 2,000 lines earlier and did not work on a phone. The
   reason is worth writing down because it has now caught me twice:

       MEDIA QUERIES DO NOT ADD SPECIFICITY.

   `.ub-nav__in .ub-brand` appears again near the end of this file at
   (0,2,0) with font-size 1.3125rem. My mobile rule had the SAME specificity
   and came earlier, so the later one won at every width and the logo stayed
   full size on an iPhone while everything around it shrank.

   Being last is the fix. Do not move this block back up, and if a later rule
   is ever added for .ub-nav__in, put it above this.
   ========================================================================== */
/*  ---- THE NAVIGATION SCALES CONTINUOUSLY, NOT IN STEPS ----

    Signed in, the bar carries the wordmark plus "My Account", "Support",
    "Sign out" and the theme toggle. Two things were wrong on a phone:

    1. The links WRAPPED — "My Account" became "My" / "Account" and
       "Sign out" became "Sign" / "out", inside pills sized for one line, so
       the items sat at different heights and the bar read as broken. Fixed
       with white-space: nowrap where .ub-nav__links a is defined; that is the
       actual defect and it would have happened at any font size.

    2. It stepped at breakpoints. Android is not one width — 360, 384, 393,
       412 and 430 are all common, and a device at 412px got the tablet
       sizing while one at 399px got the phone sizing, for no reason a user
       could see.

    So the whole bar is now FLUID: every dimension is a clamp() against vw,
    so it scales smoothly from 320px to 640px and every Android width lands
    somewhere sensible without needing its own breakpoint.

    Measured at 390px (iPhone 12/13/14): links 11px, no wrapping, all three
    on the same baseline, no horizontal overflow.

    Nothing is hidden at any size. "Sign out" disappearing on a phone is
    worse than "Sign out" being small — it is the one control a customer on
    a shared device genuinely needs. And the wordmark stays: it was hidden
    below 400px for one round, which removed the brand name from most
    iPhones. A logo you cannot read is not a space saving.  */
@media (max-width: 640px) {
    /*  ⚠ THE THEME TOGGLE HUNG OFF THE RIGHT EDGE. Reported from an iPhone:
        the circle sat hard against the screen and looked clipped.

        It is an OVERFLOW, not a margin. The brand, the links and the toggle
        together were wider than .wrap's content box, and because the toggle
        is `flex: none` and last, the excess pushed it out past the 1.25rem
        padding — so the padding meant to keep it clear was doing nothing.

        Both lines below are needed: less side padding buys real room, and
        letting the links shrink lets the row fit instead of spilling.

        ⚠ INSIDE THE 640px QUERY, so the desktop bar is untouched. */
    .ub-nav__in {
        gap: clamp(.125rem, 1.2vw, .5rem);
        height: clamp(52px, 14vw, 58px);
        padding-inline: .75rem;
    }
    /*  min-width:0 is the part that gets left out. A flex item will not
        shrink below its content width without it, whatever flex says. */
    .ub-nav__links { min-width: 0; flex-shrink: 1; }
    .ub-nav__in .ub-brand {
        font-size: clamp(.75rem, 3.3vw, 1.0625rem);
        margin-right: 0;
        gap: clamp(.25rem, 1vw, .375rem);
    }
    .ub-nav__in .ub-brand svg {
        height: clamp(20px, 5.6vw, 26px);
        width: auto;
    }
    .ub-nav__links { gap: 0; }
    .ub-nav__links a {
        font-size: clamp(.625rem, 2.9vw, .8125rem);
        padding: clamp(.25rem, 1vw, .375rem) clamp(.25rem, 1.3vw, .4375rem);
    }
    /*  The theme toggle is the least important thing in the bar, so it gives
        up its width first rather than squeezing the links. */
    /*  right matches .ub-nav__in's own padding-inline at this width (above,
        .75rem) - see the sticky-clamp comment on the base .ub-theme rule. */
    .ub-nav__in .ub-theme {
        width: clamp(26px, 7.5vw, 34px);
        height: clamp(26px, 7.5vw, 34px);
        flex: none;
        right: .75rem;
    }
}

/*  ---- NETWORK STRIP ON THE AIRTIME PAGE ----

    The same component as the DisCo strip on /v2, with one difference: four
    logos rather than six, so the grid is told four columns instead of
    inheriting six and leaving two empty cells at the end of the row.

    Everything else — the white chip, the height, the image treatment, the
    responsive collapse — is shared, because it is the same idea answering
    the same question on a different page. Two implementations of "who do we
    cover" would drift, and one of them would be the one nobody updated. */
/*  ⚠ ONE ROW, ALWAYS - NEVER WRAPS, EVEN ON MOBILE. Explicit standing
    instruction, 2026-08-13, after two grid/flex-wrap attempts (auto-fit,
    then flex-wrap with row-centering) both let a phone-width screen split
    this strip across two or three rows: "everything should be on the same
    line... if it's squashed on the same line because it's mobile, I don't
    care." Airtime carries 4 of these chips, Data Bundles 7 - flex with
    NO wrap and each chip allowed to shrink is what makes both fit one row
    at any width: the whole strip compresses together rather than any chip
    dropping to a second line. This does mean chips get genuinely small on
    a narrow phone - that trade was made explicitly, not by accident. */
.ub-discos--nets {
    display: flex;
    flex-wrap: nowrap;
    gap: .375rem;
}

.ub-discos--nets .ub-disco {
    flex: 1 1 0;
    min-width: 0;
}

@media (max-width: 860px) {
    .ub-discos--nets { gap: .25rem; }
}

.ub-discos--nets .ub-disco { padding: 0 .25rem; }

/*  The image chips carry no text; the text-only ones (Smile, Glo SME,
    9mobile SME - see Bundles.cshtml) must not wrap to a second line inside a
    52px-tall chip sized for one. */
.ub-discos--nets .ub-disco { white-space: nowrap; }

/*  VTpass publish these as photographs with their own white margins, so they
    need slightly more room inside the chip than the DisCo marks do or they
    read as postage stamps. */
.ub-discos--nets .ub-disco img { max-height: 56px; }

/*  Glo SME / 9mobile SME: the ordinary mark, shrunk to make room for a small
    "SME" tag beside it - the same company's chip, not a new one, so it reads
    as a variant rather than a duplicate logo in the strip. */
.ub-disco--tag { gap: .3125rem; }
.ub-disco--tag img { max-height: 30px !important; }
.ub-disco--tag b {
    font-size: .5625rem; font-weight: 800; letter-spacing: .02em;
    color: var(--text-3); border: 1px solid var(--disco-edge, rgba(0,0,0,.14));
    border-radius: 4px; padding: 1px 3px; white-space: nowrap;
}

/*  ⚠ NARROW PHONES ONLY (confirmed on the owner's iPhone, mobile Safari,
    2026-08-13): the one-row `nowrap` strip above (`.ub-discos--nets`) is a
    standing instruction and stays exactly as it is. But at phone widths the
    strip compresses every chip to its equal share of a much narrower row,
    and the two tag chips carry BOTH a logo and the "SME" badge side by side
    - there is no longer room for both across, so the badge clipped into the
    chip edge or the neighbour. The strip must not wrap to a second row; the
    two-part CONTENT of just these two chips can, onto a second line inside
    their own box, which needs far less width than fitting image + badge
    side by side. Scoped to `.ub-disco--tag` only - the five plain-logo
    chips are unaffected and keep their single-row inline layout. */
@media (max-width: 480px) {
    .ub-discos--nets .ub-disco--tag {
        flex-direction: column;
        justify-content: center;
        gap: .125rem;
        padding: .1875rem .125rem;
    }
    .ub-discos--nets .ub-disco--tag img { max-height: 24px !important; }
    .ub-discos--nets .ub-disco--tag b { font-size: .5rem; padding: 0 2px; }
}

/* ==========================================================================
   THE SERVICE SELECTOR — account page
   ==========================================================================
   A row of chips above the two columns, choosing which service the whole page
   is about.

   Tabs rather than a dropdown: five options is inside the range where showing
   them all costs less than hiding them behind a tap, and on a phone a chip is
   an easier target than a native select.

   Scrolls horizontally below 520px rather than wrapping to two rows. A tab
   strip that wraps stops reading as one control, and the second row usually
   goes unnoticed.
   ========================================================================== */
.ub-svctabs {
    display: flex;
    flex-wrap: nowrap;
    gap: .5rem;
    margin: 0 0 1.25rem;
    padding-bottom: .25rem;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
.ub-svctabs::-webkit-scrollbar { display: none; }

.ub-svctab {
    flex: none;
    padding: .5rem 1rem;
    border-radius: 999px;
    border: 1px solid var(--line-hi);
    background: var(--surface);
    color: var(--text-2);
    font-family: inherit;
    font-size: .875rem;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.ub-svctab:hover { border-color: var(--amber); color: var(--text); }

/*  The selected one is the brand colour, filled. A border-only treatment was
    not distinguishable enough from hover at a glance. */
.ub-svctab.on {
    background: var(--amber);
    border-color: var(--amber);
    color: #FFFFFF;
}

/*  The stand-in card for a service bought on its own page. Same shell as the
    buy card so the column does not change shape when the tab changes. */
.ub-acct__svc { display: flex; flex-direction: column; gap: 1rem; }
.ub-acct__svcp { margin: 0; color: var(--text-2); font-size: .9375rem; line-height: 1.6; }

/*  The service picker on the account page. A dropdown rather than tabs:
    five today and more later, and a select is the control people already
    know for "which one of these". It also costs no width, which matters
    sitting above a two-column layout. */
.ub-svcpick { max-width: 20rem; margin: 0 0 1.5rem; }
.ub-svcpick .ub-lab { margin-bottom: .375rem; }

/* ------------------------------------------------- account: the service picker --- */

/*  Full width of the card below it, and the same corner radius and border, so
    it reads as part of the same control group rather than as a stray box.

    It was narrower than the card, which is the kind of few-pixels difference
    that makes a page look unfinished without anybody being able to say why. */
.ub-acct__svc {
    display: block;
    width: 100%;
    margin-bottom: .75rem;
    font-size: 1rem;
    font-weight: 600;
    padding: .8rem .9rem;
}

/*  THE HISTORY WAS TOO TALL. Every row of a long-standing account rendered at
    once, so the page ran on for screens and the columns beside it ended in a
    large empty space - the "blank area" that looked broken rather than roomy.

    Capped and scrolled instead: about eight rows visible, the rest a scroll
    inside the panel. The filters above it stay put, which is what makes a cap
    acceptable - you narrow the list rather than hunt down it. */
.ub-tx__scroll {
    max-height: 26rem;
    overflow-y: auto;
    overscroll-behavior: contain;
}

@media (max-width: 720px) {
    .ub-tx__scroll { max-height: 20rem; }
}

/* ---------------------------------------- account: service detail + picker --- */

/*  THE NETWORK MARK, at the size of the DisCo mark beside a meter.

    It rendered at the artwork's natural size, which for mtn.jpg is far larger
    than the DisCo logos and made the airtime card look like a different page.
    Capped to the same height the DisCo mark uses, width auto so nothing is
    squashed. */
.ub-mdetail .ub-tx__logo {
    height: 20px;
    width: auto;
    max-width: 76px;
    object-fit: contain;
    vertical-align: middle;
}

/*  A SELECT THAT LOOKS LIKE A SELECT.

    Bare, it read as a heading in a box - there was no way to tell it could be
    opened. This used to draw its own caret (a #888, 2.2-stroke, 24x24-viewBox
    SVG) - the one holdout still doing that after every other picker moved
    onto --ub-chevron, found 2026-08-14 in the same sweep that unified the
    admin nav dropdowns. Now reads the same variable as .ub-sel / .uadm-sel,
    with matching position/size/padding, so a service picker that ever
    reuses this class draws the identical arrow rather than a close cousin. */
.ub-acct__svc {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    background-image: var(--ub-chevron);
    background-repeat: no-repeat;
    background-position: right 1rem center;
    background-size: 15px;
    padding-right: 2.75rem;
}

/*  The empty chart, holding the same vertical space the real one would, so
    switching service does not make the column jump. */
.ub-chart__empty {
    padding: 1.25rem 0 2rem;
}
/*  ⚠ .ub-reward* (THE PAY-STEP REDEEM CARD) REMOVED 2026-08-12, along with
    the markup in Index.cshtml and the view-model block in ubox-v2.js it
    styled. See the note in Account.cshtml where the account-page rewards
    card used to be for the rest of the loyalty program cleanup. Pre-existing
    and unrelated: .ub-rewards / .ub-rewardcard below are dead CSS from that
    earlier, 2026-08-09 removal, out of scope for this pass. */

/*  ---- THE PROFILE NAME EDITOR ----

    A <details> because it is done once and then ignored. Native disclosure:
    keyboard and screen-reader correct for free, no script. */
.ub-prof { margin: 0 0 1rem; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); }
/*  The canonical disclosure. Everything else lines up with THIS, through
    --disc-inset, rather than by repeating its numbers. */
/*  ⚠ :not(.ub-cardh), OR IT BEATS THE SHARED CARD HEADING ON SPECIFICITY.
    `.ub-prof > summary` scores (0,1,1) against `.ub-cardh` at (0,1,0), so this
    rule's `display: flex` won and the Details card alone rendered as flex
    while the other three were grid. In flex the dot has no column to sit in,
    so it was flung to the far right of the row and the title shifted left —
    reported twice as "why is Details still wrong".

    Nothing about the card was wrong. One legacy selector outranked the new
    one, which is the same trap as the `content: none` reset a few hundred
    lines up. Any rule here that targets a summary must exclude .ub-cardh. */
.ub-prof > summary:not(.ub-cardh) {
    padding: .75rem var(--disc-inset); cursor: pointer; font-size: .9375rem; font-weight: 600;
    list-style: none; display: flex; align-items: center; gap: .5rem;
}
.ub-prof > summary::-webkit-details-marker { display: none; }
.ub-prof > summary:hover { color: var(--dot-1); }

/*  ==========================================================================
    .ub-disc — THE ONE DISCLOSURE MARKER. Arrow on the LEFT, before the text.

    Put this class on any <summary> and it gets the house arrow. Every
    pull-down on the site uses it; see the note on --disc-mark in :root for
    why the arrow moved from the right-hand end to the front.

    ⚠ ROTATES, IT DOES NOT SWAP GLYPHS. One ▸ turned 90° is unmistakably the
    same control in a second state, where ▸ becoming ▾ can read as two
    different marks. It also animates, which swapping content cannot.

    ⚠ flex-shrink: 0 AND A FIXED WIDTH, or the arrow is squeezed to nothing
    when the heading beside it is long — the exact case where somebody most
    needs to see the row is openable.
    ========================================================================== */
.ub-disc { display: flex; align-items: center; gap: .5rem; cursor: pointer; list-style: none; }
.ub-disc::-webkit-details-marker { display: none; }
.ub-disc::before {
    content: '▸';
    flex: 0 0 auto;
    width: var(--disc-mark);
    color: var(--text-3);
    font-size: var(--disc-mark);
    line-height: 1;
    transition: transform .15s ease;
    transform-origin: 45% 50%;
}
details[open] > .ub-disc::before { transform: rotate(90deg); }
.ub-disc:hover::before { color: var(--text); }

/*  The old right-hand arrows, retired.

    ⚠ :not(.ub-cardh), AND THAT IS THE WHOLE POINT. As first written this
    matched .ub-cardh::after too — which is now the DOT, not an arrow. So
    "Your details" rendered with NO DOT, its grid row had one fewer child, and
    its title landed in a different column from the other three cards.

    Reported as "the second card is still not in line" after the headings had
    supposedly been unified. The headings were unified; a leftover reset three
    hundred lines above was quietly deleting one of their parts. */
.ub-prof > summary:not(.ub-cardh)::after,
.ub-mrow summary:not(.ub-cardh)::after,
.ub-ownerbox > summary:not(.ub-cardh)::after { content: none; }
.ub-diag summary::before { content: '▸ '; }
/*  See the note on the same pair earlier in this file: .ub-diag IS the
    <details>, so the descendant selector this used could never match. */
.ub-diag[open] > summary::before { content: '▾ '; }

.ub-prof__n { margin: 0 1rem .875rem; color: var(--text-3); font-size: .8125rem; line-height: 1.6; }
.ub-prof__row { display: flex; gap: .75rem; padding: 0 1rem; flex-wrap: wrap; }
.ub-prof__row label { flex: 1 1 12rem; display: flex; flex-direction: column; gap: .25rem; font-size: .8125rem; color: var(--text-2); }
.ub-prof__pw { display: flex; flex-direction: column; gap: .25rem; padding: .75rem 1rem 0; font-size: .8125rem; color: var(--text-2); }
.ub-prof__act { display: flex; align-items: center; gap: .75rem; padding: .875rem 1rem 1rem; flex-wrap: wrap; }
/*  var(--good), NOT var(--ok, #16a34a) — 2026-08-16 audit. --ok is defined
    NOWHERE in this file, so the fallback literal was the value every
    customer actually got: one flat green tuned for neither ground, ~3.9:1
    on a dark card at this small size. --good is the site's one success
    green and is themed per ground. */
.ub-prof__msg { font-size: .8125rem; color: var(--good); }
.ub-prof__msg.is-bad { color: var(--danger); }

/*  Divides the name fields from the email change. They live in one panel but
    they are not the same kind of change: a name is a label, the address is
    the account identifier and where every token is sent. The rule is what
    stops the second set of fields reading as more of the first. */
.ub-prof__sep {
    height: 1px;
    background: var(--line);
    margin: .375rem 1rem .875rem;
}

/*  ---- YOUR ADDRESS IS THE WAY INTO YOUR DETAILS ----

    Replaces "Hello, <name>" over the email. A customer who has just signed
    in knows who they are; what they cannot find is where to change anything.
    So the address in the corner became the control, because that is where
    everybody looks for it. */
.ub-acct__me {
    display: inline-flex; align-items: center; gap: .4rem;
    padding: .2rem .5rem; margin: -.2rem -.5rem;
    border: 0; border-radius: 8px;
    background: transparent; color: inherit;
    font: inherit; cursor: pointer;
}
.ub-acct__me:hover { background: var(--surface-2); color: var(--dot-1); }
.ub-acct__me:focus-visible { outline: 2px solid var(--dot-2); outline-offset: 2px; }
.ub-acct__me__i { opacity: .5; font-size: .8em; }
.ub-acct__me:hover .ub-acct__me__i { opacity: 1; }

/*  Shown while no meter is chosen, in place of the totals, the chart and the
    history — none of which mean anything until there is one. */
.ub-acct__pick {
    margin: 0 0 1rem; padding: .875rem 1rem;
    border: 1px dashed var(--line); border-radius: 12px;
    color: var(--text-3); font-size: .875rem; line-height: 1.6;
}

/*  ---- SHOW / HIDE ON A PASSWORD FIELD ----
    Inside the field's box, not beside it, so the form keeps one column and the
    input keeps full width on a phone. */
.ub-pw { position: relative; display: block; }
.ub-pw .ub-in { width: 100%; padding-right: 4.25rem; }
.ub-pw__eye {
    position: absolute; right: .5rem; top: 50%; transform: translateY(-50%);
    padding: .3rem .55rem;
    border: 1px solid var(--line); border-radius: 6px;
    background: var(--surface-2); color: var(--text-2);
    font: inherit; font-size: .75rem; font-weight: 600;
    cursor: pointer;
}
.ub-pw__eye:hover { color: var(--dot-1); border-color: var(--dot-1); }
.ub-pw__eye:focus-visible { outline: 2px solid var(--dot-2); outline-offset: 2px; }

/*  ---- THE DISCLOSURE ARROW ON <details> SUMMARIES ----

    The owner box expanded when tapped and gave no sign it could be. A
    <summary> with `list-style: none` (or inside a flex container, which
    strips it in WebKit) loses the native triangle, so the only way to
    discover the control was to try it.

    Drawn rather than relying on the native marker, so it is the same on every
    browser and rotates instead of swapping glyph. */
/*  ⚠ padding 1rem TO MATCH .ub-prof, or the two arrows do not line up.
    .ub-prof > summary has `padding: .75rem 1rem`, so its ▾ sits 1rem from the
    right edge; this one sat at .75rem. A quarter of a rem out, on two cards
    stacked directly above one another, which is close enough to look like a
    mistake and far enough to be visible. Both are now 1rem. */
.ub-ownerbox > summary {
    position: relative; cursor: pointer; list-style: none;
    /*  Room for the arrow plus the shared inset, so long text never runs
        under it however wide the card gets. */
    padding-right: calc(var(--disc-inset) + 1.25rem);
}
.ub-ownerbox > summary::-webkit-details-marker { display: none; }
/*  ⚠ ▾ / ▴ LIKE EVERY OTHER DISCLOSURE HERE. This drew its own chevron out
    of two CSS borders and rotated it, which was a nicer animation and the
    THIRD distinct marker on the site. Consistency beats the animation: the
    same gesture should carry the same mark everywhere, and "Your details" is
    the one people meet first. */
.ub-ownerbox > summary::after {
    content: '▾'; position: absolute; right: var(--disc-inset); top: 50%;
    transform: translateY(-50%);
    color: var(--text-3); font-size: .875rem; line-height: 1;
    opacity: .8;
}
.ub-ownerbox[open] > summary::after { content: '▴'; }
.ub-ownerbox > summary:hover::after { opacity: 1; }

/*  ---- SPINNER VISIBILITY ----

    Same size, same three arcs, easier to see. Each arc was a 2px stroke of a
    single brand colour on a light ground, which at 17px is a hairline that
    reads as a smudge rather than as motion.

    Thicker stroke, and each arc now covers TWO sides instead of one, so at any
    moment there is roughly twice as much line on screen. The colours are
    unchanged - it is still the logo's three dots - it is the weight that was
    wrong, not the palette. */
.ub-spin {
    border-width: 2.5px;
    border-top-color: var(--dot-1);
    border-right-color: color-mix(in srgb, var(--dot-1) 45%, transparent);
}
.ub-spin::before,
.ub-spin::after { border-width: 2.5px; }
.ub-spin::before {
    border-right-color: var(--dot-2);
    border-top-color: color-mix(in srgb, var(--dot-2) 45%, transparent);
}
.ub-spin::after {
    border-bottom-color: var(--dot-3);
    border-left-color: color-mix(in srgb, var(--dot-3) 45%, transparent);
}

/*  ---- NO FOCUS RING ON THE STEP HEADING ----

    The heading carries tabindex="-1" and is focused in script every time the
    step changes, so a screen reader announces "Pay" instead of leaving the
    reader on a button that has just vanished. The global :focus-visible rule
    then drew an amber ring around that one heading and nothing else, which
    reads as a box round step 2 for no reason anybody can see.

    Safe to remove: tabindex="-1" means it is NOT reachable by tabbing, so no
    keyboard user can ever focus it deliberately and lose an indicator. The
    focus move stays; only the paint goes. */
#ub-step-heading:focus,
#ub-step-heading:focus-visible { outline: none; }

/*  ---- THE TOKEN READOUT HAS TO SURVIVE BIG NUMBERS ----

    It was a flex row with NO WRAP and a fixed 1.75rem gap. At "4.4 kWh" and
    "1,000" that looks right; at "4,000 kWh" and "1,000,000" on a narrow phone
    the two blocks run into each other, because nothing was allowed to shrink
    and nothing was allowed to drop to a second line.

    Three changes, all of which keep it centred:

      wrap        two columns while they fit, stacked and still centred when
                  they do not - rather than overlapping
      shrink      the figures scale with the viewport instead of sitting at a
                  fixed 1.25rem
      min-width:0 a flex child defaults to min-width:auto, which REFUSES to go
                  below its content width. Without this the row overflows the
                  card instead of wrapping, which is the actual bug. */
.ub-lcd__sub {
    flex-wrap: wrap;
    gap: .875rem 1.75rem;
    align-items: start;
}
.ub-lcd__sub > div {
    min-width: 0;
    flex: 0 1 auto;
    text-align: center;
}
.ub-lcd__sub b {
    font-size: clamp(1rem, 4.5vw, 1.25rem);
    line-height: 1.2;
    white-space: nowrap;
}

/*  The token itself already scrolls rather than wrapping - a meter token must
    never be broken across lines, because somebody reading it into a keypad
    would lose their place. Left as is deliberately. */

/*  ---- THE PASSWORD EYE ----

    Both eyes live in the SVG and CSS shows one, so pressing the button never
    rebuilds the markup and never drops the focus ring mid-interaction. The
    script only toggles .is-shown.

    Crossed-out eye while hidden, open eye once revealed: the icon shows what
    you GET by pressing it. */
.ub-pw { position: relative; display: block; }
.ub-pw .ub-in { width: 100%; padding-right: 3rem; }
.ub-eye {
    position: absolute; right: .375rem; top: 50%; transform: translateY(-50%);
    display: inline-flex; align-items: center; justify-content: center;
    width: 2.25rem; height: 2.25rem; padding: 0;
    border: 0; border-radius: 8px;
    background: transparent; color: var(--text-3);
    cursor: pointer;
}
.ub-eye svg { width: 1.25rem; height: 1.25rem; }
.ub-eye:hover { color: var(--dot-1); background: var(--surface-2); }
.ub-eye:focus-visible { outline: 2px solid var(--dot-2); outline-offset: 2px; }

/*  Hidden is the default, so the crossed-out eye shows and the open one does
    not. .is-shown swaps them. */
.ub-eye .ub-eye__open { display: none; }
.ub-eye.is-shown .ub-eye__shut { display: none; }
.ub-eye.is-shown .ub-eye__open { display: inline; }
.ub-eye.is-shown { color: var(--dot-1); }

/*  ==========================================================================
    YOUR REWARDS — one card, not two (2026-08-05)

    Rewards and "Your details" were two stacked cards: two headings, two
    arrows, two boxes, for what is one thing. They are one pull-down now.

    ⚠ AND EXACTLY ONE PURPLE DOT. `.ub-panel__h h2::before` already draws it.
    A .ub-disc__dot span was added on top and every heading got TWO — do not
    add a second dot to a panel heading, ever.
    ========================================================================== */
.ub-rewardcard .ub-prof {
    border: 0; background: none; margin: 1.25rem 0 0; padding: 1.25rem 0 0;
    border-top: 1px solid var(--line);
}
.ub-rewardcard .ub-prof__h {
    margin: 0 0 .25rem; font-size: .9375rem; font-weight: 600; color: var(--text);
}
.ub-rewardcard .ub-prof__n { margin: 0 0 .875rem; padding: 0; }
.ub-rewardcard .ub-prof__row { padding: 0; }

/*  ==========================================================================
    ONE ALIGNMENT FOR ALL FOUR ACCOUNT CARDS (2026-08-05)

    Your rewards, Your details, Electricity purchased and Transaction history
    are the same kind of thing and must line up exactly: same left edge, same
    arrow position, same heading size. They did not — .ub-prof and
    .ub-ownerbox each carried their own padding, border and radius from when
    they were standalone boxes, so their headings sat a few pixels in from the
    panels above and below and the whole column looked stepped.

    Everything is a .ub-panel now and inherits the panel's padding, so there
    is ONE number controlling the left edge rather than three that have to be
    kept equal by hand.
    ========================================================================== */
.ub-prof.ub-panel,
.ub-rewardcard { padding: 1.5rem; margin-bottom: 1.25rem; }
.ub-prof.ub-panel > summary,
.ub-rewardcard > summary { margin-bottom: 0; }
details.ub-panel[open] > summary { margin-bottom: 1.25rem; }

/*  Headings identical across all four, whatever element carries them. */
.ub-panel__h h2 { margin: 0; font-size: 1.1875rem; line-height: 1.3; }

/*  The rewards figures were pushed to the right by a justify-content the
    standalone card used to need. Inside a panel they start at the left edge
    like everything else on the page. */
.ub-rewardcard .ub-rewards { justify-content: flex-start; gap: 2.5rem; }

/*  The owner-only explainer is a block inside the rewards card now, not its
    own pull-down: a rule above it, no border, no radius, no second arrow. */
.ub-rewardcard .ub-ownerbox {
    border: 0; background: none; border-radius: 0;
    margin: 1.25rem 0 0; padding: 1.25rem 0 0;
    border-top: 1px solid var(--line);
}
.ub-rewardcard .ub-ownerbox__h {
    display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
    font-size: .9375rem; font-weight: 600; margin-bottom: .875rem; padding: 0;
}

/*  The two meter-scoped cards are always PRESENT now; only their contents
    wait for a meter to be chosen. Hiding the whole card left the account page
    with two of its four sections missing, which reads as broken rather than
    as "nothing selected yet". */
.ub-panel__wait { margin: 0; color: var(--text-3); font-size: .875rem; }

/*  ==========================================================================
    ⚠ THE HEADING SAT ON THE RIGHT-HAND EDGE. HERE IS WHY. (2026-08-05)

    .ub-panel__h carries `justify-content: space-between` — correct when the
    heading was alone with a badge on the far right. .ub-disc then adds the
    arrow as a ::before, so the row has TWO flex children: arrow and heading.
    space-between does exactly what it says and throws them to opposite ends —
    arrow hard left, heading hard right.

    Reported three times as "transaction history is offset" and each time I
    adjusted padding, which was never the cause. The cause is one property.

    flex-start on any .ub-panel__h that is also a .ub-disc, and anything that
    genuinely belongs on the right (the chart's meter badge) is pushed there
    with margin-left:auto instead.
    ========================================================================== */
.ub-panel__h.ub-disc { justify-content: flex-start; }
.ub-panel__h.ub-disc .ub-chart__meter { margin-left: auto; }

/*  ==========================================================================
    A DOT PER CARD, FROM THE MARK'S OWN PALETTE

    Every heading dot was the same purple. The logo is six dots in orange,
    amber and red beside a purple U, so the page had one brand colour where
    the mark has four. Each card now takes its own, in the order they appear
    on the mark, which also makes the cards tellable apart at a glance when
    they are all collapsed.
    ========================================================================== */
.ub-rewardcard          { --dot: var(--dot-1, #F97316); }  /* orange */
.ub-prof.ub-panel       { --dot: var(--dot-2, #FBBF24); }  /* amber  */
.ub-panel--chart        { --dot: var(--dot-3, #A78BFA); }  /* purple */
.ub-panel--history      { --dot: var(--dot-4, #EF4444); }  /* red    */

/*  ⚠ NOT SCOPED TO .ub-dash. The dot rule that existed only fired inside the
    dashboard, so the account page headings had no dot of their own at all and
    were relying on a span I had added by hand. */
.ub-panel__h h2::before,
.ub-panel__h .ub-disc__t > h2::before {
    content: ''; display: inline-block; vertical-align: middle;
    width: .5rem; height: .5rem; border-radius: 50%;
    margin-right: .5rem; background: var(--dot, var(--brand));
}

/*  ==========================================================================
    ONE CARD HEADING. See Pages/Shared/_CardHead.cshtml.

    ⚠ EVERY RULE ABOVE THAT TRIED TO ALIGN THESE HEADINGS IS SUPERSEDED. Four
    cards each had their own markup and their own padding, so aligning them
    meant keeping four numbers equal by hand — and four times running, moving
    one broke another. There is one heading now and one set of numbers.

    Grid, not flex, and deliberately: `grid-template-columns: auto auto 1fr`
    pins the arrow and the dot to fixed tracks, so the TITLES ALL START ON THE
    SAME PIXEL however long or short the text beside them is. With flex the
    start position drifts with content, which is what "nothing is lined up"
    looked like.
    ========================================================================== */
.ub-cardh {
    display: grid;
    grid-template-columns: auto auto 1fr;
    align-items: start;
    gap: 0 .5rem;
    cursor: pointer;
    list-style: none;
    padding: 0;
    margin: 0;
}
.ub-cardh::-webkit-details-marker { display: none; }

/*  The arrow. Column 1, fixed width, rotates rather than swapping glyph. */
.ub-cardh::before {
    content: '▸';
    grid-column: 1;
    /*  ⚠ BIG ENOUGH TO READ AS A CONTROL. It shipped at .8125rem and looked
        like punctuation beside a 19px heading — a speck on a phone. It is the
        only thing on the row saying the card opens, so it is sized like it
        matters, and its line-height is tuned to sit on the heading's cap
        height rather than float in the middle of the line box. */
    width: 1.125rem;
    color: var(--text-2);
    font-size: 1.125rem;
    line-height: 1.15;
    transition: transform .15s ease;
}
details[open] > .ub-cardh::before { transform: rotate(90deg); }
.ub-cardh:hover::before { color: var(--text); }

/*  The dot. Column 2, its colour chosen by data-dot so the palette stays in
    CSS and the markup only ever names one. */
.ub-cardh::after {
    content: '';
    grid-column: 2;
    grid-row: 1;
    width: .5rem; height: .5rem;
    margin-top: .5rem;
    border-radius: 50%;
    background: var(--dot, var(--brand));
}
.ub-cardh[data-dot="rewards"] { --dot: #F97316; }
.ub-cardh[data-dot="details"] { --dot: #FBBF24; }
.ub-cardh[data-dot="chart"]   { --dot: #A78BFA; }
.ub-cardh[data-dot="history"] { --dot: #EF4444; }

/*  Title and caption. Column 3, so they share one left edge across all four
    cards no matter what is in columns 1 and 2. */
.ub-cardh__t { grid-column: 3; min-width: 0; }
.ub-cardh__h {
    margin: 0;
    font-size: 1.1875rem;
    line-height: 1.3;
    font-weight: 650;
    display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
}
.ub-cardh__n {
    display: block;
    margin-top: .1875rem;
    font-size: .8125rem;
    font-weight: 400;
    color: var(--text-3);
    line-height: 1.5;
}
/*  Shown only while shut — it exists to help somebody decide to open the
    card, not to sit above the contents once they have. */
details[open] > .ub-cardh .ub-cardh__n { display: none; }

.ub-cardh__tag {
    font-size: .625rem; font-weight: 700; letter-spacing: .08em;
    text-transform: uppercase; color: var(--spark);
    border: 1px solid currentColor; border-radius: 999px;
    padding: .125rem .4375rem;
}

/*  Anything on the right is pushed there HERE, never with space-between on
    the row — that is what threw the headings to the right-hand edge. */
.ub-cardh > :not(.ub-cardh__t) { grid-column: 3; justify-self: end; align-self: center; }

details[open] > .ub-cardh { margin-bottom: 1.25rem; }

/*  ==========================================================================
    THE FOUR ACCOUNT CARDS, AS APPROVED FROM THE MOCKUP (2026-08-05)

    Rewards, Details, Purchases, Transaction history. Same card, same heading,
    same three columns, all shut on arrival.

    ⚠ THIS BLOCK IS LAST IN THE FILE ON PURPOSE. Earlier rules for .ub-prof,
    .ub-ownerbox and .ub-panel__h each set their own padding and their own
    marker, and they are what pulled the four headings out of line four times
    running. Rather than hunt every one of them down, the settled geometry is
    stated once, here, where it wins.
    ========================================================================== */
.ub-panel.ub-rewardcard,
.ub-panel.ub-prof,
.ub-panel--chart,
.ub-panel--history {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    padding: 1.125rem 1.25rem;
    margin-bottom: .875rem;
}

/*  The heading: arrow track, dot track, then everything else. A FIXED first
    two columns is what makes every title start on the same pixel — with flex
    the start drifts with the content beside it, which is what "nothing is
    lined up" was. */
.ub-cardh {
    display: grid;
    grid-template-columns: 1.5rem .75rem 1fr;
    align-items: start;
    column-gap: .625rem;
    padding: 0;
    margin: 0;
}
.ub-cardh::before {
    grid-column: 1; grid-row: 1;
    width: auto; font-size: 1rem; line-height: 1.45;
}
.ub-cardh::after {
    grid-column: 2; grid-row: 1;
    margin-top: .5rem;
}
.ub-cardh__t { grid-column: 3; grid-row: 1; }
.ub-cardh__h { font-size: 1.125rem; }

/*  The body is separated by a rule rather than by margin, so an open card
    reads as one object instead of two stacked ones. */
details.ub-panel[open] > .ub-cardh { margin-bottom: 0; }
details.ub-panel[open] > .ub-cardh ~ * { margin-top: 0; }
details.ub-panel[open] > .ub-cardh + * {
    margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--line);
}

/*  ==========================================================================
    THE ACCOUNT CARDS, LAID OUT FOR A BROWSER (2026-08-06)

    ⚠ DESIGNED AT DESKTOP FIRST, ON PURPOSE, AND THIS IS THE CORRECTION.
    The four cards were built and checked on a phone, where one column is the
    only sensible answer. Carried to a browser unchanged, that gave four
    full-width cards each ONE LINE TALL — "Rewards" and a sentence, then a
    metre of empty violet. It looked unfinished, because it was: the layout
    had never been designed for the width it was being shown at.

    Two columns for the SHORT cards, full width for the ones with real content:

        Rewards          Details          <- a few rows each, side by side
        Purchases                         <- holds a chart
        Transaction history               <- holds a list

    Below 900px it collapses back to one column, which is where the phone
    layout was right all along.
    ========================================================================== */
/*  ⚠ ONE COLUMN, ALWAYS. STACKED, NOT SIDE BY SIDE.

    A two column arrangement was tried here and was wrong: it put Details
    beside Rewards, which reads as two unrelated things competing for the eye
    rather than one list to work down. The cards are a SEQUENCE - rewards,
    then your details, then what you bought, then every transaction - and a
    sequence belongs in a column.

    So .ub-dash__main stays the flex column it already is, at every width. The
    fix for the empty space is tighter cards, below, not more columns. */

/*  A collapsed card should be compact. Open, it needs room to breathe — but
    shut it is a heading and a sentence, and 1.125rem of padding around that
    on a 770px card is what read as "a big space". */
details.ub-panel:not([open]) { padding-top: .875rem; padding-bottom: .875rem; }

/* ==========================================================================
   Error and status pages (2026-08-07)

   The error page uses `wrap`, which is the full page measure. Three buttons
   stretched across 1400px with a sentence floating above them read as an
   unfinished page — which is a poor thing to show somebody at the exact
   moment something has already gone wrong for them.

   Held to a reading measure and centred, so it looks composed. The buttons
   size to their own text rather than sharing the row equally, because "Buy
   electricity" is the one people want and it should not be the same size as
   the two beside it by accident.
   ========================================================================== */
.ub-errpage {
    max-width: 40rem;
    margin-inline: auto;
}
.ub-errpage .ub-btnrow {
    flex-wrap: wrap;
}
.ub-errpage .ub-btnrow > .ub-btn {
    flex: 0 0 auto;
}

/* ==========================================================================
   Long-form pages: terms, privacy, support (2026-08-07, extended 2026-08-13)

   ⚠ .ub-prose WAS NEVER DEFINED. Privacy.cshtml and Terms.cshtml both wrapped
   their content in it and it styled nothing at all, so the two documents a
   customer reads before deciding whether to trust us with a payment were
   full-page-width text with headings jammed against paragraphs. Legible in
   the sense that the characters were on screen, and not readable in any
   sense that matters.

   A reading measure, and room between things. 38rem -> 60rem -> 46rem,
   2026-08-12: 38rem read as cramped once the rest of the site widened, 60rem
   overcorrected ("it's way too wide, way too wide"). 46rem is roughly 90
   characters at this size — still a proper reading measure, not the full
   page.

   ⚠ NOW A VISIBLE CARD, NOT JUST A NARROWED COLUMN. "It needs to be in a
   card, you know, like the card of the buy meter, but a bigger card for this
   text" — same surface/border/shadow language as .ub-buy, so the two
   documents a customer reads before trusting us with a payment look like
   they belong to the same site instead of floating text on bare background.

   ⚠ 2026-08-13: THE THIRD PAGE JOINED, NOT A LOOKALIKE OF IT. "I love the way
   you've done that card. Every other card, Privacy, Support should resemble
   this... laid out exactly the same way." Privacy and Support used to sit in
   a separate .ub-panel.ub-legal box (flat, full-width, no shadow) that had
   drifted from this one. Both now use .ub-prose itself for their outer
   shell, so a change made here reaches all three instead of three copies
   slowly disagreeing. h3/ul/li below exist for Privacy's lists and
   short-version box, which Terms's own content never needed — the .ub-feats
   /.ub-facts/.ub-steps cards Support nests inside this shell (action tiles,
   the opening-hours dl, the FAQ grid) already carry their own styling from
   elsewhere in this file and are untouched. */
.ub-prose {
    max-width: 46rem;
    margin-inline: auto;
    /*  bottom margin 3rem -> 4rem — "leave some space between the... they're
        too close together at the bottom": the card's own footer edge and
        the site footer directly below it read as touching. */
    margin-block: 1.5rem 4rem;
    padding: clamp(1.75rem, 4vw, 2.75rem) clamp(1.5rem, 4vw, 2.5rem) clamp(2.5rem, 6vw, 3.5rem);
    background: linear-gradient(168deg, var(--surface) 0%, var(--ink-2) 100%);
    border: 1px solid var(--line-hi);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow);
}

/*  ⚠ THE TITLE MUST NEVER WRAP (2026-08-12) - reported live, twice tonight,
    once on an admin field and now here: a heading breaking onto a second
    line reads as broken, not as "still legible". clamp() shrinks the size
    smoothly as the viewport narrows instead of letting it wrap - there is
    no width at which "Terms and conditions" or "Privacy policy" needs two
    lines. */
.ub-prose h1 {
    margin-bottom: .25rem;
    font-size: clamp(1.5rem, 5vw, 2rem);
    white-space: nowrap;
}

/*  Headings need space ABOVE them, far more than below: the gap is what tells
    a reader a new section has started, and a heading sitting equidistant
    between two paragraphs belongs to neither. */
.ub-prose h2 {
    margin-top: 2.25rem;
    margin-bottom: .5rem;
    font-size: 1.125rem;
}

.ub-prose p {
    margin: 0 0 .875rem;
    line-height: 1.65;
    color: var(--text-2);
}

.ub-prose strong { color: var(--text); }

/*  The TO CONFIRM markers are set in <em>. They are notes to us, not to the
    reader, so they are dimmed rather than emphasised — and they should look
    conspicuously unfinished for as long as they survive. */
.ub-prose em { color: var(--text-3); font-style: italic; }

.ub-prose a { color: var(--brand); text-underline-offset: .2em; }

/*  Sub-headings and lists — Privacy's "short version" box and its bulleted
    information lists use these; Terms has neither, so this never mattered
    until Privacy moved into the same shell (2026-08-13). Values carried over
    unchanged from the old .ub-legal component so nothing on Privacy actually
    moved or resized, only the card it sits inside did.

    ⚠ h3 IS SCOPED TO A DIRECT CHILD, THE OTHER TWO ARE NOT. Support also
    sits in .ub-prose now, and its FAQ/action cards (.ub-step h3, .ub-feat
    h3) carry their own, larger h3 size a few levels further down inside
    .ub-steps/.ub-feats. A plain ".ub-prose h3" is a same-specificity
    descendant match for those too and — appearing later in the file — would
    win the tie and shrink every FAQ and contact-card heading down to
    Privacy's sub-heading size. Every h3 Privacy actually uses (see
    Privacy.cshtml) sits directly inside the .ub-prose wrapper with nothing
    between them, so "> h3" still reaches all of them and none of Support's.
    ul/li have no such collision — Support's content uses dl and p, never a
    bare list — so they are left as ordinary descendant selectors, which is
    what lets this reach the nested <ul> inside .ub-legal__tldr too. */
.ub-prose > h3 {
    font-size: .9375rem;
    font-weight: 650;
    margin: 1.25rem 0 .5rem;
    color: var(--text);
}
.ub-prose ul { padding-left: 1.125rem; margin: 0 0 .875rem; }
.ub-prose li { margin-bottom: .375rem; line-height: 1.65; color: var(--text-2); }

/*  The draft banner and anything else in a warnbox keeps its own width. */
.ub-prose .ub-warnbox { margin-block: 1.25rem; }

/*  ---- FAQ accordions (/faq, 2026-08-16) ----

    Native <details> so they cost no script - see the header comment in
    Faq.cshtml. Card treatment from the same tokens every other card uses;
    the chevron is the site's one arrow (--ub-chevron, same artwork as
    .ub-sel and the combo fields), rotated when open, replacing the
    browser's triangle marker so the two kinds of disclosure cannot drift
    into different arrows. */
.ub-faq {
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: 12px;
    margin-bottom: .625rem;
}
.ub-faq > summary {
    cursor: pointer;
    list-style: none;             /* the default triangle, standards path  */
    padding: .875rem 2.75rem .875rem 1.125rem;
    font-weight: 600;
    color: var(--text);
    background-image: var(--ub-chevron);
    background-repeat: no-repeat;
    background-position: right 1rem center;
    background-size: 15px;
    transition: none;
}
.ub-faq > summary::-webkit-details-marker { display: none; }  /* Safari  */
.ub-faq[open] > summary { border-bottom: 1px solid var(--line); }
.ub-faq > p {
    padding: .875rem 1.125rem;
    margin: 0;
    color: var(--text-2);
    line-height: 1.65;
}

/* ==========================================================================
   "How you buy" — insight from a customer's own history (2026-08-07)

   Sits under the summary cards and answers what the totals cannot. Styled as
   a quiet block rather than more cards on purpose: these are sentences, and
   four more tiles beside four tiles would read as eight equal facts when
   these are commentary on the ones above.
   ========================================================================== */
.ub-insight {
    margin-top: 1.25rem;
    padding: 1rem 1.125rem;
    border: 1px solid var(--line);
    border-radius: .75rem;
    background: var(--surface-2);
}
.ub-insight__h {
    margin: 0 0 .5rem;
    font-size: .8125rem;
    font-weight: 700;
    letter-spacing: .02em;
    text-transform: uppercase;
    color: var(--text-3);
}
.ub-insight__l {
    margin: 0;
    padding-left: 1.1rem;
    display: grid;
    gap: .375rem;
    font-size: .9375rem;
    line-height: 1.55;
    color: var(--text-2);
}
.ub-insight__l strong { color: var(--text); }

/*  The overdue line is the only one that asks for anything, so it is the only
    one that carries the accent. */
.ub-insight__due { color: var(--brand); font-weight: 600; }

/* ==========================================================================
   "Create an account" offer, after a guest purchase (2026-08-07)

   Sits under the token, never above it. A guest has just paid and the token is
   the thing they came for; anything competing with it is an obstacle.

   Quiet by design. This is an offer, not an interruption, and it must not read
   like a step still to be completed - somebody who has their token is finished
   and should feel finished.
   ========================================================================== */
.ub-signupoffer {
    display: block;
    margin-top: 1.25rem;
    padding: .875rem 1rem;
    border: 1px solid var(--line);
    border-radius: .75rem;
    background: var(--surface-2);
    font-size: .875rem;
    line-height: 1.55;
    color: var(--text-2);
}
.ub-signupoffer b { display: block; color: var(--text); margin-bottom: .125rem; }
.ub-signupoffer strong { color: var(--text); word-break: break-all; }
.ub-signupoffer .ub-btn { display: inline-flex; }

/*  ---- THE CROSS-SELL BOX ----

    Same card as the account offer above it, deliberately: this is one more
    quiet aside after the purchase, not a promotion, and it must not out-shout
    the token sitting above it. Ghost buttons for the same reason - the
    primary action on this screen is "copy your token", and nothing here
    competes with it.

    Both themes come from the same tokens the offer box uses, so it cannot
    render a dark card on a light ground. See _CrossSell.cshtml for what it
    says and why. */
.ub-xsell {
    display: block;
    margin-top: 1.25rem;
    padding: .875rem 1rem;
    border: 1px solid var(--line);
    border-radius: .75rem;
    background: var(--surface-2);
    font-size: .875rem;
    line-height: 1.55;
    color: var(--text-2);
}
.ub-xsell b { display: block; color: var(--text); margin-bottom: .125rem; }

/*  Two buttons that share one width, the rule every stacked control on this
    site follows - never one wide button beside a narrow one. They sit side by
    side where there is room and stack below 26rem, which is where two
    full-width buttons stop fitting on one line. */
.ub-xsell__row {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
    margin-top: .75rem;
}
.ub-xsell__row .ub-btn {
    /*  Hug the label, same as every other button - see the note on .ub-btn.
        These were flex:1 1 9rem, which split the row evenly between "Buy
        airtime" and "Buy data" and stretched both. */
    flex: 0 0 auto;
    width: auto;
    max-width: 100%;
    min-height: 42px;
    display: inline-flex;
}

/* ==========================================================================
   Cookie and tracking consent (Phase 6C)
   ========================================================================== */
.ub-cookie {
    position: fixed;
    z-index: 220;
    /*  Centred floating dialog on every device — the 50/50 transform
        below is what centres it, and ubox-consent.js's device classes only
        vary density (columns / stacked buttons), never position. */
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: min(45rem, calc(100vw - 1.75rem));
    max-height: 85vh;
    overflow: auto;
    padding: 1.5rem 1.75rem;
    /*  ⚠ OPAQUE, AND WITH A SCRIM. It was 92% of --bg with a 14px
        backdrop blur, which on the real /power page put the DisCo logo
        tiles and the buy card straight through the middle of the consent
        text - the paragraph about analytics cookies was sitting on smeared
        white rectangles and could not be read. A blur is not a background.

        The second shadow is the scrim: a 100vmax spread paints the whole
        viewport behind the panel without needing a second element in three
        separate layouts. It dims the page, gives the dialog an edge, and
        makes it obvious that nothing else is live until a choice is made. */
    background: var(--surface);
    border: 1px solid var(--line-hi);
    border-radius: 1rem;
    box-shadow: 0 0 0 100vmax rgba(8, 5, 20, .68),
                0 24px 64px rgba(16, 13, 34, .55);
}
.ub-cookie[hidden] { display: none; }
/*  ---- THE BOX IS CENTRED, AND SO IS WHAT IS IN IT ----

    Reported 2026-08-28: "everything's left justified. It should be at least
    in the middle of the box. And the buttons should either be stacked, or
    they should be all lined up horizontally."

    Both were real, and the first was not a text-align problem - the headings
    and paragraphs already carried text-align:center. The INNER GRID was
    "minmax(0, 1.2fr) minmax(280px, .8fr)", two columns, and the second column
    holds .ub-cookie__choices, which is display:none until Manage choices is
    pressed. So on the view everybody actually sees, one visible child sat in
    a 60%-wide left column with an empty 40% column beside it: text centred
    within its column, and the whole block hugging the left of a 720px panel.

    One column, always, with the contents centred in it. The choices list
    appears UNDER the copy when it is asked for rather than beside it, which
    is also the only arrangement that reads the same on a phone and a desktop.

    The buttons are the second half of the ask. They were flex-wrap: wrap, so
    the three-button state fitted on one line and the four-button state (once
    Manage choices adds Save) broke to three-plus-one - neither a row nor a
    stack. They are now exactly one of the two: equal-width in a single
    non-wrapping row where there is room, and a full-width vertical stack on
    phone and tablet. Never a ragged wrap.

    ⚠ THIS BLOCK IS DUPLICATED IN ubox-consent.css, WHICH _UBoxLayout2.cshtml
    LOADS INSTEAD OF THIS FILE. The two carry different palettes on purpose
    (this one themes off --bg/--line, that one is standalone light) but the
    LAYOUT rules must stay identical. Change one, change the other. */
.ub-cookie__inner {
    /*  ONE COLUMN. See the note above - the old two-column grid is what put
        every visible word in the left 60% of a centred box. */
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    gap: 1rem;
    align-items: start;
    text-align: center;
}
/*  ⚠ NO max-width HERE. It was capped at 46ch while the buttons under it
    ran the full width of the panel, so the sentence block was a narrow
    centred column floating above a wide row - which is exactly what it
    looked like. Everything in this stack shares one width, the same rule the
    buy cards follow. */
.ub-cookie__copy { width: 100%; }
.ub-cookie h2 {
    margin: 0 0 .375rem;
    font-size: 1.125rem;
    line-height: 1.2;
    letter-spacing: 0;
    text-align: center;
}
.ub-cookie p {
    margin: .25rem 0;
    color: var(--text-2);
    font-size: .9375rem;
    line-height: 1.55;
    text-align: center;
}
.ub-cookie__controller {
    font-size: .8125rem !important;
    color: var(--text-3) !important;
    text-align: center;
}
.ub-cookie__choices {
    display: none;
    gap: .5rem;
    /*  Under the copy, not beside it, and never wider than a comfortable
        reading measure - see the note at the top of this block. */
    width: 100%;
    text-align: left;
}
.ub-cookie.is-managing .ub-cookie__choices { display: grid; }
.ub-cookie__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: .625rem .75rem;
    border: 1px solid var(--line);
    border-radius: .5rem;
    background: var(--surface);
}
.ub-cookie__row b {
    display: block;
    font-size: .875rem;
    color: var(--text);
}
.ub-cookie__row span span {
    display: block;
    color: var(--text-2);
    font-size: .8125rem;
    line-height: 1.35;
}
.ub-cookie__row input {
    width: 1.25rem;
    height: 1.25rem;
    flex: 0 0 auto;
}
.ub-cookie__always {
    font-size: .8125rem;
    color: var(--text-3);
    white-space: nowrap;
}
.ub-cookie__actions {
    grid-column: 1 / -1;
    display: flex;
    /*  ⚠ WAS nowrap WITH EQUAL WIDTHS. Both are gone: the buttons hug their
        labels now (see .ub-btn), and a row of hugged buttons that cannot
        wrap will overflow its panel the moment a fourth one appears. Wrapping
        and centring keeps them a tidy row where there is room and a tidy
        centred block where there is not. */
    flex-wrap: wrap;
    justify-content: center;
    gap: .625rem;
    width: 100%;
    /*  A hairline above the buttons, the way a dialog footer is separated
        from its body everywhere else. Without it the row read as one more
        line of the paragraph above. */
    padding-top: .75rem;
    border-top: 1px solid var(--line);
}
.ub-cookie__actions .ub-btn {
    /*  Natural width, never squeezed to an equal share: "Reject
        non-essential" is twice the label "Accept all" is, and forcing them
        to the same width wraps the long one onto two lines inside its own
        button. They are centred as a group instead. */
    flex: 0 1 auto;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-align: center;
}
/*  ⚠ SPECIFICITY, NOT ORDER. The hide rule used to be plain
    ".ub-cookie__save" - one class - while the rule above that sizes the
    buttons is ".ub-cookie__actions .ub-btn", which is two. Its
    display:inline-flex therefore beat the display:none, and Save choices was
    on screen from the moment the banner appeared, beside Manage choices,
    offering to save a set of choices nobody had been shown yet. Caught by
    measuring the rendered buttons on .47, not by reading this file. Both
    selectors below now carry more weight than the sizing rule. */
.ub-cookie__actions .ub-btn.ub-cookie__save { display: none; }
.ub-cookie.is-managing .ub-cookie__actions .ub-btn.ub-cookie__save { display: inline-flex; }

/*  ⚠ .ub-btn--primary IS NOT A CLASS THIS STYLESHEET DEFINES. The site's
    primary button is .ub-btn--go; the consent markup was written against
    ubox-consent.css, which carries its own --primary rule, and _UBoxV2.cshtml
    and _UBoxAdmin.cshtml load THIS file instead. So "Accept all" and "Save
    choices" rendered as bare bordered buttons, indistinguishable from "Reject
    non-essential" beside them - the recommended action did not read as the
    recommended action anywhere a customer would actually see it.

    Deliberately NOT aliased to .ub-btn--go: that is the amber-gradient
    Confirm-and-pay button, and a cookie banner must not wear the same clothes
    as the control that spends somebody's money. Brand purple, flat. */
.ub-cookie .ub-btn--primary {
    background: var(--brand);
    border-color: var(--brand);
    color: #FFFFFF;
}
.ub-cookie .ub-btn--primary:hover:not(:disabled) {
    background: var(--brand-hi);
    border-color: var(--brand-hi);
}

/*  ---- DYNAMIC LAYOUT BY DEVICE ----
    ubox-consent.js stamps one of ub-cookie--phone / --tablet / --desktop on
    the panel (re-evaluated live on resize and rotation).

    The BOX ITSELF is a centred floating dialog on every device - position is
    fixed with its own 50/50 transform on the base rule above, and nothing
    here moves it. What differs per device is density:

      phone   single column, whole-width stacked buttons
      tablet  single column, buttons stacked the same way as a phone
      desktop two columns, buttons inline

    The buttons on phone and tablet stack FULL WIDTH. `.ub-btn` is capped at
    max-width: 34ch, so a plain flex row leaves them narrow and left-aligned;
    the override drops the cap and lets the stack stretch. */
/*  The inner grid is one column on EVERY device now (see the note at the
    top of this block), so there is no per-device grid rule left to write.
    What still differs by device is the buttons: a row where there is room,
    a full-width stack where there is not. */

/*  On a phone the choice rows were description-left, switch-right, which
    squeezed "Allows advertising measurement tags such as Google Ads, Meta,
    TikTok, Microsoft or Snap when configured" into about two thirds of a
    narrow screen while a 20px checkbox held the rest. The control goes under
    its own label instead, at the trailing edge where a thumb reaches it. */
.ub-cookie--phone .ub-cookie__row {
    flex-direction: column;
    align-items: stretch;
    gap: .5rem;
}
.ub-cookie--phone .ub-cookie__row input { align-self: flex-end; }
.ub-cookie--phone .ub-cookie__always { align-self: flex-end; }

/*  ⚠ NO LONGER A FULL-WIDTH STACK ON TOUCH. The three buttons were
    stretched edge to edge on phone and tablet, which is the same "longer
    than the words in it" problem as everywhere else. They hug their labels
    and wrap onto the next line when the row runs out - so on a phone they
    still end up stacked, but each one is the size of what it says. */
.ub-cookie--phone .ub-cookie__actions,
.ub-cookie--tablet .ub-cookie__actions {
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
}
.ub-cookie--phone .ub-cookie__actions .ub-btn,
.ub-cookie--tablet .ub-cookie__actions .ub-btn {
    flex: 0 0 auto;
    width: auto;
    max-width: 100%;
}


