/* Shelli Nexus. Hand-written: no CSS framework, because the portals are a handful of dense
   screens and a framework would be more bytes than the whole design.

   Shared by every portal, which is the point. It began in the Admin Portal and moved here when
   the Customer Portal needed the same buttons, cards, notices and account pages — and a second
   copy would have been a design system that drifts one fix at a time. Anything genuinely
   specific to one portal belongs in that portal's own scoped stylesheet, not here. */

:root {
    --ink: #14171c;
    --ink-soft: #4b5563;
    --ink-faint: #8b95a5;
    --paper: #f6f7f9;
    --surface: #ffffff;
    --line: #e3e7ed;

    --accent: #1f5fd6;
    --accent-dark: #17489f;

    /* WhatsApp's green, the only colour here we did not choose. Both portals want it now: the
       visitor's chat button, and the Admin screen where staff set the number behind it. Kept
       beside the palette rather than in one portal's sheet, because the alternative is two
       copies of somebody else's brand colour drifting apart. */
    --wa-green: #1fa855;
    --wa-green-dark: #17833f;
    --wa-green-soft: #e7f6ed;

    /* .NET MAUI's purple. Draf IT builds on .NET, and the mark says so without a word of
       copy — blue for Nexus, purple for what it is made of. */
    --maui: #512bd4;
    --maui-dark: #3f1fae;

    /* One gradient, used everywhere the brand appears: the mark, the active nav item, the
       account card's rule. Declared once so a future palette change is one line. */
    --brand: linear-gradient(135deg, var(--accent) 0%, var(--maui) 100%);
    --good: #11734b;
    --good-soft: #e6f5ee;
    --warn: #8a5300;
    --warn-soft: #fdf3e2;
    --bad: #a3232b;
    --bad-soft: #fdecec;

    --sidebar: #14171c;
    --sidebar-ink: #b9c1cc;
    --sidebar-ink-strong: #ffffff;

    --radius: 10px;

    /* Elevation rather than outline. Every panel used to be a 1px rectangle, which is what made
       these screens read as older than they are — a hairline box is how a page looked before
       browsers could draw a cheap shadow. The token existed and nothing used it. */
    --shadow: 0 1px 2px rgb(20 23 28 / 4%), 0 2px 8px rgb(20 23 28 / 5%);

    /* The focus ring, following the accent instead of naming a colour.

       It used to be a literal blue, which meant the Customer Portal — retuned to Shelli's
       purple — drew blue rings on purple fields. `color-mix` keeps one rule honest in both
       portals, and there is exactly one place to change it. */
    --ring: color-mix(in srgb, var(--accent) 22%, transparent);
}

* {
    box-sizing: border-box;
}

/* Script hides things by setting the `hidden` property, and without this that
   quietly does nothing to anything a class gave a `display` to.

   `[hidden]` and `.button` carry the same specificity, and at equal weight an author rule beats the
   user agent's - so `.button { display: inline-block }` wins and the element stays painted with the
   attribute set.

   Found 21 Aug 2026 on the thread's jump button, which would have sat on screen
   permanently. The copy button had it too, and there the failure was the one its
   own comment was written to prevent: it ships hidden and is revealed only where
   the clipboard API exists, so on a browser without it somebody was being shown
   a button that could not copy.

   `!important` because that is the only weight that beats a class, and because
   `hidden` means hidden - there is no case where something carries the attribute
   and should still be drawn. */
[hidden] {
    display: none !important;
}

html,
body {
    margin: 0;
    padding: 0;
}

body {
    background: var(--paper);
    color: var(--ink);
    font-family: "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif;
    font-size: 15px;
    line-height: 1.55;

    /* Nothing in this portal is meant to scroll sideways. If something does, it is a bug —
       and this makes it show up as a clipped element rather than as a page that drifts. */
    overflow-x: hidden;
}

/* Anything genuinely too wide for a phone — a table, a log line — scrolls inside itself. */
.scroll-x {
    -webkit-overflow-scrolling: touch;
    max-width: 100%;
    overflow-x: auto;
}

h1 {
    font-size: 1.6rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    margin: 0 0 0.4rem;
}

/* FocusOnNavigate moves focus to the page heading after each navigation so a screen
   reader announces where it landed. It does that by adding tabindex="-1", which also makes
   the browser draw a focus ring around the title on every page — a visual artefact of an
   accessibility feature nobody sighted asked for. Keyboard focus rings elsewhere are
   untouched. */
[tabindex="-1"]:focus {
    outline: none;
}

h2 {
    font-size: 1.05rem;
    font-weight: 600;
    margin: 0 0 0.6rem;
}

a {
    color: var(--accent);

    /* The underline sat on the letters. Dropping it a couple of pixels and thinning it is most
       of the difference between a 2005 link and a current one, and it stays underlined — colour
       alone is not a link. */
    text-decoration-thickness: 1px;
    text-underline-offset: 0.15em;
}

/* Keyboard focus on anything that is not a field. Fields draw their own ring below; buttons and
   links were left to the browser's default, which differs per browser and lands square on a
   rounded control. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[role="button"]:focus-visible {
    border-radius: var(--radius);
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.muted {
    color: var(--ink-faint);
}

/*
    A machine-generated identifier sitting under a human name — a device's hardware identity, a
    client id. Monospaced because these get read aloud down a telephone and compared character by
    character, and a proportional font makes 1/l and 0/O the same shape.

    `anywhere` rather than `break-word`: only `anywhere` counts towards a table cell's minimum
    width, so without it one unbreakable identifier sets the column and pushes the stacked phone
    layout wider than the screen.
*/
.mono-small {
    font-family: ui-monospace, "Cascadia Mono", "SFMono-Regular", Consolas, monospace;
    font-size: 0.8125rem;
    overflow-wrap: anywhere;
}

.lede {
    color: var(--ink-soft);
    max-width: 60ch;
}

.page-status {
    color: var(--ink-faint);
    padding: 2rem;
}

/* --- Shell ------------------------------------------------------------------ */

.shell {
    display: flex;
    min-height: 100vh;
}

.shell-main {
    display: flex;
    flex: 1;
    flex-direction: column;
    min-width: 0;
}

/* The drawer's state, off-screen rather than display:none so it keeps its place in the tab
   order. Tab reaches it first and Space opens the menu — which is the whole reason this is
   a checkbox and not a div. */
.drawer-state {
    height: 1px;
    opacity: 0;
    pointer-events: none;
    position: absolute;
    width: 1px;
}

.drawer {
    background: var(--sidebar);
    color: var(--sidebar-ink);
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
    gap: 0.25rem;
    overflow-y: auto;
    padding: 1rem 0.75rem 2rem;
    width: 232px;
}

.drawer-head {
    align-items: center;
    display: flex;
    justify-content: space-between;
    margin-bottom: 1.25rem;
}

/* A quiet square target for an icon. 40px here, lifted to 44 on a touch screen below. */
.icon-button {
    align-items: center;
    background: none;
    border: 0;
    border-radius: 8px;
    color: inherit;
    cursor: pointer;
    display: inline-flex;
    flex-shrink: 0;
    height: 40px;
    justify-content: center;
    padding: 0;
    width: 40px;
}

.icon-button:hover {
    background: rgb(127 127 127 / 14%);
}

/* The one save on an issue, drawn as an icon rather than as a word: Coloured, because .icon-button
   on its own is a quiet target and this is the primary action on the screen.

   Declared after .icon-button:hover on purpose: same specificity, so order is what decides. */
.icon-button-accent {
    background: var(--accent);
    color: #fff;
}

.icon-button-accent:hover {
    background: var(--accent-dark);
}

/* The cart, in the header beside the account menu.

   Positioned so the count can sit on its corner. The count is a separate element
   rather than a ::after on the link, because it carries a number that changes and
   generated content cannot be read by everything that needs to read it — the
   accessible name is a visually-hidden sentence beside it. */
.cart-button {
    position: relative;
    text-decoration: none;
}

/* Small, and deliberately not a perfect circle: a two-digit basket is ordinary and a
   fixed width would clip it. min-width keeps a single digit from looking squashed. */
.cart-count {
    background: var(--accent);
    border-radius: 999px;
    color: #fff;
    font-size: 0.68rem;
    font-weight: 700;
    line-height: 1;
    min-width: 1.05rem;
    padding: 0.18rem 0.3rem;
    position: absolute;
    right: 2px;
    text-align: center;
    top: 2px;
}

/* All three only mean anything while the drawer floats over the page. Above the breakpoint
   it is simply the left column, and a button to open what is already open is a lie.
   Declared after .icon-button on purpose: same specificity, so order is what decides. */
.drawer-open,
.drawer-close,
.drawer-scrim {
    display: none;
}

/* The visible buttons are labels, which never take focus. The ring has to be drawn on the
   checkbox's behalf or a keyboard user cannot see what Space is about to do. */
.drawer-state:focus-visible ~ .shell-main .drawer-open,
.drawer-state:focus-visible ~ .drawer .drawer-close {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.brand {
    align-items: center;
    color: var(--sidebar-ink-strong);
    display: flex;
    font-weight: 600;
    gap: 0.6rem;
    padding: 0.5rem;
    text-decoration: none;
}

.brand-mark {
    align-items: center;
    background: var(--brand);
    border-radius: 8px;

    /* Lifts it off a dark sidebar so the gradient reads as a mark rather than as a smudge. */
    box-shadow: 0 2px 8px rgb(81 43 212 / 45%);
    color: #fff;
    display: flex;
    flex-shrink: 0;
    font-weight: 700;
    height: 28px;
    justify-content: center;
    letter-spacing: -0.02em;
    width: 28px;
}

.nav-section {
    margin-bottom: 1rem;
}

.nav-heading {
    color: #6c7889;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    padding: 0.35rem 0.5rem;
    text-transform: uppercase;
}

.nav-link {
    align-items: center;
    border-radius: 6px;
    color: var(--sidebar-ink);
    display: flex;
    gap: 0.5rem;
    padding: 0.4rem 0.5rem;
    text-decoration: none;
}

.nav-link:hover {
    background: #1f242c;
    color: var(--sidebar-ink-strong);
}

.nav-link.active {
    background: var(--brand);
    color: #fff;
}

.nav-soon {
    background: #262d37;
    border-radius: 999px;
    color: #7c8797;
    font-size: 0.65rem;
    letter-spacing: 0.04em;
    margin-left: auto;
    padding: 0.05rem 0.4rem;
    text-transform: uppercase;
}

/* Two controls at any width: the menu and the account. Everything that used to live up here
   moved into the account panel, which is why this row no longer has anything to wrap. */
.topbar {
    align-items: center;
    background: var(--surface);
    border-bottom: 1px solid var(--line);
    display: flex;
    gap: 0.5rem;
    padding: 0.5rem 1rem;

    /* Sticky at every width, so the menu is reachable from the bottom of a long form. */
    position: sticky;
    top: 0;
    z-index: 30;
}

/* The name goes home. It already did — it is an anchor to "/" — but with no underline and no
   colour it reads as a printed title rather than as something to press, so nobody tries. The
   affordance appears on hover and on keyboard focus. */
.brand:hover .brand-text,
.brand:focus-visible .brand-text {
    text-decoration: underline;
}

/* The mark in the topbar, and only while the drawer is not a column.

   .brand was written for the Admin Portal's dark sidebar and carries that palette, which is
   why it needs undoing here rather than being copied into a second class.

   Hidden by default, shown below the breakpoint — the same shape as .drawer-open above,
   and for the same reason. This started as the fix for a header that read as plain text on a
   portal with no sidebar; the drawer landed afterwards and carries the lockup itself, so above
   860px both were drawn and the product signed its own name twice on one screen. The narrow
   case is still real: down there the drawer is off-canvas and the header would be unbranded. */
.topbar .brand {
    color: inherit;
    display: none;
    padding: 0;
}

/* --- Account menu ----------------------------------------------------------- */

/*
 * Pushed to the right, and it takes the account menu's job of doing so. Two elements each
 * claiming the free space would split it between them and land the button in the middle of
 * the bar, so whichever comes first claims it and the other stops asking.
 */
.topbar-download {
    margin-left: auto;

    /* Centred on its own text, and clear of the margin .button-primary carries for the case it
       was written for - one button under a column form. In a bar that has already centred
       everything, that margin drops the only control on the row below the account beside it,
       and .button being inline-block leaves its label on a baseline the flex row does not
       share. The public nav met both and fixed them; this one is the same bar signed in. */
    align-items: center;
    display: inline-flex;
    justify-content: center;
    min-height: 2rem;
    white-space: nowrap;
}

/* Two classes, because one is not enough: .button-primary is written further down this file and
   would win the tie against a single .topbar-download, which is how the margin survived being
   zeroed and left the bar 19px taller than its own contents. The same shape .action-row uses. */
.topbar .button-primary {
    margin-top: 0;
}

.user-menu {
    margin-left: auto;
    position: relative;
}

.topbar-download ~ .user-menu {
    margin-left: 0;
}

.user-button {
    align-items: center;
    border-radius: 999px;
    cursor: pointer;
    display: flex;
    gap: 0.5rem;
    list-style: none;
    padding: 0.2rem 0.6rem 0.2rem 0.2rem;
}

/* Safari draws its own triangle from a separate pseudo-element. */
.user-button::-webkit-details-marker {
    display: none;
}

.user-button:hover {
    background: rgb(127 127 127 / 14%);
}

.avatar {
    align-items: center;
    background: var(--brand);
    border-radius: 999px;
    color: #fff;
    display: flex;
    flex-shrink: 0;
    font-size: 0.75rem;
    font-weight: 700;
    height: 32px;
    justify-content: center;
    width: 32px;
}

.user-name {
    font-weight: 600;
    max-width: 12rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.chevron {
    color: var(--ink-faint);
    transition: transform 120ms ease;
}

.user-menu[open] .chevron {
    transform: rotate(180deg);
}

/* Clicking away closes the panel. The catcher covers the viewport, belongs to the summary,
   and so toggles the menu shut on any click outside it — the one behaviour <details> does
   not have on its own, and the reason none of this needs a script. */
.user-menu[open] .user-button::before {
    content: "";
    inset: 0;
    position: fixed;
    z-index: -1;
}

.user-panel {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    min-width: 16rem;
    padding: 0.4rem;
    position: absolute;
    right: 0;
    top: calc(100% + 0.4rem);
    z-index: 40;
}

.user-identity {
    border-bottom: 1px solid var(--line);
    display: flex;
    flex-direction: column;
    line-height: 1.3;
    margin-bottom: 0.3rem;
    padding: 0.4rem 0.6rem 0.6rem;
}

.user-identity-name {
    font-weight: 600;
    overflow-wrap: anywhere;
}

.user-identity-title {
    color: var(--ink-faint);
    font-size: 0.85rem;
}

.menu-item {
    background: none;
    border: 0;
    border-radius: 6px;
    color: var(--ink);
    cursor: pointer;
    display: block;
    font: inherit;
    padding: 0.5rem 0.6rem;
    text-align: left;
    text-decoration: none;
    width: 100%;
}

.menu-item:hover {
    background: var(--paper);
}

.menu-item-strong {
    color: var(--bad);
    font-weight: 600;
}

.menu-group {
    border-top: 1px solid var(--line);
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    margin: 0.3rem 0;
    padding: 0.5rem 0.6rem;
}

.menu-label {
    color: var(--ink-faint);
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.menu-form {
    margin: 0;
}

/* One centred column for every page, rather than content stretched across whatever width
   the monitor happens to be. Left-aligned on a 2000px screen leaves the eye travelling half
   a metre from a label to its field, and the page reads as though it failed to load.

   64rem, and `width: 100%` so it still fills a phone. Nothing below the breakpoint changes:
   the padding is what governs there, and the maximum never comes into play. */
.content {
    margin-inline: auto;
    max-width: 64rem;
    padding: 1.75rem 1.5rem 3rem;
    width: 100%;
}

/* --- Cards and data --------------------------------------------------------- */

.cards {
    display: grid;
    gap: 1rem;

    /* min(300px, 100%) rather than a flat 300px: on a 320px phone a flat minimum makes the
       track wider than the screen, and the whole page scrolls sideways. */
    grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
    margin-top: 1.5rem;
}

/* A grid holding one card is not a grid. auto-fit gives it a 300px track and leaves the rest
   of a wide screen empty beside it, which reads as a card that failed to load rather than as
   the only card there is.

   It happens more than it looks: several of these rows are one card plus another behind a
   permission, so whoever lacks that permission is the person who sees the broken-looking
   version — and never mentions it, because they assume that is what the page is. */
.cards:has(> .card:only-child) {
    grid-template-columns: 1fr;
}

/* --- Tabs on a record page -------------------------------------------------- */

/* Ordinary links, so they work with JavaScript off and can be sent to a colleague.

   Scrolls sideways rather than wrapping: four tabs wrapped onto two lines on a phone look
   like two groups of something, and the second line reads as less important than the first. */
.tabs {
    border-bottom: 1px solid var(--line);
    display: flex;
    gap: 0.25rem;
    margin: 1.5rem 0 0;
    overflow-x: auto;

    /* The active tab's underline sits on the container's border, so a scrollbar appearing
       must not push it out of alignment. */
    scrollbar-width: thin;
}

.tab {
    border-bottom: 2px solid transparent;
    color: var(--ink-soft);
    display: block;
    font-weight: 600;
    padding: 0.6rem 0.9rem;
    text-decoration: none;
    white-space: nowrap;
}

.tab:hover {
    color: var(--ink);
}

/* Two signals, not one. Colour alone fails for anybody who cannot distinguish these two,
   and the underline is what carries the meaning for them.

   The four selectors under it are the issue screen's panes, which are shown with CSS rather
   than navigated to — see .pane-pick. They are listed here rather than given a rule of their
   own so an open tab looks the same however it was opened. */
.tab-active,
.pane-pick-detail:checked ~ .work-layout .tab-detail,
.pane-pick-metrics:checked ~ .work-layout .tab-metrics,
.pane-pick-test:checked ~ .work-layout .tab-test,
.pane-pick-files:checked ~ .work-layout .tab-files {
    border-bottom-color: var(--accent);
    color: var(--ink);
}

/* A label is not a link, and without this nothing says it can be clicked. */
.tabs label.tab {
    cursor: pointer;
    user-select: none;
}

@media (pointer: coarse) {
    .tab {
        min-height: 44px;
        padding-block: 0.75rem;
    }
}

.card {
    background: var(--surface);

    /* Border and shadow, not one or the other: the shadow gives the panel its lift and the hairline
       keeps its edge crisp where a shadow alone goes muddy against grey paper. */
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 1.5rem 1.6rem;
}

/* The sentence under a card's heading explains the heading, so it sits with it rather than
   floating halfway to the first field. */
.card > h2 + .muted,
.card > h2 + .lede,
.card > h2 + p {
    margin-top: -0.2rem;
}

/* ...and the form after it starts a new block, rather than running straight on from a
   sentence it is not part of. */
.card > p + .account-form,
.card > p + form {
    margin-top: 1.15rem;
}

.card dl {
    display: grid;
    gap: 0.35rem 1rem;
    grid-template-columns: auto 1fr;
    margin: 0 0 1rem;
}

.card dt {
    color: var(--ink-faint);
}

.card dd {
    margin: 0;

    /* An email address has no spaces to break at, so on a phone it either wraps mid-word
       or pushes the card off the screen. Wrapping is the lesser evil. */
    overflow-wrap: anywhere;
}

code {
    background: var(--paper);
    border-radius: 4px;
    font-size: 0.85em;
    overflow-wrap: anywhere;
    padding: 0.1rem 0.3rem;
}

/* A value somebody has to copy out and type in somewhere else — the identifier a Shelli
   installation links to a customer by.

   `user-select: all` selects the whole thing on one click. A copy button would need an
   interactive circuit and everything here is static rendering, so the browser's own selection
   is what works on a cold page load. Sized up from the ordinary inline code, because this is
   thirty-six characters somebody is reading one at a time to check they got it right. */
.identifier {
    display: inline-block;
    font-size: 0.95rem;
    letter-spacing: 0.02em;
    padding: 0.35rem 0.55rem;
    user-select: all;
}

/* The value and its copy button. Wraps rather than squeezing, because the value can be a
   36-character id or a whole invitation link and neither shortens. */
.copy-text {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: 0.6rem 0 0;
}

.copy-text .identifier {
    flex: 1 1 16rem;
    min-width: 0;
}

/* Sized so pressing it does not move the value beside it. The label swaps to "Copied" on
   success, which is longer in both languages — hence a minimum rather than padding alone. */
.copy-button {
    flex: 0 0 auto;
    min-width: 6.5rem;
}

.pill {
    border-radius: 999px;
    font-size: 0.78rem;
    font-weight: 600;
    padding: 0.1rem 0.55rem;
}

.pill-good {
    background: var(--good-soft);
    color: var(--good);
}

.pill-warn {
    background: var(--warn-soft);
    color: var(--warn);
}

/* Wrong, not merely late. The soft pair already exists for notices; the pill wanted it too,
   and until the operations page needed to say "the backups are not happening" nothing in the
   Admin Portal had a state worse than a warning. */
.pill-bad {
    background: var(--bad-soft);
    color: var(--bad);
}

/* Ended, rather than wrong. A closed customer is not a problem to be fixed, so it reads as
   quieter than the others instead of louder. */
.pill-muted {
    background: var(--paper);
    color: var(--ink-faint);
}

/* --- Validation ------------------------------------------------------------- */

/* Blazor's own class names, styled here because without them a rule the browser cannot
   check — an address that is not an address, a value too long — fails the post and renders
   nothing at all. The form simply reappears, unchanged and unexplained, and the person
   presses the button again. */
.validation-errors {
    background: var(--bad-soft);
    border-radius: 8px;
    color: var(--bad);
    font-size: 0.9rem;
    list-style: none;
    margin: 0 0 1rem;
    padding: 0.7rem 0.9rem;
}

.validation-errors li + li {
    margin-top: 0.35rem;
}

.validation-message {
    color: var(--bad);
    display: block;
    font-size: 0.85rem;
    margin: -0.4rem 0 0.6rem;
}

.account-form .invalid {
    border-color: var(--bad);
}

/* --- Notices ---------------------------------------------------------------- */

.banner {
    border-bottom: 1px solid var(--line);
    padding: 0.7rem 1.5rem;
}

.banner-warning {
    background: var(--warn-soft);
    border-bottom-color: #f0dcb8;
    color: var(--warn);
}

.notice {
    border-radius: var(--radius);
    margin-bottom: 1rem;
    padding: 0.7rem 0.9rem;
}

.notice-error {
    background: var(--bad-soft);
    color: var(--bad);
}

.notice-good {
    background: var(--good-soft);
    color: var(--good);
}

.notice-blocked {
    background: var(--surface);
    border: 1px solid var(--line);
    margin: 2rem auto;
    max-width: 48ch;
    padding: 1.5rem;
}

/* --- Provider settings ------------------------------------------------------ */

.provider-group {
    margin-top: 2rem;
}

.provider-group h2 {
    border-bottom: 1px solid var(--line);
    padding-bottom: 0.4rem;
}

/* A form is a single column of fields. Left in the .cards grid it would sit in a 300px
   track next to an empty one, which reads as a broken layout rather than a narrow form. */
/* Full width of the page column, which is itself capped and centred. The old 46rem cap
   predates that column: it kept forms from stretching across a whole monitor, but now it
   only makes a card narrower than the heading sitting above it, so the two disagree about
   where the page starts. The fields inside stay narrow — see .account-form. */
.card-wide {
    grid-column: 1 / -1;
}

/* Consecutive cards standing on their own need their own rhythm. Cards inside .cards are a
   grid and take its gap instead, so this is scoped to the ones that are not. */
.content > .card + .card {
    margin-top: 1.25rem;
}

.card-link {
    color: inherit;
    display: block;
    text-decoration: none;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

.card-link:hover,
.card-link:focus-visible {
    border-color: var(--accent);
    box-shadow: var(--shadow);
}

.card-link h3 {
    font-size: 1rem;
    margin: 0 0 0.5rem;
}

.empty-category {
    margin-top: 0.8rem;
    max-width: 60ch;
}

.crumbs {
    font-size: 0.85rem;
    margin-bottom: 0.5rem;
}

.facts {
    display: grid;
    gap: 0.4rem 1rem;
    grid-template-columns: auto 1fr;
    margin: 1rem 0 0;
}

.facts dt {
    color: var(--ink-faint);
}

.facts dd {
    margin: 0;
    overflow-wrap: anywhere;
}

/* The provider's own error text. Pre-wrapped rather than pre, because an AWS message is a
   paragraph and a horizontal scrollbar under it helps nobody read it. */
.provider-detail {
    background: var(--paper);
    border-radius: var(--radius);
    font-size: 0.85rem;
    margin: 1rem 0 0;
    padding: 0.7rem 0.9rem;
    white-space: pre-wrap;
    word-break: break-word;
}

.shared-key {
    font-size: 1.05rem;
    letter-spacing: 0.04em;
    margin: 0.5rem 0;
}

.steps {
    margin: 0 0 1.5rem;
    padding-left: 1.2rem;
}

.steps li + li {
    margin-top: 0.7rem;
}

/* Recovery codes are read off the screen and typed into a password manager, so they are
   laid out to be scanned rather than to fill the width. */
.recovery-codes {
    display: grid;
    gap: 0.4rem 1rem;
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    list-style: none;
    margin: 1rem 0 0;
    padding: 0;
}

.recovery-codes code {
    font-size: 0.95rem;
    letter-spacing: 0.03em;
}

/* --- Forms and buttons ------------------------------------------------------ */

/* The card fills the page column; the fields inside do not. A text input as wide as a
   monitor is harder to read than a short one, and the eye loses the line between a label
   and the box under it. 34rem is roughly the widest a single-line field stays comfortable.

   No effect on a phone, where the card is already narrower than this. */
.account-form {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    max-width: 34rem;
}

.account-form label,
.field-label {
    font-weight: 600;
    margin-top: 1rem;
}

/* Names a group of controls that no single <label for> can point at — the six boxes of a
   one-time code. A span, so it needs the block behaviour a label already has. It must not
   be folded into the rule above: `.account-form label` would outweigh `.method-option` and
   `.check`, which are labels laid out as rows, and flatten both. */
.field-label {
    display: block;
}

/* The first field starts where the card's sentence left off, without a second gap on top of
   the one that sentence already leaves. */
.account-form > label:first-of-type,
.account-form > .field-label:first-of-type {
    margin-top: 0;
}

/* Selected by exclusion rather than by listing every type.

   Blazor's <InputText> renders an input with no type attribute at all, so a rule written
   as input[type="text"] never matches it — which is why one field on the invite page looked
   designed and the two next to it looked like raw browser defaults. Excluding the boxes and
   the ticks is shorter than enumerating every type a form might grow, and it cannot fall
   behind when one is added.

   .code-digit is out because it has its own, larger type; the coarse-pointer rule below
   would otherwise shrink a 24px digit to 16px. */
.account-form input:not([type="checkbox"]):not([type="radio"]):not(.code-digit),
.account-form select,
.account-form textarea {
    width: 100%;
}

/* --- One-time code ---------------------------------------------------------- *

   Six boxes that behave as one field. The behaviour is in code-input.js and is entirely
   optional — see CodeInput.razor for how this posts without it. */

.code-input {
    display: flex;
    gap: 0.5rem;
    margin-top: 0.15rem;
    max-width: 21rem;
}

.code-digit {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    caret-color: var(--accent);

    /* Equal shares of whatever room there is, and allowed to shrink below their content —
       so six boxes fit a 320px screen instead of pushing the page sideways. */
    flex: 1 1 0;
    min-width: 0;

    font-family: inherit;
    font-size: 1.4rem;
    font-weight: 600;
    height: 3.1rem;
    padding: 0;
    text-align: center;

    /* Digits of one width, so the row does not shift as it fills. */
    font-variant-numeric: tabular-nums;
}

.code-digit:focus-visible {
    border-color: var(--accent);
    outline: 2px solid var(--ring);
    outline-offset: 1px;
}

/* The fallback field, only ever rendered inside <noscript>. */
.code-plain {
    font-size: 1.2rem;
    letter-spacing: 0.3em;
}

.field-hint {
    color: var(--ink-faint);
    font-size: 0.82rem;
    margin: 0.15rem 0 0;
}

.checkbox {
    align-items: center;
    display: flex;
    font-weight: 400 !important;
    gap: 0.45rem;
}


/* --- Controls ---------------------------------------------------------------

   One look for every field in the product rather than only the ones inside an
   .account-form. The pricing grid's boxes live in table cells, so they inherited
   nothing and rendered as raw browser defaults sitting next to fields that were
   designed — the same failure as the missing type attribute above, from the
   opposite direction: there the selector was too narrow for the markup, here it
   was too narrow for the page.

   Selected by exclusion for that same reason. Blazor's <InputText> renders an
   input with no type attribute at all, so input[type="text"] never matches
   it. Buttons, ticks and file pickers are excluded because they are controls of
   a different shape; .code-digit has its own, larger type. */

input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="file"]):not([type="range"]):not([type="color"]):not(.code-digit),
select,
textarea {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--ink);
    font: inherit;
    padding: 0.5rem 0.7rem;

    /* A hairline of depth, so a field reads as somewhere to type rather than as
       a rectangle drawn on the page. Any more and a form of twelve of them looks
       embossed. */
    box-shadow: inset 0 1px 2px rgb(20 23 28 / 4%);
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

input:not([type="checkbox"]):not([type="radio"]):not(.code-digit):hover:not(:disabled),
select:hover:not(:disabled),
textarea:hover:not(:disabled) {
    border-color: #cfd6e0;
}

/* The ring replaces the outline rather than removing it — same job, drawn to the
   field's own corner radius so it reads as one control instead of a box with a
   second box around it. Plain :focus, not :focus-visible: a field somebody
   clicked into is a field they are typing in, and the browser's own caret is too
   thin to find on a dense screen. */
input:not([type="checkbox"]):not([type="radio"]):not(.code-digit):focus,
select:focus,
textarea:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--ring);
    outline: none;
}

input:disabled,
select:disabled,
textarea:disabled {
    background: var(--paper);
    color: var(--ink-faint);
    cursor: not-allowed;
}

::placeholder {
    color: var(--ink-faint);
}

/* The native arrow is drawn by the platform and looks like a different decade on
   every one of them. Replaced with a chevron as a data URI — img-src already
   allows data:, so this widens nothing.

   padding-right leaves room for it; without that the longest option's text runs
   underneath the arrow. */
select {
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='none' stroke='%234b5563' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round' d='M1 1.5 6 6.5l5-5'/%3E%3C/svg%3E");
    background-position: right 0.65rem center;
    background-repeat: no-repeat;
    background-size: 12px 8px;
    padding-right: 2rem;
}

/* Every country on earth in one control.

   A bare <select> sizes itself to its widest option, and "Bonaire, Sint Eustatius and Saba (BQ)"
   is wider than a phone. Left alone it pushes the whole page into horizontal scroll — found on
   the fiscal setup screen at 375px, where the page was 568px wide because of this one element.

   Capped rather than shortening the names: the code in brackets is what somebody checks their
   answer against, and truncating the label would take it away. */
.country-select {
    max-width: 100%;
}

/* A field in a table cell fills its column. Left at its default width it is an
   island in the middle of the cell, and a column of them does not line up. */
.data-table input:not([type="checkbox"]):not([type="radio"]),
.data-table select,
.data-table textarea {
    width: 100%;
}

.button {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--ink);
    cursor: pointer;
    display: inline-block;
    font: inherit;
    font-weight: 600;
    padding: 0.5rem 0.9rem;
    text-align: center;
    text-decoration: none;
}

.button {
    transition: background-color 0.12s ease, border-color 0.12s ease;
}

.button:hover {
    border-color: #cfd6e0;
}

/* A press the pointer can feel. Without it a button that posts a slow form gives no sign it
   was hit, and people press it again — which is how a payment gets recorded twice. */
.button:active {
    transform: translateY(1px);
}

/* Shrink-wrapped to its words.

   A column flex container stretches its children, so a submit button was coming out as wide
   as the widest field — a 34rem slab of accent blue that reads as a banner rather than as the
   one thing on the card to press. Full width comes back on a phone, where a wide target is
   worth more than the restraint. */
.account-form > .button,
.account-form > button[type="submit"] {
    align-self: flex-start;
}

.button-primary {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
    margin-top: 1.2rem;
    padding-inline: 1.35rem;
}

.button-primary {
    box-shadow: 0 1px 2px rgb(20 23 28 / 12%);
}

.button-primary:hover {
    background: var(--accent-dark);
    border-color: var(--accent-dark);
}

.button-quiet {
    font-weight: 500;
}

/* For a table row's own actions, where a full-size button per row turns a list of six people
   into a wall of buttons. Still 44px tall on a touch screen — see the coarse-pointer rule
   below, which every button already obeys. */
.button-small {
    font-size: 0.85rem;
    padding: 0.3rem 0.6rem;
}

/* Stacked, not side by side: a narrow actions column would otherwise squeeze two buttons into
   a column too thin to read either. */
.data-table td > .button-small,
.data-table td form + form {
    margin-top: 0.35rem;
}

/* Flex rather than block, so the mark and the words sit on one baseline and stay centred
   together as the button grows to the card's width. */
.button-google {
    align-items: center;
    display: flex;
    gap: 0.6rem;
    justify-content: center;
    margin-top: 0.25rem;
}

.google-mark {
    flex-shrink: 0;
}

/* Divides one page into the two or three things it is actually about. Spaced like a new
   start rather than like a paragraph, because a heading that hugs the card above it reads
   as that card's subtitle. */
.section-heading {
    border-top: 1px solid var(--line);
    font-size: 1.15rem;
    margin: 2.25rem 0 0.4rem;
    padding-top: 1.75rem;
}

/* --- Language picker -------------------------------------------------------- *

   A globe naming the language you are reading, opening to the ones you can switch to. Each
   option is a submit button, so the choice is the change — there is no Apply, and nothing
   waits for a script. */

.culture-menu {
    position: relative;
}

.culture-button {
    align-items: center;
    border-radius: var(--radius);
    color: var(--ink-soft);
    cursor: pointer;
    display: flex;
    gap: 0.45rem;
    list-style: none;
    padding: 0.4rem 0.6rem;
}

.culture-button::-webkit-details-marker {
    display: none;
}

.culture-button:hover {
    background: rgb(127 127 127 / 14%);
}

.culture-current {
    font-weight: 500;
}

.culture-menu[open] .chevron {
    transform: rotate(180deg);
}

/* Same click-away catcher as the account menu, for the same reason. */
.culture-menu[open] .culture-button::before {
    content: "";
    inset: 0;
    position: fixed;
    z-index: -1;
}

.culture-options {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    display: flex;
    flex-direction: column;
    margin: 0;
    min-width: 11rem;
    padding: 0.3rem;
    position: absolute;
    right: 0;

    /* Opens upward. This sits at the bottom of the sign-in card and at the bottom of the
       account panel, so downward would leave the list off the end of both. */
    bottom: calc(100% + 0.35rem);
    z-index: 60;
}

.culture-option {
    background: none;
    border: 0;
    border-radius: 6px;
    color: var(--ink);
    cursor: pointer;
    font: inherit;
    padding: 0.5rem 0.6rem 0.5rem 1.9rem;
    position: relative;
    text-align: left;
    white-space: nowrap;
    width: 100%;
}

.culture-option:hover {
    background: var(--paper);
}

/* The tick marks what you are reading now. Drawn rather than written, so it needs no
   translation and cannot be mistaken for part of the language's name. */
.culture-option-current {
    font-weight: 600;
}

.culture-option-current::before {
    color: var(--accent);
    content: "✓";
    left: 0.6rem;
    position: absolute;
}

.account-tools {
    border-top: 1px solid var(--line);
    display: flex;
    justify-content: flex-end;
    margin-top: 1.5rem;
    padding-top: 1rem;
}

/* The way out of an account form — "I forgot my password", "back to sign in". Quiet and
   centred, below the button rather than beside it: it is an escape hatch, not the second half
   of a decision, and giving it equal weight makes people stop and read it every time. */
.account-aside {
    color: var(--ink-soft);
    font-size: 0.9rem;
    margin: 1rem 0 0;
    text-align: center;
}

/* Present to a screen reader, absent to everyone else. The picker's label is redundant
   next to a control that shows the current language, but a control with no label at all is
   unusable to somebody who cannot see it. */
/* WhatsApp's mark, wherever it is put. Block, because an inline SVG sits on the text baseline
   and leaves a descender's worth of gap under it - inside a round button that is a mark sitting
   visibly high. Never shrinks: beside a longer sentence in Spanish it is a flex item, and a
   squashed logo is worse than none. */
.wa-mark {
    display: block;
    flex: none;
}

/* The mark on a disc of its own colour, for a page heading that is about WhatsApp. */
.wa-badge {
    align-items: center;
    background: var(--wa-green-soft);
    border-radius: 30%;
    color: var(--wa-green);
    display: inline-flex;
    flex: none;
    height: 2.75rem;
    justify-content: center;
    width: 2.75rem;
}

.visually-hidden {
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}

/* --- Account pages ---------------------------------------------------------- */

.account-page {
    align-items: center;
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: 100vh;
    padding: 2rem 1rem;
}

.account-card {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 12px;
    box-shadow: var(--shadow);
    max-width: 400px;
    overflow: hidden;
    padding: 2rem;
    position: relative;
    width: 100%;
}

/* A band of the brand across the top of the sign-in card. The one place the gradient is
   worth showing at size — it is the first thing anyone sees of Nexus. */
.account-card::before {
    background: var(--brand);
    content: "";
    height: 4px;
    left: 0;
    position: absolute;
    right: 0;
    top: 0;
}

.account-brand {
    align-items: center;
    display: flex;
    font-weight: 600;
    gap: 0.6rem;
    margin-bottom: 1.5rem;
}

.account-lede {
    color: var(--ink-soft);
    margin-top: 0;
}

.account-footer {
    color: var(--ink-faint);
    font-size: 0.82rem;
    margin-top: 1.5rem;
}

.divider {
    align-items: center;
    color: var(--ink-faint);
    display: flex;
    font-size: 0.82rem;
    gap: 0.75rem;
    margin: 1.25rem 0 0.75rem;
}

.divider::before,
.divider::after {
    background: var(--line);
    content: "";
    flex: 1;
    height: 1px;
}

/* --- Blazor's own error strip ----------------------------------------------- */

#blazor-error-ui {
    background: #fff3cd;
    bottom: 0;
    box-shadow: 0 -1px 2px rgb(0 0 0 / 20%);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.6rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
}

/* --- Narrow screens --------------------------------------------------------- *

   The drawer stops being a column and starts sliding over the page. It is the same element
   with the same links — only its position changes — so there is never a second copy of the
   navigation to keep in step with the first.

   Opening it needs no JavaScript: the header's button is a label for a hidden checkbox.
   That matters here more than anywhere, because a static-rendered portal on a slow phone
   cannot assume a script has arrived. Following a link is a real navigation, so the drawer
   closes by itself on the next page without anything having to remember to close it. */

@media (width <= 860px) {
    .drawer {
        bottom: 0;
        left: 0;

        /* Clears the notch when a phone is held in landscape. Zero everywhere else. */
        padding-left: max(0.75rem, env(safe-area-inset-left));
        position: fixed;
        top: 0;

        /* Moved off-screen rather than hidden, so it stays in the reading order and the
           open is something the eye can follow. */
        transform: translateX(-100%);
        transition: transform 180ms ease;
        width: min(17rem, 82vw);
        z-index: 50;
    }

    .drawer-state:checked ~ .drawer {
        box-shadow: 0 0 40px rgb(20 23 28 / 35%);
        transform: translateX(0);
    }

    .drawer-open,
    .drawer-close {
        display: inline-flex;
    }

    /* The drawer has slid off-canvas and taken the lockup with it, so the header carries it. */
    .topbar .brand {
        display: flex;
    }

    /* Only hit-testable once the drawer is open, so it never swallows a tap on the page. */
    .drawer-state:checked ~ .drawer-scrim {
        background: rgb(20 23 28 / 45%);
        display: block;
        inset: 0;
        position: fixed;
        z-index: 40;
    }

    .topbar {
        padding-top: max(0.5rem, env(safe-area-inset-top));
    }

    .topbar .brand-text {
        font-size: 0.95rem;
    }

    /* The avatar already identifies the session, and the name is what pushes this row wide
       enough to wrap on a 375px screen. */
    .user-menu .user-name {
        display: none;
    }

    .user-panel {
        max-width: calc(100vw - 1.5rem);
    }

    .banner {
        font-size: 0.9rem;
        padding: 0.7rem 1rem;
    }

    .content {
        padding: 1.25rem 1rem 2rem;
    }

    h1 {
        font-size: 1.3rem;
    }

    /* A two-column definition list squeezes the value into a few characters once the
       label is a Spanish noun phrase. One column per row instead. */
    .card dl,
    .facts {
        gap: 0.1rem;
        grid-template-columns: 1fr;
    }

    .card dd,
    .facts dd {
        margin-bottom: 0.6rem;
    }

    .card {
        padding: 1rem;
    }

    .cards {
        gap: 0.75rem;
        margin-top: 1rem;
    }

    .provider-group {
        margin-top: 1.5rem;
    }

    .account-card {
        padding: 1.5rem 1.25rem;
    }

    /* Full width, because a half-width button on a phone is a smaller target for no gain. */
    .account-form .button,
    .account-form button[type="submit"] {
        width: 100%;
    }

    .recovery-codes {
        grid-template-columns: repeat(2, 1fr);
    }
}

/* Comfortable to hit with a thumb. 44px is the smallest target most accessibility
   guidance accepts, and a mis-tap on "Sign out" costs a whole sign-in. */
@media (pointer: coarse) {
    .button,
    .culture-button,
    .culture-option,

    /* A summary is a real target — it is the only way to open a disclosure — and it is not a
       button, so it inherited none of this. It measured 26px on a phone. */
    .disclosure > summary,
    input:not([type="checkbox"]):not([type="radio"]):not(.code-digit),
    select {
        min-height: 44px;
    }

    /* min-height alone leaves the text at the top of a 44px box. */
    .disclosure > summary {
        align-items: center;
        display: flex;
    }

    /* 16px exactly. Below that, mobile Safari zooms the page when a field takes focus and
       does not zoom back out — so the first tap on a login form leaves the user on a
       sideways-scrolling page they then have to pinch their way out of.

       The selector is character-for-character the one that styles these fields above. It has
       to be: that rule carries `font: inherit`, which resets the size back to the body's 15px,
       so anything weaker here loses and the trap stays open. It was already open once — the
       earlier version of this rule was outweighed by a typed selector and the sign-in form
       zoomed on every iPhone. It was open a second time when fields appeared outside
       .account-form — the pricing grid — and this rule still named only that one form.

       Keep the two selectors identical. If one changes, change both. */
    input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="file"]):not([type="range"]):not([type="color"]):not(.code-digit),
    select,
    textarea {
        font-size: 16px;
    }

    .nav-link {
        min-height: 40px;
    }

    /* The menu and account buttons are the two things every screen is reached through. */
    .icon-button {
        height: 44px;
        width: 44px;
    }

    .menu-item {
        min-height: 44px;
    }
}

/* The sign that a press was heard, while enhanced navigation fetches the next page. The browser
   draws nothing for one - it does not consider it a navigation - so a slow page is a link that
   appears not to work, and the first visit to a screen after a deployment is exactly that: the
   billing screen took ten seconds to compile once and forty milliseconds every time after.

   Above everything, including the sticky header, because it is about the whole window rather than
   anything on it. nav-progress.js draws it and takes it away; see the file for why it never
   pretends to know how far along it is. */
.nav-progress {
    background: var(--accent);
    height: 3px;
    left: 0;
    position: fixed;
    top: 0;
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 6s cubic-bezier(0.15, 0.85, 0.25, 1);
    width: 100%;
    z-index: 1000;
}

/* Most of the way and no further. The bar cannot know when the server will answer, and one that
   sits full while nothing has arrived is a worse lie than one that is still moving. */
.nav-progress-running {
    transform: scaleX(0.92);
}

/* Somebody who has asked their system for less motion gets the drawer without the slide.
   It still opens; it simply stops moving on its way in. */
@media (prefers-reduced-motion: reduce) {
    /* The bar stops crawling and simply stands there: the point is that something was heard,
       and that survives losing the animation. */
    .nav-progress {
        transition: none;
        transform: scaleX(1);
    }

    .drawer,
    .chevron,
    .button,
    a.stat,
    .data-table tbody tr {
        transition: none;
    }

    /* The lift and the press are motion too, and they are the two that fire most often. */
    a.stat:hover,
    .button:active {
        transform: none;
    }
}

.button,
.culture-button {
    white-space: nowrap;
}

/* A method choice: radio, name, and the sentence explaining what picking it means. Laid
   out as a row so the whole block is one tap target rather than a 16px circle. */
.method-option {
    align-items: flex-start;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    cursor: pointer;
    display: flex;
    gap: 0.7rem;
    margin-bottom: 0.6rem;
    padding: 0.8rem 0.9rem;
}

.method-option:has(input:checked) {
    border-color: var(--accent);
    box-shadow: inset 0 0 0 1px var(--accent);
}

.method-option input[type="radio"] {
    flex-shrink: 0;
    margin-top: 0.25rem;
}

.method-option span {
    display: block;
}

.method-option .field-hint {
    margin: 0.15rem 0 0;
}

/* The fields behind a choice on a page that cannot reveal them.

   Static rendering has no way to show a fieldset when a radio changes, and an unrendered field
   posts nothing and binds to its default — so on the pairing page every create-a-shop field is
   on screen whichever option is selected. Boxed and set back so it reads as "the details, if you
   are opening one" rather than as five more questions everybody has to answer. */
.proposal {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    margin: 0 0 0.9rem;
    padding: 0.4rem 0.9rem 0.9rem;
}

.proposal legend {
    color: var(--ink-soft);
    font-size: 0.85rem;
    font-weight: 600;
    padding: 0 0.35rem;
}

/* --- Staff administration --------------------------------------------------- */

/* Title on the left, the one action on the right — and it wraps rather than squeezing the
   button off a phone screen. */
.page-header {
    align-items: flex-start;
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    justify-content: space-between;
}

.page-header .lede {
    margin-bottom: 0;
}

/* The title and its sentence, as one flex child.

   Without this wrapper the sentence is a sibling of the heading, and the space-between above
   — which exists to put an action button opposite the title — throws it against the right
   margin, half a metre from the heading it belongs to on a wide screen. Wrapping cannot be
   left to flex-wrap either: .lede caps at 60ch, so it still fits on the heading's line and
   never wraps. */
.page-heading {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

.page-header .button {
    margin-top: 0.35rem;
}

/* The mark of the product a page is about, above its heading.

   Nexus serves several products and most of its screens are about all of them, so this is not
   the shell's identity — that is the Nexus mark in the topbar. It appears where a page belongs
   to one product and the person arrived from outside: pairing, where somebody has read a code
   off a screen in their shop and needs to recognise where they have landed.

   Height is fixed and width left to the file's own ratio, so a second product's mark can drop
   in beside this one without a rule per logo. */
.product-lockup {
    margin: 0 0 0.35rem;
}

.product-lockup img {
    display: block;
    height: 42px;
    width: auto;
}

.data-table {
    border-collapse: collapse;
    min-width: 42rem;
    width: 100%;
}

/* Still small caps, because a column heading is a label and not a sentence — but lighter, wider
   and quieter than it was. The old one was the same weight as the data underneath it, which made
   the heading row compete with the rows it describes. */
.data-table th {
    border-bottom: 1px solid var(--line);
    color: var(--ink-faint);
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    padding: 0.45rem 0.75rem 0.55rem;
    text-align: left;
    text-transform: uppercase;
}

.data-table td {
    border-bottom: 1px solid var(--line);
    padding: 0.8rem 0.75rem;
    vertical-align: top;
}

/* A row under the pointer, so the eye can hold its place across a wide table. Very faint: this
   is orientation, not selection, and anything stronger reads as "you have chosen this". */
.data-table tbody tr {
    transition: background-color 0.1s ease;
}

.data-table tbody tr:hover {
    background: color-mix(in srgb, var(--paper) 60%, transparent);
}

/* The last rule in a table draws a line under the whole thing, which then sits a few pixels
   above the card's own edge and reads as a mistake. */
.data-table tbody tr:last-child td {
    border-bottom: 0;
}

.data-table td .pill {
    display: inline-block;
    margin: 0 0.25rem 0.2rem 0;
}

.data-table .muted {
    font-size: 0.85rem;
}

/* Whose move it is, on the support queue. Deliberately not muted when it is ours: the queue is
   sorted to put these first and the row has to say why. Still a second line under the status,
   so it keeps the muted size and does not compete with the subject. */
.data-table .waiting-on-us {
    color: var(--ink);
    font-size: 0.85rem;
    font-weight: 600;
}

/* Six columns of short facts. Left alone the browser gave most of the width to the subject and
   wrapped everything else — a two-word heading over two lines, a timestamp over two, a state
   over three — so rows were tall, uneven and slow to scan. Only the subject wraps now; the
   table already scrolls sideways rather than squeezing, so nothing is lost on a narrow screen. */
.support-queue th,
.support-queue time,
.support-queue td .pill,
.support-queue .waiting-on-us {
    white-space: nowrap;
}

.support-queue td:first-child {
    min-width: 13rem;
}

/* A deactivated account is still listed — that is the point of not deleting it — but it
   should not read as one of the live ones. */
.row-muted td {
    opacity: 0.6;
}

/* --- Audit trail ------------------------------------------------------------ */

/* Filters on one line, wrapping rather than squeezing on a phone. */
.filter-bar {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-bottom: 1.25rem;
}

/*
    Two or more small forms, each one label + field + button, stacked as separate decisions.

    Not `.filter-bar`: those are one form whose parts belong together, and these are alternatives
    that must not read as a single row of choices. Release and Revoke sitting flush against each
    other is how somebody ends a shop's licence meaning to free a till.

    `align-items: end` rather than the default: each form is as tall as its own label, and
    stretching would leave the buttons at different heights beside their inputs.
*/
.action-forms {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
    margin-top: 0.75rem;
}

.action-forms > form {
    align-items: end;
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
}

.action-forms label {
    margin: 0;
}

.action-forms input {
    flex: 1 1 16rem;
    min-width: 12rem;
}

/* Nothing about the look, on purpose. It comes from the shared control rules, which is also
   what keeps the select and the search box the same height as each other — they sit side by
   side, and two paddings that differ by a hair show up as a step in the row.

   It matters twice over for the select: `background: var(--surface)` is a shorthand that
   resets background-image, and background-image is where the chevron lives. With
   `appearance: none` already applied that would leave a dropdown with no arrow at all. */

/* A search box carries a placeholder naming what it searches, and an unstyled input is
   about twenty characters wide — narrow enough to cut that placeholder in half and leave
   somebody guessing which columns the box actually looks at. Grows to fill the row and
   wraps onto its own line before it shrinks below what the hint needs. */
.filter-bar input[type="search"] {
    flex: 1 1 18rem;
    min-width: 14rem;
}

/* A refused attempt reads differently from a successful one at a glance. Tinted rather
   than red text, because a page of red is a page nobody can scan. */
.row-refused td {
    background: var(--bad-soft);
}

/* A "Details" toggle that opens a row's JSON in place.

   The detail used to sit open in its column, where a JSON object wrapped to roughly one
   character per line and dragged every neighbouring row's height with it. Closed, this is
   one word wide; open, it is indented and readable. No script and no dialog — the query
   string on that page already carries the filters and the cursor, so a `:target` dialog
   would have to preserve them to open and again to close. */
.disclosure > summary {
    border-radius: var(--radius);
    color: var(--accent);
    cursor: pointer;
    display: inline-block;
    font-size: 0.85rem;
    font-weight: 600;
    list-style: none;
    padding: 0.15rem 0.35rem;
    user-select: none;
}

.disclosure > summary::-webkit-details-marker {
    display: none;
}

.disclosure > summary:hover {
    background: var(--paper);
}

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

/* Raw JSON, indented for reading. Capped and scrolled inside itself: a detail should be
   legible without turning one entry into half the screen.

   The min-width is what makes it readable at all. A table column is sized by its content, and
   closed this cell holds one word — so the open panel inherited about 120px and wrapped
   `"before": "Basic/2027-08-13"` over four lines, which is the problem this disclosure was
   built to solve. Opening one widens the column instead; the table already sits in .scroll-x,
   so the overflow scrolls rather than breaking the page.

   Only while the table is a table. Below 860px each row becomes a card the full width of the
   screen, where there is no column to force wider and 22rem is simply wider than the phone. */
.audit-detail {
    background: var(--paper);
    border-radius: var(--radius);
    display: block;
    font-size: 0.78rem;
    margin-top: 0.4rem;
    max-height: 18rem;
    max-width: 32rem;
    overflow: auto;
    overflow-wrap: anywhere;
    padding: 0.6rem 0.75rem;
    white-space: pre-wrap;
}

@media (width > 860px) {
    .audit-detail {
        min-width: 22rem;
    }
}

/* A sent message's body, shown verbatim on the messaging screen.

   Capped in height and scrolled inside itself, because a marketing email is longer than a
   screen and the diagnostic facts above it are what somebody opened this page for.

   Wrapped rather than scrolled sideways: an HTML body has no line breaks worth honouring, and
   a horizontal scrollbar under it helps nobody read it. */
.message-body {
    background: var(--paper);
    border-radius: var(--radius);
    font-size: 0.8rem;
    margin: 0.6rem 0 0;
    max-height: 22rem;
    overflow-y: auto;
    overflow-wrap: anywhere;
    padding: 0.7rem 0.9rem;
    white-space: pre-wrap;
}

/* A small removable token: a country a payment gateway is restricted to, an "add" affordance
   that reads as one of the tokens beside it. Laid out as a list so a screen reader announces
   how many there are, which is the fact somebody is checking. */
.chip-list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    list-style: none;
    margin: 0.5rem 0 1rem;
    padding: 0;
}

.chip {
    align-items: center;
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: 999px;
    display: inline-flex;
    font-size: 0.85rem;
    gap: 0.4rem;
    padding: 0.25rem 0.7rem;
    text-decoration: none;
}

/* The one you are looking at.

   The market chips on the pricing page carried this class and nothing styled it, so Colombia,
   Worldwide and "add a market" were three identical pills and the only mark of which list was on
   screen was aria-current - which a sighted reader never meets. The prices changed under a bar
   that looked the same either way.

   Tinted rather than filled: these sit in a row that includes an "add" chip, and a solid pill
   beside it would read as a button rather than as the current choice. */
.chip-active {
    background: color-mix(in srgb, var(--accent) 10%, var(--paper));
    border-color: var(--accent);
    color: var(--accent-dark);
    font-weight: 600;
}

/* 44px of tappable area around a small glyph, without making the chip itself that tall.

   `.chip-remove` is a <button> rather than a link, because removing a country from a
   package being edited posts the form back rather than navigating — the typed values
   have to survive, and a link would throw them away. It is listed here rather than
   given a look of its own so there is one definition of what the control looks like. */
.chip a,
.chip .chip-remove {
    align-items: center;
    color: var(--ink-faint);
    display: inline-flex;
    font-size: 1rem;
    justify-content: center;
    line-height: 1;
    margin: -0.6rem -0.4rem -0.6rem 0;
    min-height: 44px;
    min-width: 32px;
    text-decoration: none;
}

.chip a:hover,
.chip .chip-remove:hover {
    color: var(--bad);
}

/* The button reset the link does not need. */
.chip .chip-remove {
    background: none;
    border: 0;
    cursor: pointer;
    font-family: inherit;
    padding: 0;
}

/* --- Charts ------------------------------------------------------------------

   Column charts drawn as inline SVG on the server. No library: both portals render statically,
   so anything that draws in the browser would need script on a page that has none. */
.chart {
    margin: 0 0 1.5rem;
}

.chart-caption {
    font-weight: 600;
    margin-bottom: 0.35rem;
}

.chart-legend {
    display: flex;
    flex-wrap: wrap;
    font-size: 0.85rem;
    gap: 0.25rem 1rem;
    margin-bottom: 0.5rem;
}

.chart-key {
    align-items: center;
    display: inline-flex;
    gap: 0.4rem;
}

.chart-swatch {
    border-radius: 3px;
    display: inline-block;
    height: 0.7rem;
    width: 0.7rem;
}

/* A year of months squeezed into a phone puts the labels at about seven pixels. It keeps its
   width and scrolls inside .scroll-x instead, the way every wide table here already does. */
/* No aspect-ratio here: the viewBox already gives the SVG its intrinsic ratio, and a number
   written down would be right for twelve columns and wrong for any other count. */
.chart-canvas {
    display: block;
    height: auto;

    /* A ceiling, because height: auto takes its shape from the viewBox and a caller with very
       few columns draws a box that is taller than it is wide — which across a wide card becomes a
       column some thousands of pixels high. TrendChart floors its column count so this should
       never be reached; it is here so that the next component to get the arithmetic wrong is a
       chart with odd proportions rather than a page nobody can scroll past. */
    max-height: 22rem;

    min-width: 30rem;
    width: 100%;
}

.chart-baseline {
    stroke: var(--line);
    stroke-width: 1;
}

/* The ceiling as a line, with the number underneath. Without one, a column of eleven and a
   column of eleven thousand draw exactly the same picture. */
.chart-rule {
    stroke: var(--line);
    stroke-dasharray: 3 3;
    stroke-width: 1;
}

.chart-label {
    fill: var(--ink-faint);
    font-family: inherit;
    font-size: 11px;
}

.chart-scale {
    color: var(--ink-faint);
    font-size: 0.8rem;
    margin: 0.3rem 0 0;
}

.bar {
    rx: 2;
}

.bar-accent {
    background: var(--accent);
    fill: var(--accent);
}

.bar-maui {
    background: var(--maui);
    fill: var(--maui);
}

.bar-good {
    background: var(--good);
    fill: var(--good);
}

/* Ranked bars lying on their side, for the charts whose labels are names rather than hours.
   Plain elements: a horizontal bar is a box of a given width, and an SVG would fix a coordinate
   system that stops the names wrapping. */
.rank-list {
    display: grid;
    gap: 0.55rem;
    list-style: none;
    margin: 0;
    padding: 0;
}

/* Name, bar, figure. The bar takes what is left, so a long name pushes the track rather than
   truncating itself — the whole reason these are not columns. */
.rank-row {
    align-items: center;
    display: grid;
    gap: 0.25rem 0.75rem;
    grid-template-columns: minmax(6rem, 11rem) 1fr auto;
}

.rank-label {
    font-size: 0.9rem;
    overflow-wrap: anywhere;
}

.rank-track {
    background: var(--paper);
    border-radius: 3px;
    display: block;
    height: 0.7rem;
    overflow: hidden;
    width: 100%;
}

.rank-fill {
    border-radius: 3px;
    display: block;
    height: 100%;

    /* A row at nought still shows where it would start. An absent bar reads as a missing row. */
    min-width: 2px;
}

.rank-value {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    text-align: right;
    white-space: nowrap;
}

/* On a phone the three columns become two lines: the name over the bar, the figure beside it.
   Squeezed into 375 pixels the track is otherwise about forty wide, which draws every row the
   same length. */
@media (max-width: 40rem) {
    .rank-row {
        grid-template-columns: 1fr auto;
    }

    .rank-label {
        grid-column: 1 / -1;
    }
}

/* --- Pager -------------------------------------------------------------------

   Under every list: how many rows there are, how many to show at once, and which page.

   Three groups on one row, wrapping to three rows on a phone rather than shrinking. The order
   is the order the questions are asked in — how many are there, how many do I want, where am I
   — and it is the same on every screen so the controls are muscle memory rather than a hunt.

   Every control is an anchor. Both portals render statically, so a select that navigated would
   need script and a GET form would drop the filters already in the address. */
.pager {
    align-items: center;
    border-top: 1px solid var(--line);
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 1.25rem;
    justify-content: space-between;
    margin-top: 1.25rem;
    padding-top: 0.9rem;
}

.pager-range {
    color: var(--ink-faint);
    font-size: 0.85rem;
    margin: 0;
}

.pager-sizes,
.pager-steps {
    align-items: center;
    display: flex;
    gap: 0.3rem;
}

.pager-legend {
    color: var(--ink-faint);
    font-size: 0.85rem;
    margin-right: 0.2rem;
}

/* 44px so a size can be tapped on a phone, which is where the choice matters most — the
   shared control rules cover fields, and these are links. */
.pager-size,
.pager-step {
    align-items: center;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    display: inline-flex;
    color: var(--ink);
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
    padding: 0 0.6rem;
    text-decoration: none;
}

.pager-size:hover,
.pager-step:hover {
    background: var(--paper);
}

.pager-size-active {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
    font-weight: 600;
}

/* Kept in place rather than removed at the ends of the list. A control that disappears moves
   the one beside it under the cursor, so "next" ends up where "previous" just was. */
.pager-step-off {
    color: var(--ink-faint);
    cursor: default;
    opacity: 0.45;
}

.pager-step-off:hover {
    background: none;
}

.pager-position {
    font-size: 0.85rem;
    padding: 0 0.5rem;
    white-space: nowrap;
}

/* The 16px and the 44px target now come from the shared control rules, which cover every
   field rather than the two this bar happens to hold. */

/* A role, its permission count and the sentence explaining it. Laid out as a row so the
   whole block is the tap target rather than a 16px box. */
.role-picker {
    border: 0;
    margin: 0.7rem 0 0;
    padding: 0;
}

.role-picker legend {
    font-weight: 600;
    padding: 0;
}

/*
    A platform and who delivers it, read as one line. The tick keeps its own width so the
    pickers line up down the column — a select that starts wherever the platform name happens
    to end makes five rows look like five different questions.
*/
.lane-row {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    margin-top: 0.35rem;
}

.lane-row .checkbox-row {
    flex: 0 0 11rem;
    margin: 0;
}

.lane-row select {
    flex: 1 1 12rem;
    min-width: 0;
}

.check {
    align-items: flex-start;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    cursor: pointer;
    display: flex;
    font-weight: 400 !important;
    gap: 0.7rem;
    margin-top: 0.5rem !important;
    padding: 0.7rem 0.9rem;
}

.check:has(input:checked) {
    border-color: var(--accent);
    box-shadow: inset 0 0 0 1px var(--accent);
}

/* Disabled and visible, rather than hidden. A missing role looks like a bug; a greyed one
   carrying its reason is the rule doing its job in public. */
.check-disabled {
    background: var(--paper);
    cursor: not-allowed;
    opacity: 0.65;
}

.check input[type="checkbox"] {
    flex-shrink: 0;
    margin-top: 0.3rem;
}

.check > span {
    display: flex;
    flex-direction: column;
}

.check .muted {
    font-size: 0.85rem;
}

.permission-grid {
    display: grid;
    gap: 1.25rem;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}

.permission-grid h3 {
    font-size: 0.9rem;
    margin: 0 0 0.35rem;
}

/* The role editor puts a fieldset per module inside the same grid the read-only view fills
   with headings, so the legend has to read as the same heading — otherwise switching between
   "what this role carries" and "what it should carry" looks like two different pages. */
.permission-grid > .role-picker {
    margin: 0;
    min-width: 0;
}

.permission-grid > .role-picker legend {
    font-size: 0.9rem;
    margin-bottom: 0.35rem;
}

/* A checkbox per permission is a lot of them, so they sit closer together here than in the
   role picker on the invite page, where there are eight. */
.permission-grid .check {
    margin-top: 0.35rem !important;
    padding: 0.5rem 0.7rem;
}

/* Room for a name, an address and possibly a badge — the members list is people, not tags. */
.role-members li {
    padding: 0.35rem 0;
}

.plain {
    list-style: none;
    margin: 0;
    padding: 0;
}

.plain li {
    color: var(--ink-soft);
    font-size: 0.88rem;
    padding: 0.1rem 0;
}

/* Each button is its own form — static rendering has no other way to post — so they are
   laid out as a row rather than stacking as four full-width blocks. */
.action-row {
    /* Without this the row stretches its children, and a neighbour of the primary button
       grows to cover that button's top margin as well: a Cancel link 60px tall beside a
       41px Save, starting 19px higher. */
    align-items: center;

    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
}

/* .button-primary carries a top margin so a lone submit sits clear of the last field of a
   column form. In a row of buttons there is no field above it and the margin only pushes it
   out of line with everything beside it — so it is zeroed on the button and put back on the row,
   which is what actually needs clearing.

   Without the margin on the row the buttons sit flush against the box above them. The intent was
   always there; zeroing it on the button lost it for every form in the portal at once. */
.action-row {
    margin-top: 1.2rem;
}

.action-row .button-primary {
    margin-top: 0;
}

.action-row form {
    margin: 0;
}

.button-danger {
    border-color: #e9c3c5;
    color: var(--bad);
}

.button-danger:hover {
    background: var(--bad-soft);
    border-color: var(--bad);
}

/* An invitation link, shown once. Wraps rather than scrolls: it is meant to be selected and
   copied, and a value hiding past the right edge is one somebody copies half of. */
.one-time-value {
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    font-size: 0.85rem;
    margin: 0.5rem 0;
    overflow-wrap: anywhere;
    padding: 0.7rem 0.9rem;
    white-space: pre-wrap;
}

.notice-warn {
    background: var(--warn-soft);
    color: var(--warn);
}

@media (width <= 860px) {
    /* The table stops being a table. Each row becomes a card and each cell carries its own
       heading from data-label — a 42rem grid on a 375px screen is unreadable however well
       it scrolls. */
    .data-table {
        min-width: 0;
    }

    .data-table thead {
        display: none;
    }

    .data-table tr {
        background: var(--surface);
        border: 1px solid var(--line);
        border-radius: var(--radius);
        display: block;
        margin-bottom: 0.75rem;
        padding: 0.6rem 0.8rem;
    }

    .data-table td {
        border: 0;
        display: grid;
        gap: 0.15rem 0.75rem;
        grid-template-columns: 8rem 1fr;

        /* `anywhere` rather than `break-word`, because only `anywhere` counts when the browser
           works out the smallest the cell can be. Without it one long unbreakable token sets the
           floor for the whole card: `someone@example.com` measured 250px, the 8rem label column
           added 128, and every card on the audit trail came out 394px wide on a 375px screen — a
           stack of cards that scrolled sideways, which is precisely what becoming a stack of cards
           was meant to avoid. */
        overflow-wrap: anywhere;
        padding: 0.25rem 0;
    }

    .data-table td::before {
        color: var(--ink-faint);
        content: attr(data-label);
        font-size: 0.78rem;
        font-weight: 600;
        text-transform: uppercase;
    }

    .page-header .button {
        width: 100%;
    }

    .permission-grid {
        gap: 0.9rem;
        grid-template-columns: 1fr;
    }

    .action-row {
        flex-direction: column;
    }

    .action-row .button {
        width: 100%;
    }
}


/* --- Entitlement picker -----------------------------------------------------

   The licence screen's list of features and add-ons. A fieldset rather than a
   table: every row is a control, and a table of controls reads to a screen
   reader as data somebody might sort.

   Laid out with flex-wrap so a narrow phone stacks the quantity box under the
   label instead of squeezing both onto one line — this sits after the mobile
   media query, so it needs no breakpoint of its own. */

.entitlement-picker {
    border: 1px solid var(--nexus-border);
    border-radius: var(--nexus-radius);
    padding: 0.85rem 1rem 1rem;
    margin: 0.35rem 0 1.1rem;
}

.entitlement-picker legend {
    font-weight: 600;
    padding: 0 0.4rem;
}

.entitlement-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem;
    padding: 0.4rem 0;
    border-bottom: 1px solid var(--nexus-border-subtle, var(--nexus-border));
}

.entitlement-row:last-child {
    border-bottom: 0;
}

.entitlement-row label {
    flex: 1 1 14rem;
    margin: 0;
    display: flex;
    align-items: center;
    gap: 0.35rem;
    font-weight: 400;
}

/* Narrow, because it holds a small count and a full-width box would read as the
   most important thing in the row. Never below 16px: anything smaller makes iOS
   zoom the page when it is focused. */
.entitlement-row input[type="number"] {
    width: 6rem;
    font-size: 1rem;
}

/* A single tick and its sentence, on one line.

   Three screens named this class before anything defined it — the pricing dialog,
   the licence tab and the market editor — so a tick option inherited
   `.account-form label`'s 600 weight and read as a field heading with a box
   stuck to it. The weight is the whole point of the fix: a choice is not a
   label. */
.checkbox-row {
    align-items: center;
    cursor: pointer;
    display: flex;
    font-weight: 400;
    gap: 0.45rem;
}

/* Full width belongs to text boxes, not to a 16px tick that would stretch across
   the card. Scoped to the row so `.account-form input:not([type="checkbox"])`
   above is untouched. */
.checkbox-row input[type="checkbox"] {
    flex: 0 0 auto;
    margin: 0;
}


/* --- Dialog ----------------------------------------------------------------

   Opened by the address rather than by script: the server renders the one dialog the query string
   names and marks it `is-open`. Everything good about `:target` survives — the open dialog is part
   of the URL, it can be sent to a colleague, and it works with JavaScript blocked — and it fixes
   what `:target` cost. Rendering every dialog at once meant rendering every dialog's form at once,
   and a page cannot post one of many identically shaped forms and still know which model to bind.
   Nothing on that screen could be saved.

   The cost is that it cannot trap focus or close on Escape the way a real
   <dialog> does. Acceptable here because every dialog is a form somebody
   deliberately opened, and the backdrop is a link so a click outside still
   closes it. */

.modal {
    display: none;
}

.modal.is-open {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 100;
    overflow-y: auto;
    padding: 2rem 1rem;
}

/* A link, not a div, so closing works without script and reads to a screen
   reader as something activatable. */
.modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
    display: block;
}

.modal-panel {
    position: relative;
    max-width: 46rem;
    margin: 0 auto;
    background: var(--nexus-surface, #fff);
    color: inherit;
    border-radius: var(--nexus-radius);
    box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.3);
    padding: 1.25rem 1.5rem 1.5rem;
}

/* Somebody's typed answer, shown back to the person it was written for.

   `pre-wrap` because they pressed return where they meant a new line, and collapsing that turns a
   short list of what is wrong into one run-on sentence. Bounded width because prose set to the
   full width of a dialog is hard to read back. */
.review-answer {
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    margin: 0.35rem 0 0;
    max-width: 60ch;
    padding: 0.8rem 0.9rem;
    white-space: pre-wrap;
}

/* A document is read at the size it was written, not at form width. Still capped so it cannot
   grow past the viewport on a wide screen, and still the ordinary panel underneath. */
.modal-panel-wide {
    max-width: min(64rem, 95vw);
}

/* Tall enough to read a page of a tax registration without scrolling the frame twice, and
   bounded by the viewport so the dialog never grows past the screen it sits on. */
.file-preview-frame {
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    display: block;
    height: min(70vh, 46rem);
    width: 100%;
}

/* Fits the panel, keeps its shape, and never sets the dialog's width — a photographed document
   is whatever size the telephone made it. */
.file-preview-image {
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    display: block;
    margin: 0 auto;
    max-height: min(70vh, 46rem);
    max-width: 100%;
    object-fit: contain;
}

.modal-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    margin-bottom: 0.75rem;
}

.modal-header h2 {
    margin: 0;
}

.modal-close {
    font-size: 1.6rem;
    line-height: 1;
    text-decoration: none;

    /* A comfortable target on a phone. A 16px × in the corner of a dialog is
       the control people miss and then close the wrong thing. */
    padding: 0.25rem 0.6rem;
}

/* Header with an action on the right — a card title plus its "add" button. */
.card-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}

.card-header h2 {
    margin: 0;
}

.entitlement-row .entitlement-name {
    flex: 1 1 14rem;
    margin: 0;
    font-weight: 400;
}

@media (max-width: 40rem) {
    .modal.is-open {
        padding: 0;
    }

    .modal-panel {
        min-height: 100%;
        border-radius: 0;
    }
}


/* --- Stat tiles -------------------------------------------------------------

   A row of counts above a list: how many, how many are wrong, how many need
   somebody today. Each one is a filter, so they are anchors rather than
   figures — the number and the thing it selects are the same control, and a
   count you cannot click is a count you then have to go and reproduce.

   The counts describe the whole estate, never the current filter. "Lapsed 3"
   that becomes "Lapsed 3 of 3" once clicked tells nobody anything. */

.stat-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin: 0 0 1.25rem;
}

.stat {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    color: inherit;
    display: flex;
    flex: 1 1 8rem;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 7rem;
    padding: 0.85rem 1rem;
    text-decoration: none;
}

/* Only the ones that are links. A plain span in the row is information, not a
   control, and lifting it on hover would promise a click that does nothing.

   One pixel, and no extra shadow: enough to answer "is this clickable", not enough to make a
   row of six tiles jump about as the pointer crosses them. */
a.stat {
    transition: border-color 0.12s ease, transform 0.12s ease;
}

a.stat:hover {
    border-color: var(--accent);
    transform: translateY(-1px);
}

/* Both shadows, because box-shadow is one property. The inset ring alone replaces the
   elevation, so the selected tile was the one tile sitting flat on the page — the opposite of
   what selecting it should look like, and only visible when a tile happens to be active. */
.stat-active {
    border-color: var(--accent);
    box-shadow: inset 0 0 0 1px var(--accent), var(--shadow);
}

.stat-value {
    font-size: 1.5rem;
    font-weight: 600;
    line-height: 1.1;
}

.stat-label {
    color: var(--ink-faint);
    font-size: 0.8125rem;
}

/* Colour carries meaning here, so it never carries it alone — every tile is
   also labelled, and the warning ones sit next to their own word. */
.stat-warn {
    color: var(--warn);
}

.stat-good {
    color: var(--good);
}

/* The third state, and the reason it was worth adding one: on the operations page "late" and
   "not happening" are different sentences and somebody has to act on only one of them. Warn
   for both would have made the distinction live entirely in the words under the figure. */
.stat-bad {
    color: var(--bad);
}

/* Two or three links in the last column of a table row. Wrapped rather than
   inline so a narrow screen stacks them instead of pushing the table wider,
   and spaced so neither is hit by accident on a phone. */
.row-links {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    white-space: nowrap;
}

/* --- Totals ------------------------------------------------------------------

   What came in, per currency. Never one figure: prices are published per market
   and nothing is converted, so a single "revenue" number would be a rate
   somebody invented. */

.totals {
    display: flex;
    flex-wrap: wrap;
    gap: 1.25rem;
    margin: 0 0 1.25rem;
}

.total-amount {
    font-size: 1.25rem;
    font-weight: 600;
}


/* --- Choosing a file --------------------------------------------------------

   A bare <input type="file"> renders as the browser's own control: a small grey button whose label
   the page cannot set, whose width the page cannot set, and which reads "Sin archivos
   seleccionados" beside a form written in the house voice.

   The input is still the input — it is moved out of sight rather than hidden,
   so it keeps its place in the tab order and still announces itself. The label
   is the visible control: clicking a <label> activates the input it is bound
   to, which is plain HTML and needs no script.

   Not `display: none` and not `visibility: hidden`. Both take the control out
   of the accessibility tree entirely, and a required field a screen reader
   cannot reach is a form nobody can finish. */
.file-field {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
}

.file-field input[type="file"] {
    height: 1px;
    opacity: 0;
    overflow: hidden;
    position: absolute;
    width: 1px;
}

/* `.account-form label` is a class plus an element, so it outranks a bare class — and it
   sets the block display every other field caption wants. The button has to be named
   through the form to win, or it renders full width on its own line. */
.file-field-button,
.account-form .file-field-button {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--ink);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    font: inherit;
    font-weight: 600;
    padding: 0.5rem 0.9rem;
}

.file-field-button:hover,
.account-form .file-field-button:hover {
    border-color: #cfd6e0;
}

/* The keyboard lands on the input, which is invisible — so the ring has to be
   drawn on the label instead, or a person tabbing through sees nothing move. */
.file-field input[type="file"]:focus-visible + .file-field-button {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* What is chosen, once something is. Empty until then rather than "no file
   selected": a form that starts by telling you what you have not done yet is
   noise, and the button beside it already says what to do. */
.file-field-name {
    color: var(--muted);
    overflow-wrap: anywhere;
}

/* --- Unread ------------------------------------------------------------------

   Both say the same thing in the two places somebody looks: the drawer, from
   anywhere in the portal, and the row, once they are in the list. Red rather
   than the accent, because every other coloured thing on these screens is a
   state and this one is an instruction. */
.nav-badge {
    background: var(--bad);
    border-radius: 999px;
    color: #fff;
    font-size: 0.7rem;
    font-weight: 700;
    line-height: 1;

    /* Pushed to the end of the row rather than sitting against the label. */
    margin-left: auto;
    min-width: 1.25rem;
    padding: 0.2rem 0.4rem;
    text-align: center;
}

/* The chime's switch, in the header beside whatever else the page offers. A
   form rather than a link because it writes a cookie, and inline so it sits in
   the row instead of breaking it. */
.sound-toggle {
    display: inline-flex;
}

.sound-toggle .button {
    align-items: center;
    display: inline-flex;
    gap: 0.35rem;
}

.row-unread {
    color: var(--bad);
    display: inline-flex;
    margin-left: 0.4rem;
    vertical-align: middle;
}

/* --- The channel a conversation is on ----------------------------------------

   One inbox holds WhatsApp, Messenger, Instagram and the portal, so every row has to say which
   at a glance. Colour does that faster than a word, and each of these is the platform's own -
   near enough to be recognised, and drawn here rather than fetched, because a brand's logo file
   is somebody else's licence and this portal loads nothing from a content network.

   The icon is the whole cell in a list. The name is always in the markup for whoever is not
   looking at colour at all, and only on the screen where there is room for it. */

.channel-badge {
    align-items: center;
    color: var(--channel-ink, var(--ink-soft));
    display: inline-flex;
    gap: 0.35rem;
    vertical-align: middle;
}

.channel-whatsapp {
    --channel-ink: #1fa855;
}

.channel-messenger {
    --channel-ink: #0084ff;
}

.channel-instagram {
    --channel-ink: #c9308e;
}

/* The portal's own conversations, which are most of them. Deliberately the quiet one: a colour
   here would put three loud icons on a queue where one channel is the ordinary case. */
.channel-nexus {
    --channel-ink: var(--ink-faint);
}

/* A column holding nothing but an icon. Narrow, centred, and no header text - the heading is
   there for a screen reader and would otherwise set the width. */
.data-table .column-icon {
    text-align: center;
    width: 2.5rem;
}

/* --- Support threads ---------------------------------------------------------

   A conversation between a customer and Draf IT, drawn the way everybody in Latin America already
   reads one.

   "Ours" is whoever is reading, and that flips between the two portals. Our
   reply is on the right in the staff queue and on the left in the customer's
   portal, because in both of them the person reading is on the right. So these
   rules never mention Draf IT or the customer - each page decides which class a
   message wears, and the stylesheet only knows "mine" and "theirs".

   The alternative was one colour per party, the same in both applications, which
   is what the first version did: it read as a table of statements rather than a
   conversation, and nobody could tell at a glance whether they were owed a
   reply. */

/* --- The conversation owns the window ----------------------------------------

   on a thread page the window itself stops scrolling and the messages scroll instead - the header
   and the composer stay put, which is what makes it read as a chat rather than as a long document
   with a text box at the end.

   Scoped with :has() so nothing else changes. The alternative was a modifier
   class on the layout, which would mean every page that ever wants this has to
   reach up into the shell and set it - and the one that forgot to unset it would
   trap some unrelated screen at viewport height with no way to scroll.

   `dvh` rather than `vh` because a phone's address bar hides as you scroll, and
   `vh` is measured against the taller state: the composer would sit just below
   the fold until the reader scrolled a page that no longer scrolls. */
.shell:has(.thread-page) {
    height: 100dvh;
    min-height: 0;
    overflow: hidden;
}

/* Both need it. A flex child will not shrink below its content without it, so
   the chain from the shell down to the message list would refuse to fit and the
   composer would be pushed off the bottom - the exact thing this is for. */
.shell:has(.thread-page) .shell-main,
.shell:has(.thread-page) .content {
    min-height: 0;
}

.shell:has(.thread-page) .content {
    display: flex;

    /* Grow, not just shrink. `.content` is a flex child of the shell and its
       default is `flex: 0 1 auto`, so without this it is only as tall as what is
       in it: a long conversation filled the window because it overflowed, and a
       two-message one left the composer stranded halfway up the screen. */
    flex: 1 1 auto;
    flex-direction: column;
    overflow: hidden;

    /* No bottom gutter: the panel reaches the edge of the window the way a chat
       does, and 3rem of empty paper below a fixed composer is only wasted room. */
    padding-bottom: 1.25rem;
}

.thread-page {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    min-height: 0;

    /* The message list carries the scroll, so the panel's own padding would put
       a gutter between the frame and the messages that scrolled away with them.
       Each part below pads itself instead. */
    overflow: hidden;
    padding: 0;
    position: relative;
}

/* They were: the standing row and the resolve and close buttons sat on the panel's own padding with
   nothing between them and the first message. */
.thread-page .card-header {
    align-items: center;
    border-bottom: 1px solid var(--line);
    flex: 0 0 auto;
    padding: 0.9rem 1.25rem;
    row-gap: 0.6rem;
}

.thread {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    list-style: none;
    margin: 0;
    min-height: 0;
    overflow-y: auto;
    padding: 1rem 1.25rem 1.25rem;

    /* Smooth for the jump button. The first scroll of a page is done from script
       with `instant`, because a page that visibly races to the bottom on open
       reads as a bug rather than as a position. */
    scroll-behavior: smooth;
}

/* Newest at the bottom, with a short conversation resting there rather than
   floating at the top of an empty panel.

   A zero-height first flex item with `margin-top: auto`, not
   `justify-content: flex-end` - that pushes content past the top of a scroll
   container in several browsers and the oldest messages become unreachable. An
   auto margin collapses to nothing as soon as the content overflows, so it only
   acts while there is room to spare. */
.thread::before {
    content: "";
    margin-top: auto;
}

.thread-page .thread {
    flex: 1 1 auto;
}

/* --- The backdrop ------------------------------------------------------------

   chat-backdrop.svg is ours: 47 glyphs drawn from Draf IT's own subject matter -
   tills and terminals, laptops and phones, receipts and barcodes, messaging,
   rockets and robots - scattered over a jittered grid so it repeats without
   reading as one. Every glyph stays inside its own cell, which is what lets a
   640px tile repeat with no seam.

   The wash underneath is two very faint radial tints of the brand's own two
   colours, the blue of the mark into the purple. Kept this light on purpose: it
   is behind somebody's words, and a background a reader notices is a background
   competing with the thing they came to read. */
.thread-page .thread {
    background-color: #f7f7fd;
    background-image:
        url("chat-backdrop.svg"),
        radial-gradient(
            ellipse at 15% 0%,
            color-mix(in srgb, var(--accent) 7%, transparent) 0%,
            transparent 55%),
        radial-gradient(
            ellipse at 85% 100%,
            color-mix(in srgb, var(--maui) 8%, transparent) 0%,
            transparent 55%);

    /* The tile repeats; the two washes do not, or their soft edges would show up
       as bands wherever they met. */
    background-repeat: repeat, no-repeat, no-repeat;
    background-attachment: local, local, local;
}

/* The pattern runs behind them, so both need their own ground rather than the
   panel's. The bubbles already had one; the day pill was relying on the plain
   surface being plain. */
.thread-page .thread-day {
    backdrop-filter: blur(2px);
    background: color-mix(in srgb, var(--surface) 82%, transparent);
}

.thread-page .thread-theirs .thread-bubble {
    background: var(--surface);
}


/* The day a run of messages happened, across the middle. Chats have taught
   everybody to read this, and without it a thread that ran over three weeks
   looks like it happened in an afternoon. */
.thread-day {
    align-self: center;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 999px;
    color: var(--ink-faint);
    font-size: 0.75rem;
    margin: 0.6rem 0 0.4rem;
    padding: 0.15rem 0.7rem;
}

.thread-day:first-child {
    margin-top: 0;
}

.thread-message {
    display: flex;
    flex-direction: column;
    max-width: min(46rem, 78%);
}

/* Whoever is reading this screen wrote it. */
.thread-ours {
    align-self: flex-end;
}

.thread-theirs {
    align-self: flex-start;
}

/* The name, above the first message of a run only. Repeating it on every line is
   what made the old version read as a list of signed statements. */
.thread-author {
    color: var(--ink-faint);
    font-size: 0.78rem;
    font-weight: 600;
    margin: 0.5rem 0 0.15rem;
    padding: 0 0.35rem;
}

.thread-bubble {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 14px;
    padding: 0.5rem 0.7rem 0.35rem;
}

/* Ours is tinted, theirs is plain. A tint rather than the solid accent: the
   text inside is a customer's own words and has to stay at full contrast, which
   white-on-blue at this size does not reliably manage. */
.thread-ours .thread-bubble {
    background: color-mix(in srgb, var(--accent) 10%, var(--surface));
    border-color: color-mix(in srgb, var(--accent) 22%, transparent);
}

/* The tail, on the last bubble of a run - once, the way a chat does it, rather
   than on every line of a run of four. */
.thread-ours .thread-end .thread-bubble {
    border-bottom-right-radius: 4px;
}

.thread-theirs .thread-end .thread-bubble {
    border-bottom-left-radius: 4px;
}

/* The line breaks somebody typed are kept, rather than turned into markup. What
   is in here is a customer's own text and is rendered as text - the escaping is
   the whole defence, and `white-space` is how it still reads like what they
   wrote. `overflow-wrap` is for the log line pasted in with no spaces in it. */
.thread-body {
    margin: 0;
    overflow-wrap: anywhere;
    white-space: pre-wrap;
}

.thread-when {
    color: var(--ink-faint);
    display: block;
    font-size: 0.7rem;
    margin-top: 0.15rem;
    text-align: right;
}

/* --- The note, which is not part of the conversation -------------------------

   a note is deliberately not a bubble and does not take a side. It runs the full width, down the
   middle, warned - because it is the message that must not be sent by mistake, and a note that
   inherited our own colour would read on a glance as something we had said to the customer. */
.thread-note {
    align-self: stretch;
    max-width: none;
}

.thread-note .thread-bubble {
    background: var(--warn-soft);
    border: 1px dashed color-mix(in srgb, var(--warn) 45%, transparent);
    border-radius: var(--radius);
}

.thread-note .thread-author {
    align-items: center;
    color: var(--warn);
    display: flex;
    gap: 0.4rem;
}

.thread-note-badge {
    background: var(--warn);
    border-radius: 999px;
    color: #fff;
    font-size: 0.68rem;
    letter-spacing: 0.04em;
    padding: 0.05rem 0.45rem;
    text-transform: uppercase;
}

/* The list and the button that jumps to the end of it, as one block. The button
   is positioned against this rather than against the panel, so it sits above the
   last message wherever the composer happens to end up - and the staff page,
   which carries a second form under the reply, does not push it off. */
.thread-scroll {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
    position: relative;
}

/* "Jump to the newest." Shipped hidden and revealed by script once the reader
   has scrolled away from the bottom, so it never covers the message they are
   already looking at. Without the script it stays hidden and the list still
   scrolls - the button is an improvement on that, not a requirement of it. */
.thread-jump {
    align-items: center;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 999px;
    bottom: 0.85rem;
    box-shadow: var(--shadow);
    color: var(--ink);
    cursor: pointer;
    display: flex;
    font: inherit;
    font-size: 0.82rem;
    gap: 0.35rem;
    padding: 0.35rem 0.85rem 0.35rem 0.6rem;
    position: absolute;
    right: 1.1rem;
}

.thread-jump:hover {
    border-color: var(--accent);
    color: var(--accent);
}

/* Something came in while they were reading further up. Tinted rather than
   worded differently, because the button does the same thing either way and a
   second sentence would be read as a second control. */
.thread-jump[data-thread-unread] {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}

/* --- Writing one -------------------------------------------------------------

   The composer, shaped like a chat's rather than like a form: the box and the
   button on one line, the label above them only for a screen reader. */
.thread-compose {
    position: relative;
}

/* Room on the right for the button that sits over it. It is genuinely inside - the padding is what
   keeps a long line from running underneath it. */
.thread-compose textarea {
    display: block;
    min-height: 3.5rem;
    padding-right: 3.5rem;
    resize: vertical;
    width: 100%;
}

/* A round icon button over the corner of the box.
   The word is still there, for a screen reader. It carries the same string
   the visible button used to, as its label - an icon with no accessible name is
   a button that announces itself as "button". */
.thread-send {
    align-items: center;
    background: var(--accent);
    border: 0;
    border-radius: 999px;
    bottom: 0.55rem;
    color: #fff;
    cursor: pointer;
    display: flex;
    height: 2.35rem;
    justify-content: center;
    padding: 0;
    position: absolute;
    right: 0.55rem;
    transition: background-color 0.12s ease;
    width: 2.35rem;
}

.thread-send:hover {
    background: var(--accent-dark);
}

/* Said again, louder, for a phone.

   A phone makes a form's one submit button full width, which is right for the button at the
   bottom of a card and wrong for a round icon lying over the corner of a text box. That rule is
   a class plus an element and outranks the width above, so the send button came out as a slab of
   accent blue across the composer, wider than the pane it is in. Named through the box it sits
   in, which outranks it back. */
.thread-page .thread-compose .thread-send {
    width: 2.35rem;
}

.thread-send svg {
    /* Nudged, because a paper plane is visually heavier on its left. */
    margin-left: -1px;
}

/* The way in to a private note and to the sentences kept to hand, beside the send button rather
   than as forms under the conversation - which is what was making the chat short on an ordinary
   screen.

   Deliberately quiet, and deliberately not the same shape as send: the one thing
   that must never happen here is somebody reaching for one and pressing the
   other. */
.thread-note-open,
.thread-quick,
.thread-attach {
    align-items: center;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 999px;
    bottom: 0.55rem;
    color: var(--ink-soft);
    display: flex;
    height: 2.35rem;
    justify-content: center;
    position: absolute;
    text-decoration: none;
    transition: color 0.12s ease, border-color 0.12s ease;
    width: 2.35rem;
}

/* Fixed slots, counted outwards from send.

   The paperclip and the quick-reply button are drawn wherever the staff composer is; the note is
   drawn only for somebody who may write one, so the optional control takes the outermost slot.
   The other way round, a composer without the note would leave a hole in the middle of the row. */
.thread-attach {
    right: 3.35rem;
}

.thread-quick {
    right: 6.15rem;
}

.thread-note-open {
    right: 8.95rem;
}

.thread-attach:hover,
.thread-quick:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.thread-note-open:hover {
    border-color: var(--warn);
    color: var(--warn);
}

/* Room for what is there. Three on the staff composer, four where a note can be written, and the
   customer's own has only send - so this is keyed on the buttons rather than on the portal. Each
   step is one slot, and they are written outermost-last so the widest wins. */
.thread-compose:has(.thread-attach) textarea {
    padding-right: 6.3rem;
}

.thread-compose:has(.thread-quick) textarea {
    padding-right: 9.1rem;
}

.thread-compose:has(.thread-note-open) textarea {
    padding-right: 11.9rem;
}

/* Pinned under the messages rather than following them down the page. */
.thread-page .account-form,
.thread-page > .field-hint {
    background: var(--surface);
    border-top: 1px solid var(--line);
    flex: 0 0 auto;
    margin: 0;

    /* `.account-form` caps itself at 544px, which is right for a column of settings fields and
       wrong for a chat: it left the box less than two thirds of the panel with empty white beside
       it. */
    max-width: none;
    padding: 0.9rem 1.25rem 1rem;
}

/* The staff page has a second form under the reply - the private note - and on
   a screen that no longer scrolls both still have to be reachable. Capped and
   scrolled on its own, so a long note form eats its own room rather than the
   conversation's. */
.thread-page .thread-note-form {
    border-top: 1px dashed var(--line);
    max-height: 45vh;
    overflow-y: auto;
    padding-top: 0.9rem;
}

/* The state and who is waiting, above the thread. */
/* A row of pills and quiet notes under a heading: where a thing stands, at a glance.
   Named for the shape rather than for the screen — the support thread had it first and a
   release wears exactly the same row. */
.standing-row {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

/* Set apart from the reply above it, because the two boxes do opposite things
   and sitting flush would read as one form with two buttons. */
.thread-note-form {
    border-top: 1px dashed var(--line);
    margin-top: 1.25rem;
    padding-top: 1rem;
}

/* A form that is only a button - resolve, reopen, close. It exists because a
   POST needs a form and the surrounding row is laid out with flex, which would
   otherwise stretch a block-level form across it. */
.inline-form {
    display: inline;
}

@media (max-width: 40rem) {
    /* A bubble capped at 78% of a phone is a column four words wide. */
    .thread-message {
        max-width: 92%;
    }

    /* Tighter gutters. On a phone the panel is the screen, and 1.25rem down each
       side is a bubble two words narrower for no gain. */
    .thread,
    .thread-page .card-header,
    .thread-page .account-form {
        padding-left: 0.85rem;
        padding-right: 0.85rem;
    }

    /* The panel goes edge to edge, because a rounded card floating inside a
       window that no longer scrolls is a frame around a frame. */
    .shell:has(.thread-page) .content {
        padding: 0.75rem 0 0;
    }

    .thread-page {
        border-left: 0;
        border-radius: 0;
        border-right: 0;
    }
}


/* The customer's downloads: one row per file, the name and its size on the left and the
   control on the right. A list rather than a table, because there are three or four of them
   and a table with four rows and a header is more furniture than content. */
.download-list {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    list-style: none;
    margin: 0.75rem 0 0;
    padding: 0;
}

.download-row {
    align-items: center;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    justify-content: space-between;
    padding: 0.6rem 0.8rem;
}

/* One button and one line under it, before the list. The page led with a grid of cards and the
   reader had to find their own platform in it; somebody who has decided to try Shelli should be
   able to press one thing. Centred because there is only ever one control here. */
.download-hero {
    align-items: center;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    margin: 0 0 1.6rem;
    text-align: center;
}

/* The row matching the machine the reader appears to be on. A border and a tint rather than a
   badge saying "recommended": it is a guess off a user agent, and a page that announces the guess
   is a page arguing with anybody it guessed wrong about. Everything else stays exactly as
   readable, because fetching the Windows installer from a Mac is an ordinary thing to be doing. */
.download-mine {
    background: color-mix(in srgb, var(--accent) 5%, transparent);
    border-color: var(--accent);
}

/* Takes the slack so the button stays hard right, and wraps its own two lines on a phone
   rather than pushing the control onto a line of its own. */
.download-what {
    display: flex;
    flex: 1 1 12rem;
    flex-direction: column;
    gap: 0.15rem;
}

/* A product's own colour, on its own card.

   `--mark` is set per card from the catalogue and falls back to the brand accent, so a product
   nobody has coloured still draws correctly rather than transparent. The colours themselves are
   spaced along the Draf IT blue and the .NET purple: the rule is one palette across the products
   rather than a brand for each, and what these have to do is tell the cards apart. */
.download-card {
    --mark: var(--accent);

    border-top: 3px solid var(--mark);
}

.download-card-head {
    align-items: center;
    display: flex;
    gap: 0.6rem;
    margin-bottom: 0.2rem;
}

.download-card-head h2 {
    margin: 0;
}

/* The shape itself, wherever it is drawn. Inline-flex so it sits on the text baseline of
   whatever it is beside rather than on the line box, which is what leaves an SVG hanging a few
   pixels low next to a heading. It takes its colour from its surroundings — inside a preview
   that is the product's own, and in a plain list it is the text colour. */
.app-mark {
    align-items: center;
    display: inline-flex;
    flex: 0 0 auto;
}

/* The shape, in its colour, on a wash of the same. A tinted square rather than a filled one:
   at a glance it is the colour that identifies the card, and a solid block that size would be
   the loudest thing on a page whose job is a list of downloads. */
.mark-preview {
    --mark: var(--accent);

    align-items: center;
    background: color-mix(in srgb, var(--mark) 12%, transparent);
    border-radius: var(--radius);
    color: var(--mark);
    display: inline-flex;
    flex: 0 0 auto;
    justify-content: center;
    padding: 0.5rem;
}

.mark-preview-small {
    margin-right: 0.5rem;
    padding: 0.3rem;
    vertical-align: middle;
}

/* The preview beside the two controls that change it, so somebody choosing an icon and a colour
   is looking at what they are choosing rather than at two form fields. */
.mark-picker {
    align-items: center;
    display: flex;
    gap: 0.8rem;
    margin-top: 0.4rem;
}

.mark-fields {
    display: flex;
    flex: 1 1 auto;
    flex-wrap: wrap;
    gap: 0.6rem;
    min-width: 0;
}

.mark-fields select {
    flex: 1 1 10rem;
    min-width: 0;
}

/* Given a height because a colour input is otherwise as tall as its own content and sits half a
   line above the select beside it. */
.mark-fields input[type="color"] {
    align-self: stretch;
    flex: 0 0 3.5rem;
    min-height: 2.4rem;
    padding: 0.2rem;
}

/* The machine a file is for, before the words. Muted rather than in the product's colour: the
   colour belongs to the card, and five tinted glyphs down a list would take it back. */
.platform-mark {
    align-items: center;
    color: var(--muted);
    display: inline-flex;
    flex: 0 0 auto;
}

/* Level with the platform's name rather than centred on the row. A row is one, two or three
   lines deep depending on whether the file has a shape and a sentence under it, and centring put
   the icon beside the file size on the long ones — labelling the wrong line. */
.download-row .platform-mark {
    align-self: flex-start;
    margin-top: 0.15rem;
}

/* Folded away by default. Almost nobody checks a checksum, and the few who do are the people
   who most want it to be on the page rather than taken on trust. */
.download-checksums {
    margin-top: 0.9rem;
}

.download-checksums summary {
    color: var(--ink-faint);
    cursor: pointer;
    font-size: 0.9rem;
}

.download-checksums dd {
    margin: 0 0 0.6rem;
    overflow-wrap: anywhere;
}

.download-checksums code {
    font-size: 0.8rem;
}

/* --- The inbox ----------------------------------------------------------------------------

   Three panes on one screen: who is waiting, what they said, and who they are. The old shape was
   a table you clicked away from, and answering four people meant four round trips through a list
   that had scrolled back to the top by the time you returned.

   Full height rather than page-length. A conversation is read from the bottom, and a pane that
   grows with its content puts the newest message below the fold on a long thread and the composer
   somewhere off the screen entirely. So each pane scrolls inside itself and the page never does.

   The shell is asked to give up its measure by :has(), the way the issue workbench already does -
   the layout says what it needs and no route table has to know which page is which. */
.shell:has(.inbox-layout) {
    height: 100dvh;
}

.shell-main:has(.inbox-layout) {
    min-height: 0;
}

.content:has(.inbox-layout) {
    display: flex;
    flex: 1;
    max-width: none;
    min-height: 0;
    padding: 0;
}

/* The portal's navigation goes away while this screen is open. Somebody working support wants
   the support area and nothing else, and a column of links to the rest of the portal is noise
   beside a conversation. It is the shell's own elements that stand down rather than a second
   layout being drawn, so there is still one navigation and one place it is written.

   The state checkbox stands down with them. It is off-screen rather than display:none precisely
   so it keeps its place in the tab order, and left alone here it is the first thing Tab reaches
   and it opens something nobody can see. */
.shell:has(.inbox-layout) .drawer,
.shell:has(.inbox-layout) .drawer-state,
.shell:has(.inbox-layout) .drawer-open,
.shell:has(.inbox-layout) .drawer-scrim {
    display: none;
}

.inbox-layout {
    display: grid;
    flex: 1;
    grid-template-columns: minmax(0, 22rem) minmax(0, 1fr) minmax(0, 21rem);
    min-height: 0;
    width: 100%;
}

.inbox-list,
.inbox-side {
    display: flex;
    flex-direction: column;
    min-height: 0;
    min-width: 0;
}

.inbox-list {
    background: var(--surface);
    border-right: 1px solid var(--line);
}

.inbox-side {
    background: var(--surface);
    border-left: 1px solid var(--line);
}

.inbox-thread {
    display: flex;
    flex-direction: column;
    min-height: 0;
    min-width: 0;
}

/* --- Finding somebody ---------------------------------------------------------------------

   The search and the filters sit still while the list scrolls under them. A filter that scrolls
   away is one somebody forgets is on, and then the queue looks empty for a reason nothing on the
   screen explains. */
.inbox-head {
    border-bottom: 1px solid var(--line);
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    padding: 0.85rem 0.9rem;
}

.inbox-search {
    display: flex;
    gap: 0.4rem;
}

/* The way through to every filter. A column layout stretches its children, and stretched it reads
   as the main thing to press on a pane whose main thing is the list. */
.inbox-search input {
    flex: 1;
    min-width: 0;
}

/* What the pane is, and the two things that are not filters: starting a conversation, and the
   chime. Above the controls rather than among them - both were being read as one more way to
   narrow the list. */
.inbox-title {
    align-items: center;
    display: flex;
    gap: 0.5rem;
}

.inbox-title h2 {
    flex: 1;
    font-size: 0.95rem;
    margin: 0;
}

/* Starting a conversation, on its own line under the title. It shares the width with the search
   box below it rather than competing with the heading for a row that also carries the chime. */
.inbox-start {
    display: block;
    font-size: 0.82rem;
    padding-block: 0.4rem;
    text-align: center;
}

.inbox-title .sound-toggle .button {
    padding: 0.3rem 0.4rem;
}

/* Leaving, and the only way out on the screen now that the drawer is not on it. Quiet and first:
   it is not what anybody came here to do, and it belongs where the eye starts rather than beside
   the button that starts a conversation. */
.inbox-exit {
    color: var(--ink-faint);
    height: 30px;
    text-decoration: none;
    width: 30px;
}

.inbox-exit:hover {
    color: var(--ink);
}

/* The two narrowings a person triaging reaches for, side by side. They submit on change, so
   there is no button beside them to press and forget. */
.inbox-narrow {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}

/* A floor rather than nothing, so the two of them wrap instead of squeezing. Shrunk to fit
   beside the button they came out as "Todos los cana..." - a filter whose reading is guesswork
   is worse than one that costs a second line. */
.inbox-narrow select {
    flex: 1 1 8rem;
    font-size: 0.8rem;
    min-width: 8rem;
    padding-block: 0.25rem;
}

/* The way to apply a narrowing without a script. Quiet, and to the right of whatever line it
   lands on: where the script is working it is never pressed, and it still has to be reachable
   when it is not. */
.inbox-apply {
    flex: none;
    font-size: 0.78rem;
    margin-left: auto;
    padding: 0.25rem 0.55rem;
}

/* Two halves rather than two buttons sized by their own words. Left to themselves one wraps to
   two lines and the other does not, and a row of controls at two heights reads as a mistake. */
.inbox-actions {
    align-items: stretch;
    display: flex;
    gap: 0.4rem;
}

.inbox-actions .button {
    align-items: center;
    display: flex;
    flex: 1;
    font-size: 0.78rem;
    justify-content: center;
    padding-block: 0.3rem;
    text-align: center;
}

/* The state tabs, as pills rather than as a select. Four states and a count each: the count is
   the half that decides which to press, and a closed select shows one option and no counts. */
.inbox-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
}

.inbox-tab {
    align-items: center;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 999px;
    color: var(--ink-faint);
    display: inline-flex;
    font-size: 0.78rem;
    font-weight: 600;
    gap: 0.3rem;
    padding: 0.2rem 0.6rem;
    text-decoration: none;
}

.inbox-tab:hover {
    background: color-mix(in srgb, var(--ink) 6%, transparent);
    color: var(--ink);
}

.inbox-tab[aria-current="page"] {
    background: color-mix(in srgb, var(--accent) 12%, transparent);
    border-color: color-mix(in srgb, var(--accent) 30%, transparent);
    color: var(--accent);
}

.inbox-tab-count {
    font-variant-numeric: tabular-nums;
    opacity: 0.75;
}

/* --- The conversations --------------------------------------------------------------------- */

.inbox-rows {
    flex: 1;
    list-style: none;
    margin: 0;
    min-height: 0;
    overflow-y: auto;
    padding: 0;
}

.inbox-row {
    border-bottom: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
    color: inherit;
    display: grid;
    gap: 0 0.65rem;
    grid-template-areas:
        "mark who when"
        "mark what what";
    grid-template-columns: auto minmax(0, 1fr) auto;
    padding: 0.7rem 0.9rem;
    text-decoration: none;
}

.inbox-row:hover {
    background: color-mix(in srgb, var(--ink) 4%, transparent);
}

/* The one being read. A left edge rather than a fill: the row underneath it is a customer's own
   words, and tinting the whole row makes the two hardest things on this screen to tell apart -
   selected and unread - look the same. */
.inbox-row[aria-current="page"] {
    background: color-mix(in srgb, var(--accent) 8%, transparent);
    box-shadow: inset 3px 0 0 var(--accent);
}

.inbox-mark {
    align-items: center;
    background: color-mix(in srgb, var(--accent) 12%, transparent);
    border-radius: 50%;
    color: var(--accent);
    display: flex;
    font-size: 0.8rem;
    font-weight: 700;
    grid-area: mark;
    height: 2.1rem;
    justify-content: center;
    position: relative;
    width: 2.1rem;
}

/* The channel, on the corner of the mark. It is the first thing somebody needs about a row -
   whether this is WhatsApp or the portal decides what they can say back - and it belongs on the
   person rather than in a column of its own. */
.inbox-mark .channel-badge {
    background: var(--surface);
    border-radius: 50%;
    bottom: -2px;
    inset-inline-end: -3px;
    line-height: 0;
    padding: 1px;
    position: absolute;
}

.inbox-who {
    color: var(--ink);
    font-weight: 600;
    grid-area: who;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.inbox-when {
    color: var(--ink-faint);
    font-size: 0.75rem;
    grid-area: when;
    white-space: nowrap;
}

.inbox-what {
    color: var(--ink-faint);
    display: flex;
    font-size: 0.82rem;
    gap: 0.4rem;
    grid-area: what;
    min-width: 0;
}

.inbox-what span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Waiting on us, said with weight rather than with a colour. The row is already carrying a
   selected state in the brand colour, and a second colour on the same row stops meaning
   anything. */
.inbox-row[data-waiting] .inbox-who {
    font-weight: 700;
}

.inbox-row[data-waiting] .inbox-what {
    color: var(--ink);
}

.inbox-unread {
    background: var(--accent);
    border-radius: 50%;
    display: inline-block;
    flex: none;
    height: 0.5rem;
    margin-top: 0.35rem;
    width: 0.5rem;
}

.inbox-foot {
    align-items: center;
    border-top: 1px solid var(--line);
    display: flex;
    gap: 0.5rem;
    justify-content: space-between;
    padding: 0.6rem 0.9rem;
}

/* The thread card was written to fill a flex column and now does. Its frame goes: a card with a
   border and a radius inside a pane with its own is two boxes drawn around one conversation. */
.inbox-thread > .page-header {
    align-items: center;
    border-bottom: 1px solid var(--line);
    margin: 0;
    padding: 0.7rem 1rem;
}

.inbox-thread > .page-header h1 {
    font-size: 1.05rem;
    margin: 0;
}

.inbox-thread > .card {
    border-inline: 0;
    border-radius: 0;
    margin: 0;
}

.inbox-thread > .card:last-child {
    border-bottom: 0;
}

/* Back to the list, and out to the pane about the customer. Both are drawn at every width and
   shown at one: above the phone breakpoint what they reach is already on the screen, and a
   button that reveals something already visible reads as broken. */
.thread-controls {
    display: flex;
    gap: 0.4rem;
}

.inbox-back,
.inbox-about-open,
.inbox-about-close {
    display: none;
}

/* The customer pane's on-off. Off-screen rather than display:none, the same as the shell's own
   drawer and for the same reason: what is visible is a label, and a label never takes focus. */
.about-state {
    height: 1px;
    opacity: 0;
    pointer-events: none;
    position: absolute;
    width: 1px;
}

/* So the ring has to be drawn on the checkbox's behalf, or a keyboard user cannot see what
   Space is about to do. */
.about-state:focus-visible ~ .inbox-thread .inbox-about-open,
.about-state:focus-visible ~ .inbox-side .inbox-about-close {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.inbox-scrim {
    display: none;
}

/* --- What a message carries --------------------------------------------------------------------

   A picture is drawn in the bubble; anything else is a line with a way to open it. A PDF rendered
   in the middle of a conversation is a page inside a page, and the one thing a reader wants from
   it is to look at it properly.

   Capped by height rather than by width, so a photograph held portrait and one held landscape
   both fit the bubble without either being letterboxed. */
.thread-picture {
    display: block;
    margin-top: 0.35rem;
}

.thread-picture img {
    border-radius: 10px;
    display: block;
    max-height: 18rem;
    max-width: 100%;
    object-fit: cover;
}

.thread-file {
    align-items: center;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 10px;
    color: inherit;
    display: flex;
    gap: 0.5rem;
    margin: 0.35rem 0 0;
    padding: 0.45rem 0.6rem;
    text-decoration: none;
}

.thread-file:hover {
    border-color: var(--accent);
}

.thread-file-name {
    font-size: 0.85rem;
    font-weight: 600;
    overflow-wrap: anywhere;
}

.thread-file-note {
    color: var(--ink-faint);
    font-size: 0.78rem;
    margin-left: auto;
    white-space: nowrap;
}

/* Swept. Not a link, because there is nothing behind it - and said rather than hidden, or a
   thread would silently lose a photograph the next message refers to. */
.thread-file-gone {
    border-style: dashed;
    color: var(--ink-faint);
}

.thread-file-gone .thread-file-name {
    font-weight: 500;
}

/* The customer's own way to send one, under the composer rather than over its corner - that
   portal has no dialog to open and a second box is honest about being a second thing to press. */
.thread-attach-form {
    border-top: 1px solid var(--line);
    margin: 0;
    padding: 0.6rem 1.25rem 0.9rem;
}

.thread-attach-form .file-field {
    align-items: center;
    flex-wrap: wrap;
}

/* --- The sentences kept to hand ---------------------------------------------------------------

   A list somebody scans for a known entry, so each row shows the whole of what it will write
   rather than an opening line. Choosing one puts it in the composer to be edited, which is why
   Use is the primary control on the row and correcting it is the quiet one. */
.quick-list {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    list-style: none;
    margin: 0 0 1.25rem;
    max-height: 22rem;
    overflow-y: auto;
    padding: 0;
}

.quick-item {
    align-items: flex-start;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    display: flex;
    gap: 0.75rem;
    padding: 0.6rem 0.7rem;
}

/* The one open in the form below, so it is obvious which row the fields belong to. */
.quick-item[aria-current] {
    border-color: var(--accent);
}

.quick-words {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    min-width: 0;
}

.quick-body {
    color: var(--ink-soft);
    font-size: 0.85rem;
    white-space: pre-wrap;
}

.quick-actions {
    align-items: center;
    display: flex;
    flex-shrink: 0;
    gap: 0.35rem;
    margin-left: auto;
}

/* .button-primary carries a top margin for the case it was written for - the one button under a
   form. Cleared here the same way .action-row clears it, or Use sits a fifth of an inch below
   the button beside it. */
.quick-actions .button-primary {
    margin-top: 0;
}

.quick-heading {
    font-size: 0.95rem;
    margin: 0 0 0.5rem;
}

/* The rows stop being a row on a phone: two buttons and a sentence side by side leave the
   sentence four words wide. */
@media (max-width: 40rem) {
    .quick-item {
        flex-direction: column;
    }

    .quick-actions {
        margin-left: 0;
    }
}

/* --- Nothing chosen -------------------------------------------------------------------------

   The middle pane with no conversation in it. It says what this screen is for rather than sitting
   blank, because an empty three-pane layout on first open reads as something that failed to
   load. */
.inbox-nothing {
    align-items: center;
    color: var(--ink-faint);
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 0.4rem;
    justify-content: center;
    padding: 2rem;
    text-align: center;
}

.inbox-nothing svg {
    color: color-mix(in srgb, var(--accent) 40%, transparent);
}

/* --- Who they are ---------------------------------------------------------------------------

   The right pane scrolls on its own, so the actions at the top of it stay reachable on a customer
   with a long history. */
.inbox-side-scroll {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 1rem;
    min-height: 0;
    overflow-y: auto;
    padding: 1rem;
}

.inbox-side h2 {
    font-size: 0.78rem;
    letter-spacing: 0.04em;
    margin: 0 0 0.5rem;
    text-transform: uppercase;
}

.inbox-side section {
    border-top: 1px solid var(--line);
    padding-top: 0.9rem;
}

.inbox-side section:first-child {
    border-top: 0;
    padding-top: 0;
}

.inbox-facts {
    display: grid;
    font-size: 0.85rem;
    gap: 0.35rem 0.6rem;
    grid-template-columns: auto minmax(0, 1fr);
    margin: 0;
}

.inbox-facts dt {
    color: var(--ink-faint);
}

/* What the conversation is, and what may be done to it, as one line. Wrapping rather than
   squeezing: two buttons and a pill fit in this pane in Spanish, and a longer word going to a
   second line is better than either of them being clipped. */
.side-state {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

/* Named through the pane, because the pane's own rhythm rule below weighs the same and is
   written later - it would put the gap back and drop the row under the state again. */
.inbox-side .side-state .action-row {
    margin-left: auto;
    margin-top: 0;

    /* The pair stays a pair. A phone turns every action row into a column of full-width
       buttons, which is right for the footer of a form and wrong for two small controls beside
       a state - it put them one above the other down the side of the pane. The row above them
       is the thing that gives way here, and it wraps on its own. */
    flex-direction: row;
    flex-wrap: nowrap;
}

.inbox-side .side-state .action-row .button {
    width: auto;
}

/* The picker and the tick that applies it. The button keeps its own width against a select
   told to fill what is left. */
.inbox-assign {
    align-items: center;
    display: flex;
    gap: 0.4rem;
}

.inbox-assign select {
    flex: 1;
    min-width: 0;
}

/* Named through the pane for the width, because a phone makes a form's submit button full
   width - right for the button at the bottom of a card, and here it takes the whole row and
   leaves the picker beside it as a chevron. The same collision the send button has. */
.inbox-side .inbox-assign .button {
    align-items: center;
    display: flex;
    flex: none;
    padding: 0.4rem 0.6rem;
    width: auto;
}

.inbox-facts dd {
    margin: 0;
    min-width: 0;
    overflow-wrap: anywhere;
}

/* The controls under each list of facts. Without the gap a form label sits directly under the
   last value and reads as one more fact rather than as the thing that changes them. */
.inbox-side .account-form,
.inbox-side .action-row {
    margin-top: 0.8rem;
}

.inbox-side .account-form label {
    font-size: 0.8rem;
}

/* --- Narrower than three panes --------------------------------------------------------------

   The side pane goes first: it is reference, and everything on it is reachable from the thread's
   own header. Below that the list and the thread stop sharing a screen, and which one is drawn is
   decided by whether a conversation is open - the same rule a phone's mail app follows, and the
   reason the back link in the thread's header exists at all. */
@media (max-width: 78rem) {
    .inbox-layout {
        grid-template-areas:
            "list thread"
            "list side";
        grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
        grid-template-rows: minmax(0, 1fr) auto;
    }

    .inbox-list {
        grid-area: list;
    }

    .inbox-thread {
        grid-area: thread;
    }

    /* Under the conversation rather than gone. It was display:none here first, and that took the
       assign, link, resolve and close controls off the screen with it - the panel cannot simply be
       drawn twice, because two forms with one name on a page is a binding error rather than a
       layout choice. So it moves, and keeps every control reachable at every width. */
    .inbox-side {
        border-inline-start: 0;
        border-top: 1px solid var(--line);
        grid-area: side;
        max-height: 35vh;
    }

    .inbox-side-scroll {
        display: grid;
        gap: 0 1.5rem;
        grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
    }

    .inbox-side section:first-child {
        border-top: 0;
    }
}

@media (max-width: 52rem) {
    .inbox-layout {
        grid-template-areas: "thread";
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: minmax(0, 1fr);
    }

    .inbox-layout:not([data-reading]) {
        grid-template-areas: "list";
    }

    /* One at a time, and which one is decided by whether a conversation is open - the rule a
       phone's mail app follows, and the reason the thread header keeps a way back. */
    .inbox-layout[data-reading] .inbox-list,
    .inbox-layout:not([data-reading]) .inbox-thread {
        display: none;
    }

    /* Off to the side rather than under the conversation. Stacked, it took a third of a phone
       screen from the thread and pushed the composer towards the bottom of it - and everything
       on it is reference somebody asks for once, not something read while they type.

       Out of flow, so it is no longer a grid item and the layout above is one row rather than
       two. Moved rather than hidden: hiding it takes assign, link, resolve and close with it. */
    .inbox-side {
        border-inline-start: 1px solid var(--line);
        border-top: 0;
        bottom: 0;
        max-height: none;
        position: fixed;
        right: 0;
        top: 0;
        transform: translateX(100%);
        transition: transform 180ms ease;
        width: min(20rem, 86vw);
        z-index: 50;
    }

    .about-state:checked ~ .inbox-side {
        box-shadow: 0 0 40px rgb(20 23 28 / 35%);
        transform: translateX(0);
    }

    /* Only hit-testable once the pane is open, so it never swallows a tap on the conversation. */
    .about-state:checked ~ .inbox-scrim {
        background: rgb(20 23 28 / 45%);
        display: block;
        inset: 0;
        position: fixed;
        z-index: 40;
    }

    /* A column again. The two-pane width lays these out side by side to fill a short strip
       under the conversation, and there is no strip here. */
    .inbox-side-scroll {
        display: flex;
        flex-direction: column;
    }

    .inbox-about-close {
        align-self: flex-end;
        display: inline-flex;
        margin: 0.5rem 0.5rem -0.75rem;
    }

    .inbox-back,
    .inbox-about-open {
        align-items: center;
        display: inline-flex;
        gap: 0.3rem;
    }
}

/* --- The university ---------------------------------------------------------------

   Two screens share these: the authoring curriculum in the Admin Portal and the player in
   the Customer Portal. One block rather than two, because a lesson row that looks like one
   thing to whoever writes it and another to whoever watches it is how the two drift. */

/* A course on the catalogue. The card already has its padding; this only stops the button
   floating up beside the progress bar when the summary is short. */
.course-card {
    display: flex;
    flex-direction: column;
}

.course-card .action-row {
    margin-top: auto;
}

/* The progress bar. A bar as well as the fraction, because "4 of 11" is a number somebody
   has to do arithmetic on and a bar is a glance. */
.progress-track {
    background: var(--line);
    border-radius: 999px;
    height: 0.45rem;
    margin: 0.6rem 0;
    overflow: hidden;
}

.progress-fill {
    background: var(--accent);
    height: 100%;
    /* Width is an inline style: the figure is per course and known only to the page. */
    transition: width 0.2s ease;
}

/* Something beside a fixed panel: a video beside its curriculum, an issue beside the controls
   that change it. Grid rather than flex so the sidebar keeps its width while the main column
   takes whatever is left — a player that changes width per lesson is unreadable, and a form
   whose fields move when the text beside them grows is worse.

   One rule, two users. The issue board wanted exactly this and writing it again under another
   name is how a codebase ends up with two column layouts that drift apart by a rem. */
.course-layout,
.work-layout {
    display: grid;
    gap: 1.5rem;
    grid-template-columns: minmax(0, 1fr) 20rem;
}

/* minmax(0, 1fr) above and this here: a grid column is sized by its content by default, so
   a wide video — or a stack trace in a comment — would push the sidebar off the screen rather
   than shrink. */
.course-stage,
.work-main {
    min-width: 0;
}

.work-side {
    align-self: start;
    position: sticky;
    top: 1rem;
}

.course-curriculum {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 1rem;
}

.course-curriculum h2 {
    font-size: 1rem;
    margin-top: 0;
}

.course-curriculum h3 {
    color: var(--ink-soft);
    font-size: 0.85rem;
    letter-spacing: 0.02em;
    margin: 1rem 0 0.35rem;
    text-transform: uppercase;
}

.curriculum-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.curriculum-list li {
    border-radius: var(--radius);
}

.curriculum-list a {
    align-items: baseline;
    color: inherit;
    display: flex;
    gap: 0.5rem;
    padding: 0.45rem 0.5rem;
    text-decoration: none;
}

.curriculum-list a:hover {
    background: var(--muted);
}

/* Where the reader is. Marked with a colour and a weight rather than colour alone. */
.curriculum-list .is-playing {
    background: var(--muted);
}

.curriculum-list .is-playing .curriculum-title {
    font-weight: 600;
}

.curriculum-tick {
    color: var(--good);
    flex: 0 0 auto;
}

.curriculum-title {
    flex: 1 1 auto;
}

/* The player itself. A 16:9 box that the video fills, so the page does not jump as each
   lesson loads and reports its own dimensions. */
.video-frame {
    aspect-ratio: 16 / 9;
    background: #000;
    border-radius: var(--radius);
    overflow: hidden;
    width: 100%;
}

.video-frame iframe,
.video-frame video {
    border: 0;
    display: block;
    height: 100%;
    width: 100%;
}

/* --- Authoring a curriculum ------------------------------------------------------- */

.lesson-section {
    border-top: 1px solid var(--line);
    padding-top: 0.9rem;
}

.lesson-section:first-of-type {
    border-top: 0;
    padding-top: 0;
}

.lesson-section-head {
    align-items: baseline;
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    justify-content: space-between;
}

.lesson-section-head h3 {
    margin: 0;
}

.lesson-list {
    list-style: none;
    margin: 0.5rem 0 1rem;
    padding: 0;
}

/* One lesson: what it is on the left, what you can do to it on the right. Wraps rather than
   scrolls, so a narrow screen stacks the two instead of hiding the controls. */
.lesson-row {
    align-items: center;
    border-top: 1px solid var(--line);
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    justify-content: space-between;
    padding: 0.55rem 0;
}

.lesson-what {
    display: flex;
    flex: 1 1 14rem;
    flex-direction: column;
    gap: 0.15rem;
}

/* The sidebar goes under the main column rather than beside it. 900px is where 20rem of
   curriculum stops leaving a video worth watching, and where the issue's fields stop having
   room for a date and a duration side by side. */
@media (max-width: 900px) {
    .course-layout,
    .work-layout {
        grid-template-columns: minmax(0, 1fr);
    }

    /* Sticky is wrong once it is stacked: the panel would follow the reader up the page and
       sit on top of the thread they are trying to read. */
    .work-side {
        position: static;
    }
}

/* --- The issue workbench ------------------------------------------------------ */

/* A fieldset carries a browser border and padding of its own, and nothing here had ever reset it
   because every other form in this portal wraps its fields in a div. The issue screens use a real
   fieldset so the whole group can be disabled at once when work is finished — which is worth
   having, and cost two boxed outlines nobody asked for until this rule existed. */
fieldset.account-form {
    border: 0;
    margin: 0;
    min-width: 0;
    padding: 0;
}

/* The reading width is 64rem because a line of prose longer than that is hard to track back to
   its start. A workbench is not prose: it is a thread beside the controls that change it, and at
   64rem the sidebar eats a third of what is left. Widened only for the page that has one, by
   asking whether it contains the layout rather than by having the shell know which route it is
   drawing. */
.content:has(.work-layout) {
    max-width: 90rem;
}

/* The sidebar's fields fill it. 34rem is the reading maximum for a form on an open page and is
   wider than this column, so without this every control is cut off at the panel's edge.

   The main column takes the same rule for the opposite reason: a description and a test plan are
   written at length, and 34rem in the middle of a 60rem card reads as a broken box rather than as a
   measure. */
.work-main .account-form,
.work-side .account-form {
    max-width: none;
}

/* --- The issue's panes -------------------------------------------------------------------

   The one tab strip in these portals that does not navigate. Four radios and four
   labels, because the description and the test plan have to be in the DOM together to be
   saved by one button — see IssueDetail's own remarks. Everywhere else PageTabs is right and
   these rules must not be reached for.

   The radio keeps its place in the layout rather than taking display: none, so it stays
   focusable: the strip is then a real radio group and the arrow keys move between panes. */
.pane-pick {
    height: 0;
    opacity: 0;
    pointer-events: none;
    position: absolute;
    width: 0;
}

.pane {
    display: none;
}

.pane-pick-detail:checked ~ .work-layout .pane-detail,
.pane-pick-metrics:checked ~ .work-layout .pane-metrics,
.pane-pick-test:checked ~ .work-layout .pane-test,
.pane-pick-files:checked ~ .work-layout .pane-files {
    display: block;
}

/* Focus lands on the radio, which cannot be seen, so the ring has to be drawn on the label
   that belongs to it. Without this the strip works from the keyboard and gives no sign of
   where it is. */
.pane-pick-detail:focus-visible ~ .work-layout .tab-detail,
.pane-pick-metrics:focus-visible ~ .work-layout .tab-metrics,
.pane-pick-test:focus-visible ~ .work-layout .tab-test,
.pane-pick-files:focus-visible ~ .work-layout .tab-files {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

/* --- The issue's timeline ------------------------------------------------------------------

   Its own family rather than the chat's `.thread`, which is a flex scroller that pins the
   newest message to the bottom of a fixed panel. This list is newest-first, has no height of
   its own and is paged — everything that one is not. */
.issue-log {
    display: flex;
    flex-direction: column;
    list-style: none;
    margin: 0.5rem 0 0;
    padding: 0;
}

.issue-log-line {
    align-items: baseline;
    border-top: 1px solid var(--line);
    display: flex;
    font-size: 0.9rem;
    gap: 0.6rem;
    padding: 0.45rem 0;
}

.issue-log-line:first-child {
    border-top: 0;
}

.issue-log-who {
    color: var(--ink);
    font-weight: 600;
    white-space: nowrap;
}

.issue-log-when {
    color: var(--ink-faint);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* The duration reads as a quantity, so it is set like every other figure here. */
.issue-log-time {
    color: var(--ink);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    white-space: nowrap;
}

/* `min-width: 0` is what lets this shrink. A flex item's floor is its own content, so
   without it the row grows past the card rather than clipping, and the ellipsis — the whole
   point of a one-line entry — never appears. */
.issue-log-said {
    color: var(--ink-soft);
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.issue-log-more {
    white-space: nowrap;
}

/* On a phone the row is four things wider than the screen. Wrapped, with the writing on its
   own line, it is still one entry rather than a paragraph. */
@media (max-width: 40rem) {
    .issue-log-line {
        flex-wrap: wrap;
    }

    .issue-log-said {
        flex-basis: 100%;
    }
}

/* The counts under the heading. Not .stat-row: those are cards a person clicks, and these are
   three facts about the record they are already reading. */
.work-facts {
    color: var(--ink-soft);
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 1rem;
    margin: 0.35rem 0 0;
}

.work-facts strong {
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}

/* --- A plan, as a customer chooses it ----------------------------------------------------

   Moved out of customer.css, when the Admin storefront began previewing these cards: two portals
   draw them now, and this file's own rule is that anything specific to one portal belongs in that
   portal's stylesheet and everything else belongs here.

   The palette moved with them. These rules are tuned for Shelli's purple, and a preview
   drawn in the Admin Portal's blue would be a preview of something no customer sees. Wearing
   .shell-customer is what a preview container does to borrow it. */

/* The customer portal's palette, and the account pages that sit outside its shell.

   .account-split is on the sign-in, register and password pages, which use their own layout and
   so never get .shell-customer. Without it they fell back to the Nexus blue: a purple site with a
   blue sign-in button, on the one page a visitor reaches straight from an advert. Named here
   rather than restated in customer.css, because two copies of a brand colour is how a brand ends
   up with two of them. */
.shell-customer,
.account-split {
    /* Shelli's primary. The darker value is what carries text and focus rings: the bright one is
       3.6:1 on white, which is fine behind a filled button and not fine on a link. */
    --accent: #6b3fd4;
    --accent-dark: #5a33b8;
    --accent-bright: #845adf;

    /* Shelli's neutrals. Warmer and lighter than the blue-grey Nexus started with, which is most
       of why the two products did not look related. */
    --ink: #333335;
    --ink-soft: #5b6068;
    --ink-faint: #8c9097;
    --paper: #f7f8f9;
    --line: #e6eaee;

    --good: #12805f;
    --good-soft: #e7f7f1;
    --warn: #8a5a09;
    --warn-soft: #fdf4e3;
    --bad: #b23a29;
    --bad-soft: #fdeeeb;

    /* 0.5rem, the radius every card in Shelli wears. */
    --radius: 8px;

    /* Flatter than the Admin Portal's. Shelli's cards sit on the page rather than above it, and a
       drop shadow under every panel is the single thing that reads as "a different designer". */
    --shadow: 0 1px 2px rgb(51 51 53 / 5%);

    /* Restated, not inherited, and this is the trap. nexus.css defines --ring in terms of --accent,
       which looks like it would follow the purple above. It does not: a custom property's var() is
       substituted where the property is declared, so --ring computes against :root's blue and
       inherits already resolved. Overriding --accent alone left purple fields wearing blue focus
       rings — visible only to somebody tabbing through a form. */
    --ring: color-mix(in srgb, var(--accent) 22%, transparent);
}

.tile-note {
    color: var(--ink-faint);
    font-size: 0.82rem;
}

/* Past the allowance. The same grey for both left "3 of 1" reading as a fault in the page rather
   than as a shop running more tills than it pays for, which is a thing somebody has to act on. */
.tile-note-over {
    color: var(--warn);
    font-weight: 600;
}

/* How long you are paying for, chosen once above the grid.

   A segmented pill rather than the underlined .tabs strip used elsewhere. Tabs say "these are
   different pages"; this says "the same two plans, priced two ways", and the enclosed track is
   what carries that — the reader sees the alternative sitting beside what they chose. */
.term-switch {
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: 999px;
    display: inline-flex;
    gap: 0.15rem;
    margin: 0 0 1rem;
    padding: 0.2rem;
}

.term-option {
    align-items: center;
    border-radius: 999px;
    color: var(--ink-soft);
    display: inline-flex;
    font-size: 0.9rem;
    font-weight: 600;
    gap: 0.4rem;
    padding: 0.45rem 1rem;
    text-decoration: none;
    transition: background 0.12s ease, color 0.12s ease;
    white-space: nowrap;
}

.term-option:hover {
    color: var(--ink);
}

/* Two signals, not one: anybody who cannot separate these colours still has the raised surface
   and the shadow. The same rule the tab underline follows. */
.term-option-on {
    background: var(--surface);
    box-shadow: var(--shadow);
    color: var(--ink);
}

/* What the longer term gives away, on the control rather than only in the cards — it is the
   reason to press it, and a switch that only says "Anual" is not an argument. */
.term-flag {
    background: var(--good-soft);
    border-radius: 999px;
    color: var(--good);
    font-size: 0.7rem;
    font-weight: 600;
    padding: 0.1rem 0.45rem;
}

.offer-grid {
    display: grid;
    gap: 0.85rem;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}

/* A shop that is shown only its own plan gets one card, and auto-fit stretches a lone column to
   the whole row — a card three times the width of the same card beside a neighbour. Capped at
   what it would have been in a two-up row: half the grid, less half the gap. Two is the case
   worth matching, because two is what a customer choosing between plans actually sees.

   Centred, because a half-width card left against the edge reads as the first of a pair whose
   second one failed to load. */
.offer-grid-one {
    grid-template-columns: minmax(230px, calc(50% - 0.425rem));
    justify-content: center;
}

/* A switch that is a link.

   Stopping a device stops a till while it is serving customers, so this opens the confirmation
   the row has always opened rather than acting on one stray click in a table. The affordance is
   the switch; the step after it is the safety. */
.switch {
    align-items: center;
    background: var(--line);
    border-radius: 999px;
    display: inline-flex;
    flex-shrink: 0;
    height: 1.35rem;
    padding: 0.15rem;
    transition: background 0.15s ease;
    width: 2.5rem;
}

.switch-on {
    background: var(--good);
}

.switch-knob {
    background: var(--surface);
    border-radius: 50%;
    box-shadow: 0 1px 2px rgb(20 23 28 / 25%);
    height: 1.05rem;
    transition: transform 0.15s ease;
    width: 1.05rem;
}

.switch-on .switch-knob {
    transform: translateX(1.15rem);
}

/* The way through to the live numbers. Drawn as one line rather than a paragraph and a button:
   an owner opening their shop is looking for this, and everything else on the page is settled
   information they read once. */
.live-door {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    justify-content: space-between;
}

.live-door-icon {
    color: var(--accent);
    flex-shrink: 0;
}

.live-door-said {
    flex: 1 1 16rem;
}

/* A legal document as somebody actually reads one: a measured column, room between the
   paragraphs, and headings that separate the sections rather than shouting. These are several
   pages of prose in a product whose other pages are tables. */
.legal-body {
    line-height: 1.65;
    max-width: 46rem;
}

.legal-body h2 {
    font-size: 1.1rem;
    margin: 2rem 0 0.6rem;
}

.legal-body h3 {
    font-size: 1rem;
    margin: 1.4rem 0 0.5rem;
}

.legal-body p,
.legal-body ul,
.legal-body ol {
    margin: 0 0 0.9rem;
}

.legal-body ul,
.legal-body ol {
    padding-left: 1.4rem;
}

.legal-body li {
    margin-bottom: 0.35rem;
}

.legal-body hr {
    border: 0;
    border-top: 1px solid var(--line);
    margin: 2rem 0;
}

.legal-others {
    display: flex;
    flex-wrap: wrap;
    gap: 1.25rem;
}

/* Monospaced, because what is being typed is markup and a proportional font hides where a
   list stops lining up. */
.legal-editor {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.85rem;
    line-height: 1.5;
    min-height: 24rem;
    width: 100%;
}

.offer {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: calc(var(--radius) * 1.75);
    display: flex;
    flex-direction: column;
    gap: 0.5rem;

    /* Roomier than the tiles around it. A price card is read once and decided on, not scanned, and
       the competitor cards we looked at earn most of their look from the space. */
    padding: 1.5rem 1.35rem;

    /* What the stretched label below is measured against. */
    position: relative;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

/* Lifts under the pointer, and the whole tile is the target — see .offer-pick::before. It was
   a hint rather than a promise for months: the price block was pressable and the rest of the
   card was not, so somebody who read the features and pressed the feature list chose nothing. */
.offer:hover {
    border-color: var(--accent-bright);
    box-shadow: var(--shadow);
}

/* Pointer over the whole card, not only over the price. Scoped to a card that has something to
   choose: a preview, an unpriced plan and a card outside its renewal window all draw the same
   panel with nothing behind it, and a hand cursor there promises a click that does nothing. */
.offer:has(.offer-pick:not(.offer-pick-flat)) {
    cursor: pointer;
}

/* Chosen, read at the size of the card. The tint on the price block alone was easy to miss in a
   row of three, which is the moment somebody looks up to find the button. */
.offer:has(.offer-pick input:checked) {
    border-color: var(--accent);
    box-shadow: var(--shadow);
}

/* The card's title. It carries the weight the unit count used to, because the two said the same
   thing and the name is the one the cart and the invoice also use. */
.offer-name {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    font-size: 1.2rem;
    font-weight: 700;
    gap: 0.5rem;
    letter-spacing: -0.01em;
}

/* Air above the price panel. Not a border here — .offer-pick draws its own box, and a rule on
   top of it reads as a panel with a thicker lid. The divider goes above the headings instead. */
.offer-pick,
.offer-unpriced {
    margin-top: 0.5rem;
}

/* The list headings, and the rule that separates the price from what you get for it. Small,
   spaced and quiet: they label a list rather than announcing a section, and at bold body size
   every card read as four headings stacked. */
.offer-heading {
    border-top: 1px solid var(--line);
    color: var(--ink-soft);
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    margin-top: 0.65rem;
    padding-top: 0.9rem;
    text-transform: uppercase;
}

.offer-featured .offer-heading {
    border-top-color: color-mix(in srgb, #ffffff 25%, transparent);
    color: color-mix(in srgb, #ffffff 70%, transparent);
}

/* The plan the shop is on already. Marked on the card as well as in the pill beside its name,
   so the one a reader is looking for is findable without reading every heading. */
.offer-current {
    border-color: var(--accent-bright);
}

/* --- The recommended plan ----------------------------------------------------

   The one card that does not look like the others, in Shelli's purple. — and then

   Inverted, not merely tinted. A slightly stronger border is what the current plan already
   wears, and a recommendation that looks like a neighbouring state is not a recommendation. The
   whole card flips, which is also why every colour inside it has to be restated: the tokens here
   are tuned for dark ink on a white card, and a card that only changed its background would draw
   grey body text and a grey price on purple.

   Which plan this is comes off the catalogue row, never a plan key written into a stylesheet. */
.offer-featured {
    background: linear-gradient(160deg, var(--accent) 0%, var(--accent-dark) 100%);
    border-color: var(--accent-dark);
    color: #ffffff;
}

.offer-featured .offer-name,
.offer-featured .offer-price {
    color: #ffffff;
}

/* Not pure white: at this size a full-strength secondary line competes with the price above it.
   Mixed against the card rather than given an opacity, so text stays crisp. */
.offer-featured .offer-note,
.offer-featured .field-hint,
.offer-featured .offer-features li {
    color: color-mix(in srgb, #ffffff 78%, transparent);
}

.offer-featured .offer-features .feature-cost {
    color: color-mix(in srgb, #ffffff 62%, transparent);
}

/* The saving stays the good-news colour, lightened to carry on purple — the green that reads on
   white is 2.4:1 here, which is a sentence somebody would have to lean in to read. */
.offer-featured .offer-saving {
    color: #7ff0c4;
}

/* The price block on the dark card: a translucent panel rather than a white one, so the purple
   still reads through it and the card holds together as one surface. */
.offer-featured .offer-pick {
    background: color-mix(in srgb, #ffffff 10%, transparent);
    border-color: color-mix(in srgb, #ffffff 28%, transparent);
}

.offer-featured .offer-pick:hover {
    border-color: color-mix(in srgb, #ffffff 55%, transparent);
}

.offer-featured .offer-pick:has(input:checked) {
    background: color-mix(in srgb, #ffffff 18%, transparent);
    border-color: #ffffff;
}

.offer-featured .offer-pick:has(input:focus-visible) {
    outline-color: #ffffff;
}

.offer-featured .offer-unpriced {
    border-color: color-mix(in srgb, #ffffff 30%, transparent);
}

/* The badge. White on purple rather than the green the other pills use: on this card green is
   already spoken for by the saving, and two greens saying different things is one too many. */
.pill-featured {
    background: #ffffff;
    color: var(--accent-dark);
}

/* The current-plan pill has to be restated on the dark card. Its own rule is a pale green
   ground with dark green text, which on purple is a light rectangle with text nobody can read. */
.offer-featured .pill-good {
    background: color-mix(in srgb, #ffffff 22%, transparent);
    color: #ffffff;
}

/* ⚠ The whole card is the control, and this is what makes it one.

   A label activates its input wherever the label is painted, and a pseudo-element is painted as
   part of it - so one absolutely positioned box over the card turns every part of the card into
   the radio's target without nesting anything. A <label> around the card itself would be the
   obvious way and is not allowed: its content model is phrasing content, and the card holds
   paragraphs and lists.

   Only where there is something to choose. `.offer-pick-flat` is the same panel with no radio in
   it - the Admin storefront preview, the public price list, a card outside its renewal window -
   and an overlay there would swallow clicks on behalf of a control that does not exist.

   ⚠ **The card's text stops being selectable**, because the overlay sits above it. That is
   the price of this pattern and it is paid deliberately: a plan card is read and decided on, and
   nobody copies a feature list out of one. Anything that must stay clickable inside a card has to
   be lifted above the overlay, and today nothing is - the buy button is below the grid.

   Keyboard reaches it the same way it always did: the radio is still in the tab order and the
   focus ring is drawn on the block below. */
.offer-pick:not(.offer-pick-flat)::before {
    content: "";
    inset: 0;
    position: absolute;
}

/* The price block is the part that looks like a control, and it stays the part that carries the
   chosen and focused marks. A radio with a 1.25rem figure beside it was a target the size of the
   radio; this makes the price itself pressable, which is where the eye already is. */
.offer-pick {
    align-items: flex-start;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    cursor: pointer;
    display: flex;
    gap: 0.6rem;
    padding: 0.7rem;
    transition: border-color 0.12s ease, background 0.12s ease;
}

.offer-pick:hover {
    border-color: var(--accent-bright);
}

/* Drawn from the input rather than from :hover, so the chosen card still reads as chosen after
   the pointer has moved away — which is the moment somebody looks for the submit button. */
.offer-pick:has(input:checked) {
    /* Mixed rather than named, the way --ring is: a fourth purple in the palette would be a
       fourth thing to keep in step the day the brand moves. */
    background: color-mix(in srgb, var(--accent) 7%, var(--surface));
    border-color: var(--accent);
}

/* The ring belongs on the block, not on the radio inside it: the radio is small and sits at the
   edge, so a keyboard reader would otherwise get a mark they have to hunt for. */
.offer-pick:has(input:focus-visible) {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* The same panel with nothing to choose - the Admin preview, a card outside its renewal window,
   and every card on the public price list, where the reader has no account to buy with yet.

   The class was worn for months and styled nowhere, so those panels kept the pointer cursor and
   the hover border of a control that was not there. It slipped the class check because it is
   written inside a Razor expression, where the scan cannot see it. */
.offer-pick-flat {
    cursor: default;
}

.offer-pick-flat:hover {
    border-color: var(--line);
}

/* A purple button on the purple card is a rectangle nobody can see. Inverted, like everything
   else on this card that had to be restated. */
.offer-featured .button-primary {
    background: #ffffff;
    border-color: #ffffff;
    color: var(--accent-dark);
}

.offer-featured .button-primary:hover {
    background: color-mix(in srgb, #ffffff 88%, var(--accent));
    border-color: color-mix(in srgb, #ffffff 88%, var(--accent));
    color: var(--accent-dark);
}

.offer-price-block {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
}

.offer-price {
    color: var(--ink);
    font-size: 1.85rem;
    font-weight: 700;
    letter-spacing: -0.03em;
    line-height: 1.1;
}

/* Ticks on what the plan carries, not on what it sells on top: a tick beside a priced add-on
   would read as included, which is the one thing this list must not say. */
/* The tick sits against its own words, not at the far end of the card.

   Two class-plus-element selectors, and this one is written above .offer-features li - which sets
   space-between so an add-on price lines up down the right edge. Equal specificity, so the later
   rule was winning and every included feature rendered as a tick on the left margin with its name
   flung against the right one. It looked like a broken table rather than a list.

   Both classes named, so this out-ranks it wherever it sits in the file. An included feature has
   no price to hold apart from its name; that is the whole difference between the two lists. */
.offer-features.offer-included li {
    justify-content: flex-start;
}

.offer-included li::before {
    color: var(--good);
    content: "✓";
    flex: none;
    font-size: 0.8rem;
}

.offer-featured .offer-included li::before {
    color: #7ff0c4;
}

/* The discount in money. Green because it is the good news on the card, and small because it is
   an argument rather than the price — the figure above it is what gets charged. */
.offer-saving {
    color: var(--good);
    font-size: 0.8rem;
    font-weight: 600;
}

/* A gap in the catalogue, drawn as a gap. Without the dashes it reads as a sentence somebody
   left in the card rather than as the absence of the price that should be there. */
.offer-unpriced {
    border: 1px dashed var(--line);
    border-radius: var(--radius);
    padding: 0.7rem;
}

.offer-note {
    color: var(--ink-faint);
    font-size: 0.8rem;
    margin: 0;
}

/* What ordering this plan does to the shop — a sum to pay today, or a date the till changes on.
   Louder than the notes around it because it is a consequence rather than a description, and it
   is the one line on the card a customer can be surprised by afterwards. */
.offer-change {
    color: var(--ink-soft);
    font-weight: 600;
    margin-top: 0.5rem;
}

/* Inside the recommended card the ink is already light, so the same rule would disappear. */
.offer-featured .offer-change {
    color: inherit;
    opacity: 0.9;
}

/* What a plan carries, and what it will sell on top. Two of these sit in one card, so they are
   tight and unbulleted — a plan with nine features should read as a list somebody scans, not as
   nine paragraphs. */
.offer-features {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    list-style: none;
    margin: 0;
    padding: 0;
}

.offer-features li {
    color: var(--ink-soft);
    display: flex;
    font-size: 0.85rem;
    gap: 0.5rem;
    justify-content: space-between;
}

/* The price of an add-on, held apart from its name so the column of figures lines up down the
   card and two plans can be compared by running an eye down them. */
.offer-features .feature-cost {
    color: var(--ink-faint);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* What the plan hands over every month — the thing that makes one a combo, and the line that
   separates two plans somebody is choosing between. Ticked and darker than the feature list under
   it, because it is what they are buying rather than what they can do with it. */
.offer-bundle li {
    align-items: baseline;
    color: var(--ink);
    font-size: 0.9rem;
    font-weight: 600;
    gap: 0.45rem;
    justify-content: flex-start;
}

.offer-bundle li::before {
    color: var(--good);
    content: "✓";
    font-size: 0.85rem;
}

.offer-featured .offer-bundle li {
    color: #ffffff;
}

.offer-featured .offer-bundle li::before {
    color: #7ff0c4;
}

/* Pushed to the bottom so buttons line up across cards of different heights, whatever length of
   description or how many notes each one carries. */
.offer-foot {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: auto;
    padding-top: 0.5rem;
}

/* One column on a phone. Two 230px cards plus a gap is wider than a handset, and the grid would
   otherwise shrink them until the prices wrapped mid-figure. It moved with the cards: left in
   customer.css this was the one rule the Admin preview would have been missing, and it would have
   shown as a preview that reflows differently from the page it is previewing. */
@media (max-width: 640px) {
    .offer-grid {
        grid-template-columns: 1fr;
    }
}

/* The storefront preview's frame. The cards inside wear .shell-customer, which borrows the whole
   Shelli palette — including its paper — so without a ground of that colour they float on the
   Admin Portal's own and read as a rendering fault rather than as a preview. */
.storefront-preview {
    background: var(--paper);
    border: 1px dashed var(--line);
    border-radius: var(--radius);
    padding: 1.25rem;
}

/* Buying another till onto a term already running. Set apart from the list above it with a rule
   rather than a card of its own: it belongs to the devices, and a second card would read as a
   second subject on a page that already has four. */
.slot-buy {
    border-top: 1px solid var(--line);
    margin-top: 1.1rem;
    padding-top: 1.1rem;
}

.slot-buy h3 {
    font-size: 1rem;
    margin: 0 0 0.35rem;
}

.slot-buy label {
    display: block;
    font-size: 0.82rem;
    font-weight: 600;
    margin-bottom: 0.3rem;
}

/* The field is a quantity, not a sentence — sized to what it holds so it does not read as a box
   waiting for a name. Wraps under the field on a narrow screen rather than shrinking it. */
.slot-buy form {
    align-items: end;
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    margin-top: 0.8rem;
}

.slot-buy input {
    max-width: 7rem;
}

/* Extra tills chosen with the term. Sits between the grid and the button because it is part of the
   same decision — put after the button it would read as a setting rather than as something being
   bought. */
/* One row: the label, the number, and the reload that re-prices the grid. The hint drops onto its
   own line under all three, which is what the basis flag below does. */
.plan-extras {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    margin: 0 0 1rem;
}

.plan-extras label {
    font-size: 0.82rem;
    font-weight: 600;
}

.plan-extras input[type="number"] {
    max-width: 6rem;
}

.plan-extras .field-hint {
    flex-basis: 100%;
    margin: 0;
}

/* --- The reporting area ------------------------------------------------------ */

/*
    The two downloads opposite the heading, as one flex child rather than two.

    Without the wrapper the page header's `justify-content: space-between` treats each button as
    its own child and throws them to opposite ends of the row, with the heading stranded between
    them.
*/
.report-downloads {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

/*
    The four windows anybody actually asks for, under the two date boxes.

    Links rather than buttons, and deliberately quieter than the row above them. These are
    shortcuts into a control that is already on the screen; drawn as buttons they would read as a
    second way of choosing a period, competing with the one that decides it.

    The negative top margin pulls them up under the filter bar they belong to — that bar carries a
    margin meant to clear a table, and a gap of two full margins reads as two separate controls.
*/
.filter-presets {
    display: flex;
    flex-wrap: wrap;
    font-size: 0.875rem;
    gap: 1rem;
    margin: -0.75rem 0 1.25rem;
}
