/* The licensee portal. Same palette as the admin dashboard (dashboard.css), kept in its own file
   so a dashboard style change can never reshape a page customers see. */

* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
    color-scheme: dark;
    --bg: #0f1117;
    --surface: #1a1d27;
    --surface2: #242836;
    --border: #2e3345;
    --text: #e1e4ed;
    --text-dim: #8b8fa3;
    --accent: #cc0000;
    --accent-hover: #e60000;
    --discord: #5865f2;
    --discord-hover: #4752c4;
    --red: #ef4444;
    --yellow: #eab308;
    --green: #4ade80;
    --blue: #60a5fa;
    --radius: 10px;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: var(--bg);
    color: var(--text);
    min-height: 100vh;
    line-height: 1.45;
}

[hidden] { display: none !important; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.mono { font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace; }

/* ── Top bar ── */
.topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface);
    position: sticky;
    top: 0;
    z-index: 10;
}

/* The brand and Log out never wrap; the account name is what gives way on a narrow screen. */
.brand {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    font-weight: 700;
    letter-spacing: 0.01em;
    white-space: nowrap;
    flex: none;
    color: var(--text);
    text-decoration: none;
}
/* The mark is a full square tile with its own dark ground, so it is rounded like an app icon
   rather than floated as a bare glyph on the bar. */
.brand-logo { width: 32px; height: 32px; border-radius: 7px; flex: none; }

.account { display: flex; align-items: center; justify-content: flex-end; gap: 0.5rem; min-width: 0; }
.account .btn { flex: none; white-space: nowrap; }
.account-name {
    color: var(--text-dim);
    font-size: 0.9rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ── Page nav ── */
/* Its own row under the bar rather than squeezed between the brand and Log out, so it never
   costs the bar its no-wrap guarantee on a small phone. */
.portal-nav {
    display: flex;
    justify-content: center;
    gap: 0.25rem;
    padding: 0 1rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface);
}
.portal-nav-link {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 1rem;
    color: var(--text-dim);
    text-decoration: none;
    font-weight: 600;
    font-size: 0.95rem;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
}
.portal-nav-link:hover { color: var(--text); }
.portal-nav-link[aria-current="page"] { color: var(--text); border-bottom-color: var(--accent); }

/* ── Layout ── */
.page { max-width: 820px; margin: 0 auto; padding: 1.5rem 1rem 3rem; }

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.center-card {
    max-width: 440px;
    margin: 3rem auto 0;
    padding: 2rem 1.5rem;
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
}

.hero-logo { width: 88px; height: 88px; border-radius: 20px; }

h1 { font-size: 1.5rem; line-height: 1.2; }
.lede { color: var(--text-dim); }
.hint { color: var(--text-dim); font-size: 0.9rem; }

/* ── Controls ── */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: 0.6rem 1.1rem;
    border: none;
    border-radius: 8px;
    font: inherit;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    color: #fff;
}
.btn:disabled { opacity: 0.6; cursor: default; }
.btn-primary { background: var(--accent); }
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); }
.btn-secondary { background: var(--surface2); border: 1px solid var(--border); color: var(--text); }
.btn-secondary:hover:not(:disabled) { border-color: var(--text-dim); }
.btn-discord { background: var(--discord); width: 100%; }
.btn-discord:hover { background: var(--discord-hover); }
.btn-ghost {
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--border);
    min-height: 40px;
    padding: 0.35rem 0.8rem;
    font-weight: 500;
    font-size: 0.85rem;
}
.btn-ghost:hover { color: var(--text); border-color: var(--text-dim); }

#link-form { width: 100%; display: flex; flex-direction: column; gap: 0.6rem; text-align: left; }
#link-form .btn { width: 100%; margin-top: 0.4rem; }

.field-label { font-size: 0.85rem; color: var(--text-dim); }
.field {
    width: 100%;
    min-height: 44px;
    padding: 0.6rem 0.75rem;
    background: var(--surface2);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    /* 16px keeps iOS from zooming the page when the field takes focus. */
    font: 16px 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
}
.field:focus { border-color: var(--accent); outline: none; }
/* 16px here too: iOS zooms into any focused form control smaller than that, select included. */
.field-select { width: auto; min-height: 44px; font-family: inherit; font-size: 16px; padding: 0.4rem 0.6rem; }

.notice {
    padding: 0.65rem 0.8rem;
    border-radius: 8px;
    font-size: 0.9rem;
    text-align: left;
}
.notice-error {
    background: color-mix(in srgb, var(--red) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--red) 40%, transparent);
    color: #fca5a5;
}
.page > .notice { max-width: 440px; margin: 0 auto 1rem; }

/* ── Checkouts ── */
/* The view's own switches. They sit above the day cards rather than inside the toolbar so the
   range select keeps the right-hand side to itself on a phone, and they wrap onto two lines
   there rather than squeezing either label. */
.toggles {
    display: flex;
    flex-wrap: wrap;
    gap: 0 1.25rem;
    margin-bottom: 0.25rem;
}
.toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    /* A tap target. */
    min-height: 44px;
    color: var(--text-dim);
    font-size: 0.9rem;
    cursor: pointer;
}
.toggle input { width: 1.05rem; height: 1.05rem; accent-color: var(--accent); }
.toggle:hover { color: var(--text); }
.toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-bottom: 1rem;
}
.toolbar-right { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
/* A page heading with at most one quiet control beside it, kept on ONE line at every width —
   unlike .toolbar, whose controls take a row of their own on a phone. */
.titlebar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
}
.titlebar .field-select { flex: none; }

/* One strip of three rather than three tiles: on a phone the tiles took two rows, with Spent
   alone on the second. Spent gets the widest cell, since it is the longest figure. */
.stats {
    display: grid;
    grid-template-columns: 1fr 1fr 1.35fr;
    margin-bottom: 0.5rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
}
.stat {
    padding: 0.7rem 0.9rem;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 0;
}
.stat + .stat { border-left: 1px solid var(--border); }
.stat-value {
    font-size: clamp(1.1rem, 4.5vw, 1.5rem);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.stat-label { color: var(--text-dim); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.06em; }

.day { margin-bottom: 0.75rem; overflow: hidden; border-radius: 12px; }
/* The card's own surface, not a band — the same header as an Orders card. */
.day-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.25rem 1rem;
    padding: 0.75rem 1rem 0.65rem;
    border-bottom: 1px solid var(--border);
}
.day-title { font-size: 1rem; font-weight: 700; }
.day-meta { color: var(--text-dim); font-size: 0.85rem; font-variant-numeric: tabular-nums; }

.order { padding: 0.75rem 1rem 0.85rem; }
/* The combined-by-item card's single body. NOT `.order`: that class means one checkout on this
   page, and the counts taken off it would go wrong. */
.combined { padding: 0.75rem 1rem; }
.order + .order { border-top: 1px solid var(--border); }
.order-head {
    display: flex;
    justify-content: space-between;
    font-size: 0.85rem;
    color: var(--text-dim);
    margin-bottom: 0.5rem;
    font-variant-numeric: tabular-nums;
}
.order-total { color: var(--text); font-weight: 600; }

.items { display: flex; flex-direction: column; gap: 0.6rem; }
.item { display: flex; gap: 0.75rem; align-items: center; min-width: 0; }
.item-stray { opacity: 0.6; }
/* A stray was bought, so it stays listed, but small: one line of title under a smaller tile, so
   the item the checkout was FOR stays the thing you notice. */
.item-stray .thumb { width: 36px; height: 36px; margin-inline: 8px; }
.item-stray .item-title { -webkit-line-clamp: 1; font-size: 0.85rem; }
.item-empty { color: var(--text-dim); font-size: 0.85rem; }

.thumb {
    flex: none;
    width: 52px;
    height: 52px;
    border-radius: 6px;
    background: var(--surface2);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}
/* Product shots are cut out on white; the tile only turns white once one actually loads. */
.thumb:has(img) { background: #fff; }
.thumb img { width: 100%; height: 100%; object-fit: contain; }

.item-body { min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
.item-title {
    color: var(--text);
    text-decoration: none;
    font-size: 0.92rem;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.item-title:hover { text-decoration: underline; }
.item-meta { color: var(--text-dim); font-size: 0.8rem; }

.badge {
    display: inline-block;
    padding: 0 0.4rem;
    border-radius: 999px;
    font-size: 0.7rem;
    border: 1px solid color-mix(in srgb, var(--yellow) 50%, transparent);
    color: var(--yellow);
    vertical-align: 0.05em;
}

.empty { color: var(--text-dim); text-align: center; padding: 2.5rem 1rem; }

.spinner {
    width: 28px;
    height: 28px;
    border: 3px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}
#days .center-card { margin-top: 1.5rem; background: none; border: none; }

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

/* ── Orders ── */
/* A quiet text button: it is a place you go to undo something, not a control the page is read by. */
.orders-dismissed {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-height: 32px;
    border: none;
    background: none;
    font: inherit;
    font-size: 0.85rem;
    color: var(--text-dim);
    cursor: pointer;
}
.orders-dismissed:hover { color: var(--text); }
.orders-dismissed-count {
    padding: 0 0.45rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface2);
    font-size: 0.75rem;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}
/* The two dropdowns share one row evenly on a phone; a hidden account picker leaves the range
   the whole row. */
.orders-filters { display: flex; flex-wrap: wrap; gap: 0.5rem; }

.status-row { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.75rem; }
.status-chips { display: contents; }
.status-chip {
    display: inline-flex;
    align-items: center;
    flex: none;
    /* A tap target on a phone, without making the row of chips look like a row of buttons. */
    min-height: 32px;
    padding: 0.1rem 0.7rem;
    border-radius: 999px;
    font: inherit;
    font-size: 0.78rem;
    font-weight: 600;
    white-space: nowrap;
    border: 1px solid var(--border);
    color: var(--text);
    background: var(--surface2);
    cursor: pointer;
}
/* Not picked: still readable, and still counting, so it reads as a filter rather than as
   something that failed to load. The picked chips carry the ring below, so the row says which
   way round it is from BOTH sides — dimming alone is what reads as "failed to load". */
.status-chip[aria-pressed="false"] {
    opacity: 0.45;
}
/* Only while a filter is actually running: with every chip picked the row is the unfiltered page,
   and ringing all of them would announce a filter nobody set. */
.status-chips[data-filtering="true"] .status-chip[aria-pressed="true"] {
    box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 40%, transparent);
}
.status-chip[aria-pressed="false"].tone-good,
.status-chip[aria-pressed="false"].tone-info,
.status-chip[aria-pressed="false"].tone-bad {
    color: var(--text-dim);
    border-color: var(--border);
    background: var(--surface2);
}
.status-chip-count { font-weight: 500; opacity: 0.75; font-variant-numeric: tabular-nums; }
/* All, pressed: the row is unfiltered. Solid so it reads as the current choice, not as a status. */
.status-chip-all[aria-pressed="true"] {
    color: var(--bg);
    background: var(--text);
    border-color: var(--text);
    opacity: 1;
}

.orders-caption {
    display: flex;
    justify-content: space-between;
    gap: 0.75rem;
    margin: 0.9rem 0.15rem 0.5rem;
    font-size: 0.78rem;
    color: var(--text-dim);
}
.orders-caption:has(span:empty + span:empty) { display: none; }
.status-chip.tone-good {
    color: var(--green);
    border-color: color-mix(in srgb, var(--green) 45%, transparent);
    background: color-mix(in srgb, var(--green) 10%, transparent);
}
.status-chip.tone-bad {
    color: #fca5a5;
    border-color: color-mix(in srgb, var(--red) 45%, transparent);
    background: color-mix(in srgb, var(--red) 10%, transparent);
}
/* On the way. Its own colour rather than a shade of the good/bad pair, because a package in
   transit is neither finished nor failed — it is the state the page is opened to look for. */
.status-chip.tone-info {
    color: var(--blue);
    border-color: color-mix(in srgb, var(--blue) 45%, transparent);
    background: color-mix(in srgb, var(--blue) 10%, transparent);
}

.target-order { margin-bottom: 0.75rem; overflow: hidden; border-radius: 12px; }
/* Date | total over number · account | Dismiss: two even lines, so neither column leaves the
   other a gap to sit over. The card's own surface, not a band — the divider is enough. */
.target-order-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline;
    gap: 0.15rem 0.75rem;
    padding: 0.75rem 1rem 0.55rem;
    border-bottom: 1px solid var(--border);
}
.target-order-title {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem 0.5rem;
    font-weight: 600;
    font-size: 0.95rem;
}
.target-order-head .order-total {
    font-weight: 700;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.target-order-sub {
    display: flex;
    align-items: baseline;
    min-width: 0;
    font-size: 0.78rem;
    color: var(--text-dim);
    white-space: nowrap;
}
.target-order-number { flex: none; }
/* Dim at rest and never hover-only: this is read on a phone as often as on a desktop, and a
   control that only exists under a cursor does not exist there at all. */
.order-dismiss {
    justify-self: end;
    /* A tap target, without the padding pushing the line taller than the number beside it. */
    min-height: 28px;
    padding: 0 0.3rem;
    margin: -0.3rem -0.3rem -0.3rem 0;
    border: none;
    background: none;
    font: inherit;
    font-size: 0.78rem;
    color: var(--text-dim);
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 0.2em;
}
.order-dismiss:hover { color: var(--text); }
.order-dismiss:disabled { opacity: 0.5; cursor: default; }
/* Beside the order number, and the thing that gives way on a narrow screen: an email is long. */
.account-tag {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
.account-tag::before { content: '·'; margin: 0 0.4rem; opacity: 0.6; }
.badge-bad {
    border-color: color-mix(in srgb, var(--red) 50%, transparent);
    color: #fca5a5;
}

.package { padding: 0.75rem 1rem 0.9rem; display: flex; flex-direction: column; gap: 0.65rem; }
.package + .package { border-top: 1px solid var(--border); }
.package-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.3rem 0.5rem;
    font-size: 0.84rem;
    color: var(--text-dim);
}
.package-meta .status-chip { min-height: 0; padding: 0.1rem 0.6rem; font-size: 0.74rem; cursor: default; }
/* When it is coming, or when it came: the line the card is opened for. */
.package:not(.package-bad) .package-facts { color: var(--text); font-weight: 600; }

/* Cancelled or refunded: still listed, because the order carried them, but one quiet line each
   so the package that IS coming stays the thing you notice. */
.package-bad { padding-block: 0.65rem 0.75rem; gap: 0.5rem; }
.package-bad .items { opacity: 0.55; }
.package-bad .thumb { width: 36px; height: 36px; }
.package-bad .item-body { flex: 1; flex-direction: row; align-items: baseline; gap: 0.75rem; }
.package-bad .item-title { flex: 1; min-width: 0; -webkit-line-clamp: 1; font-size: 0.85rem; }
.package-bad .item-meta { flex: none; }

/* A row of its own under the items: squeezed into the status line it read as part of it. */
.tracking {
    padding: 0.45rem 0.7rem;
    border-radius: 8px;
    background: var(--surface2);
    font-size: 0.8rem;
    color: var(--text-dim);
    overflow-wrap: anywhere;
}
.tracking-carrier {
    margin-right: 0.35rem;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    color: var(--text);
}
.tracking .mono { color: var(--text); user-select: all; }
/* A linked number is UNDERLINED at rest, unlike the item titles above it. Those sit in a list
   where the shape and the thumbnail say "product, click me"; a tracking number is a lone run of
   mono characters, so nothing but the underline says it goes anywhere. The offset keeps the rule
   clear of the digits' baseline; the arrow says it leaves the portal. */
.tracking a.mono { text-decoration: underline; text-underline-offset: 0.2em; }
.tracking a.mono:hover { text-decoration-thickness: 2px; }
.tracking a.mono::after { content: ' ↗'; color: var(--blue); text-decoration: none; display: inline-block; margin-left: 0.25rem; user-select: none; }

/* ── Recap ── */
/* ── Instances ── */
.instances-lede { margin: -0.4rem 0 1rem; }
.instances-summary {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 1.05rem;
    font-weight: 600;
    margin-bottom: 1rem;
}
/* Shared by the summary line and each row. No dot state is the "not known" case, which has to
   look different from both online and offline rather than defaulting to one of them. */
.instances-dot {
    width: 0.6rem;
    height: 0.6rem;
    border-radius: 999px;
    flex: none;
    background: var(--text-dim);
}
.instances-dot.dot-on { background: var(--green); }
/* Offline, or online and reporting something that stops it buying. */
.instances-dot.dot-off,
.instances-dot.dot-bad { background: var(--red); }
.instance { padding: 0.75rem 1rem; margin-bottom: 0.6rem; }
.instance-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem 0.5rem;
    font-weight: 600;
    font-size: 0.92rem;
}
/* An instance id is "{machine}#{hash}" and a machine name can be anything. */
.instance-name { min-width: 0; overflow-wrap: anywhere; }
.instance-meta { color: var(--text-dim); font-size: 0.82rem; margin-top: 0.15rem; }
.instance-problems { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.35rem; }
.instance-unreported { font-style: italic; }

.recap-name-row { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 1rem; }
/* The shared .field is monospace for licence keys; a name is ordinary text. */
.field-text { font-family: inherit; max-width: 360px; }

/* The card sizes itself off this width (cqw); the export is a fixed 1080px either way. */
.recap-stage { max-width: 720px; margin: 0 auto; }

.recap-actions {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 1rem;
}
.recap-note {
    min-height: 1.3em;
    margin-top: 0.75rem;
    text-align: center;
    font-size: 0.85rem;
    color: var(--text-dim);
}

/* Very narrow phones: drop the Discord name from the bar before the brand or Log out suffer. */
@media (max-width: 359px) {
    .account-name { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    .spinner { animation-duration: 2.4s; }
}

@media (max-width: 480px) {
    .page { padding-top: 1rem; }
    /* Three across leaves ~90px a tile on a small phone, which truncates the one number that
       matters most. Orders and Items share a row; Spent gets the full width under them. */
    /* Scales with the glass so a five-figure Spent still fits its third of a 360px phone. */
    .stat-value { font-size: clamp(1rem, 4.6vw, 1.15rem); }
    .center-card { margin-top: 1.5rem; padding: 1.5rem 1rem; }
    .toolbar-right { width: 100%; justify-content: space-between; }
    .orders-filters > .field-select { flex: 1 1 0; min-width: 0; }
    /* One row that scrolls sideways rather than three that wrap, faded at the edge to say so. It
       bleeds to the screen edge so a chip scrolls off the glass, not off an invisible margin. */
    .status-row {
        flex-wrap: nowrap;
        overflow-x: auto;
        margin-inline: -1rem;
        padding: 2px 1rem 4px;
        scrollbar-width: none;
        -webkit-mask-image: linear-gradient(90deg, #000 88%, transparent);
                mask-image: linear-gradient(90deg, #000 88%, transparent);
    }
    .status-row::-webkit-scrollbar { display: none; }
    .field-text { max-width: none; }
    /* Full-width, stacked: thumb-sized targets for the one thing this page is for. */
    .recap-actions { flex-direction: column; }
    .recap-actions .btn { width: 100%; }
    .stat { padding: 0.65rem 0.6rem; }
}
