@font-face {
    font-family: 'Audiowide';
    src: url('../fonts/Audiowide-Regular.ttf') format('truetype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

/* lpdf's wordmark face, the same file the product site serves — keep the two in step.
   A serif on a document product says what it is, the way the orange does.

   The descriptor says 400 because that is what the file holds; Radley publishes no
   bold. The lockup asks for 600 and the browser thickens these outlines itself, so
   declaring the face any heavier would match the request and give back the regular.
   Nothing else here uses the face. */
@font-face {
    font-family: 'Radley';
    src: url('../fonts/Radley-Regular.ttf') format('truetype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

/* The figures face, for prices — the same file the product site serves, so a tier costs
   the same in the same face whichever side of the checkout the customer is standing on.
   A mono gives every digit one width, so a column of prices lines up without being told
   to. Variable, so one file covers the range. */
@font-face {
    font-family: 'JetBrains Mono';
    src: url('../fonts/JetBrainsMono-Variable.ttf') format('truetype');
    font-weight: 100 800;
    font-style: normal;
    font-display: swap;
}

/* ── Theme tokens (mirrors lpdf.io pages) ──────────────────────────────────────
   Cool neutral, and the same values as the pages styles.css — keep the two in
   step. Every neutral below is one OKLCH hue, 258. The set used to run at hue
   106, a yellow-green, which read as faded paper in light and as brown in dark;
   at low lightness a warm grey has nowhere to go but brown.

   The three text tiers are set by contrast against --bg rather than by eye,
   and the light and dark ramps are matched tier for tier, so a page does not read
   louder in one theme than the other:

       text 16:1 · muted 7:1 · subtle 4.6:1

   --text-subtle carries the footer links, the copyright and other small print, so
   it is held at 4.6:1 — the old #95958f measured 2.69:1 on this ground, which is
   below the 3:1 floor for any text at all.

   One token means something different here than on the product sites: --bg-sunken
   is this file's hover and inset surface and is what something on --bg-elev moves
   to under the pointer, where on the pages it is the bottom of the stack. It used
   to be an alias of --bg-elev, which made every row hover a no-op; it is now a
   real step off --bg-elev in both themes — down in light, up in dark, because that
   is the direction that reads as a change on each ground.

   The house mark in the header sits on --chrome and measures 4.19 there in dark
   and 4.64 in light. --brand is held at low chroma so the house does not compete
   with the products, which leaves it balanced across the two grounds instead of
   reaching for either; it only measured 3.27 in dark while the header was
   --bg-elev, so do not lighten --chrome in dark without re-measuring the mark. */
:root {
    color-scheme: light;
    --bg: #f5f8fb;
    --bg-elev: #ffffff;
    --bg-sunken: #f2f4f8;
    /* The surface of the fixed nav, the product header and the footer. Light keeps it on the
       raised surface; dark sinks it below the page — see the note there. */
    --chrome: var(--bg-elev);
    --border: #cdd1d7;
    --text: #141619;
    --text-muted: #4e5258;
    --text-subtle: #6a6e74;

    /* The empty half of the licence term bar. Its own token because --bg-sunken cannot do this
       job: the bar sits on .license-card__body, which is --bg, and in light the two measure 1.03
       apart — the unfilled part was invisible, so the bar only ever showed its fill and read as a
       rule rather than a gauge. This is a real recess in both themes (light 1.27, dark 1.37) and
       still leaves every fill colour above the 3:1 graphic floor against it (worst 3.30 / 3.07).
       Darkening it further in light starts to fail --danger-soft, which is the binding limit.

       --track-edge is the capsule's outline. The plain --border cannot draw it: in dark that token
       and --track are the same value, so the box had no edge at all, and in light the two sit 1.13
       apart, which is not a ring anyone sees. This one steps off the track by a comparable amount
       in both themes (light 1.27, dark 1.35) so the bar reads as a container either way. */
    --track: #d8dee6;
    --track-edge: #bfc6d1;

    /* ── The one colour to change ──
       The house colour. Every brand-coloured thing in the portal — the mark, focus
       rings, button fills, links, the tier ladder — resolves from this single value,
       so moving the brand is editing this line and re-running
       `logos/bin/build-portal-mark.mjs` to repaint the artwork to match.

       #467e7a: hue 190 held at chroma 0.060, a little over half what the hue can
       carry. Low enough to read as a tinted neutral rather than a colour, which is
       what lets the house sit behind four product colours without competing, and
       what keeps a focus ring in it from reading as "valid" the way a saturated
       green-blue would.

       Measured against the surfaces below — 4.64 on the light nav, 4.35 on the light
       page, 3.76 on the dark page, 3.27 on the dark nav. The last is the tight one:
       it clears the 3:1 a graphic is held to, so rings and marks are fine, but
       --bg-elev in dark cannot be lightened without re-measuring it.

       --brand-on is what sits *on* a brand fill. White clears it at 4.64, which is
       the first time that has been true here — the orange this replaced managed only
       3.41, which is why every filled control had to carry a dark label instead. */
    --brand:    oklch(0.553 0.060 190);
    --brand-on: oklch(1 0 0);

    /* Brand-coloured *text* — links, the Professional pill label, the active admin
       nav item. Deliberately the brand itself rather than a darkened variant: one
       colour is the whole palette, which is the point.

       The cost, stated so it is a choice and not an accident: at 4.35 on light and
       3.76 on dark this sits under the 4.5 small text wants. Everything graphic
       clears its own 3:1 floor; it is only the small brand-coloured text. If that
       ever needs fixing, the answer is to stop painting small text in the brand —
       links to --text with the underline carrying them — rather than to reintroduce
       a second token here. */
    --brand-text: var(--brand);

    /* The product in scope, for the things that are the product speaking rather than the
       portal: the tier pill on a licence, the get/purchase button it is bought with, and
       the checkout acknowledgement — both the link to the product's own licence and the
       tick that agrees to it.
       Everything around them — dialogs, tabs, focus rings, the rest of the links, and
       the checkout's own confirm button, which is house-coloured because it opens a
       house-coloured Paddle overlay — is portal chrome and stays on --brand. A licence
       page that painted its controls in the product's colour would be claiming the
       portal belongs to one product, which is the thing this split exists to avoid.

       Set by `productColourScope` in `src/products.ts`, which every product-scoped
       surface spreads onto its own root. Defaults to the house pair, so a control
       using these is correct on a page that has no product at all — /account, admin,
       sign-in.

       Every value is measured: --product-on is the label tone that clears 4.5:1 on
       its fill, because no product colour here can carry a white one, and the two
       --product-text-* are the fill itself walked in lightness until it clears 4.5:1
       as small text on the page. A pill's outline is a graphic at 3:1 and can be the
       raw colour; its label cannot. See the note on `ProductConfig.color`.

       Inline custom properties cannot branch on the theme, so `productColourScope`
       hands both text tones over at once and the rules below pick which to read.
       Unscoped, all four fall back to the house pair — and --brand-text is knowingly
       under 4.5, which is the house's own choice, documented above and not one the
       products share. */
    --product:    var(--brand);
    --product-on: var(--brand-on);
    --product-text-light: var(--brand-text);
    --product-text-dark:  var(--brand-text);

    --success: oklch(58% 0.12 155);

    /* Two weights of one alarm hue, carrying the licence term bar and the state badge beside it.
       Red is the only hue nothing else claims — the house aqua, lpdf's orange, authstack's blue
       and conseal's periwinkle are all spoken for — so it reads as attention whichever product's
       licences are on screen. It is reserved: a future product cannot take it.

       --danger-soft is the tier's renewal window, --danger a term that has actually ended. Same
       hue, separated by chroma rather than lightness, so the ordering holds in both themes: the
       duller one is the earlier warning. Using --danger for both would say "your licence has
       lapsed" to someone who still has a month of it left.

       Both are set against --bg-sunken, the progress track, at the 3:1 graphic floor:

           light  soft 4.06 · full 4.38        dark  soft 4.20 · full 4.38

       The light --danger resolves to #dc2626, the red the danger buttons and badges already use,
       so nothing shifts in light. Dark needs its own lighter value — that same red measures 2.83
       on the dark track, under the floor, which is why the badge was barely legible there. */
    --danger-soft: oklch(0.58 0.1 27);
    --danger: oklch(0.577 0.215 27);

    --nav-h: 48px;
    /* Shared horizontal inset for the fixed nav, the product header, and the page body —
       keep these three lined up by keeping them on this one value. */
    --portal-gutter: 2rem;

    /* Tiers used to carry three unrelated hues, two of which belong to other products
       in the set. They now step in weight through one colour on a constant edge:
       Community is outlined in it with a muted label, Professional is outlined in it
       with a full-strength one, Enterprise is filled with it. The edge is what says
       "this is a tier"; the label's weight is what says which one. Community carried a
       grey edge until it stopped reading as part of the same set as the two above it.

       That colour is --product, not --brand: a tier belongs to the thing being
       licensed, so an lpdf licence wears lpdf's orange wherever it is shown. Hue is
       product identity, weight is the tier.

       The Professional label is the colour too, not --text. It cannot be the raw
       --product: at 3.20 on the light page orange is fine as an edge, where 3:1 is the
       bar for a graphic, and well short of the 4.5 that small text needs. That is what
       --product-text-* is for — the same hue carried far enough in lightness to pass,
       per theme, measured per product. This file used to step the label in weight
       instead and say a token per product was not worth it; the ladder now matches the
       lpdf pricing page, which has always set that label in the brand, so a plan looks
       the same on both sides of the checkout. */
}
[data-theme="dark"] {
    color-scheme: dark;
    /* Elevation runs toward light in both themes. On a dark ground a darker panel
       reads as a hole punched through rather than as something raised, so every card
       climbs off the page instead of sinking below it.

       The two headers and the footer are the deliberate exception, as on GitHub: they
       are the frame the page sits in rather than a panel on it, so they sink below it. Same
       neutral hue as the rest (oklch 0.16 0.010 258), 1.11:1 off --bg — about the step
       GitHub takes. Text tiers measured on it: 16.9 · 7.1 · 4.7, so all three hold. */
    --bg: #171a1f;
    --bg-elev: #23262b;
    --bg-sunken: #2a2e33;
    --chrome: #0b0d12;
    --border: #2f3339;
    --text: #eceff3;
    --text-muted: #989da4;
    --text-subtle: #797d83;

    /* Lighter than the page, same as every other surface in this theme. Coincides with --border's
       value here; kept as its own literal so the two can move apart without silently changing the
       fill contrasts measured against it. */
    --track: #2f3339;
    --track-edge: #41474f;

    /* Lighter than the light-theme pair — see the note there. Elevation runs toward light in
       this theme, and so does anything that has to stay visible against it. */
    --danger-soft: oklch(0.66 0.09 27);
    --danger: oklch(0.68 0.18 27);
}

/* ── Base ── */
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--text); }
body { font-family: var(--rl-font-sans, system-ui, sans-serif); font-size: 16px; line-height: 1.5; -webkit-font-smoothing: antialiased; }
a { color: inherit; text-decoration: none; }

portal-app {
    display: block;
    min-height: 100dvh;
}

/* ── The product lockup: artwork beside text, not one picture of both ──────────
   The mark is a single colour on transparency, so one file serves both themes and no
   light/dark pair is needed. The word is text set in the product's own face, which is
   what lets it be selected, read by a screen reader, and re-set by editing a stylesheet
   rather than re-rendering a PNG. It inherits the page's text colour, and that is what
   makes the pair work on either ground.

   Everything is sized off the lockup's own font-size, so the mark and the word stay tied
   to each other at whatever size a surface sets. The mark is drawn a little taller than
   the cap height so it leads the pair rather than sitting level with it — the same ratio
   the product site's nav uses, and the one `logos/bin` draws the showcase lockups at.

   The face is lpdf's. A second live product sets its own off `.product-lockup-word`, the
   way the product sites do — the wordmark face is part of a product's identity, not
   something the portal picks once for all of them. */
.product-lockup {
    display: inline-flex;
    align-items: center;
    gap: 0.375em;
    text-decoration: none;
}
.product-lockup-mark { height: 1.333em; width: auto; display: block; flex: none; }
.product-lockup-word {
    font-family: 'Radley', Georgia, serif;
    font-weight: 600;
    font-size: 1em;
    line-height: 1;
    letter-spacing: 0;
    color: var(--text);
    white-space: nowrap;
}

/* ── Theme toggle button ── */
.theme-btn {
    width: 34px; height: 34px;
    display: grid; place-items: center;
    border: none; background: none; cursor: pointer;
    border-radius: 7px; color: var(--text-muted);
    transition: color .15s, background .15s;
}
.theme-btn:hover { color: var(--text); background: var(--bg-sunken); }

/* ── Sign-in card ── */
.sign-in-card {
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 2rem;
    box-shadow: 0 4px 24px -4px rgba(0,0,0,.06), 0 1px 4px -1px rgba(0,0,0,.04);
}
[data-theme="dark"] .sign-in-card {
    box-shadow: 0 4px 24px -4px rgba(0,0,0,.4), 0 1px 4px -1px rgba(0,0,0,.2);
}

/* ── authstack-sign-in buttons (Light DOM — fully styleable here) ── */
authstack-sign-in {
    display: flex;
    flex-direction: column;
    gap: 0.625rem;
}
/* The magic-link submit is styled as one more entry in this list rather than as a primary
   action: the four ways in are alternatives, not a recommendation plus three fallbacks, and a
   filled brand button next to three outlined ones read as the house endorsing email over the
   providers. It is disabled until the field holds something, which is what marks it as the
   live control once the reader has committed to that route. */
.authstack-sign-in__btn,
.authstack-sign-in__ml-submit {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    width: 100%;
    padding: 0.625rem 1.25rem;
    font-size: 0.9375rem;
    font-weight: 500;
    font-family: var(--rl-font-sans, system-ui, sans-serif);
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--bg-elev);
    color: var(--text);
    cursor: pointer;
    transition: background .15s, border-color .15s, opacity .15s;
    box-sizing: border-box;
    text-align: left;
}
/* The edge picks up the house colour under the pointer, which is the same answer the
   mail field gives on focus — reaching for a control is what turns it brand-coloured.
   The fill alone was too quiet to read as a hover here: --bg-elev to --bg is a 1.03
   step in light. It stays an outline rather than a fill because these buttons carry
   Google's, Microsoft's and GitHub's own marks, and a brand-coloured ground behind
   somebody else's logo reads as a claim on it. Measured on the hover ground: 4.35 in
   light, 3.76 in dark, both clear of the 3:1 a border is held to. */
.authstack-sign-in__btn:hover:not(:disabled),
.authstack-sign-in__ml-submit:hover:not(:disabled) { background: var(--bg); border-color: var(--brand); }
.authstack-sign-in__btn:active:not(:disabled),
.authstack-sign-in__ml-submit:active:not(:disabled) { opacity: .8; }
.authstack-sign-in__btn--disabled,
.authstack-sign-in__btn:disabled,
.authstack-sign-in__ml-submit:disabled { opacity: .5; cursor: not-allowed; }

.authstack-sign-in__icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    flex-shrink: 0;
}
.authstack-sign-in__label { flex: 1; text-align: center; }

.authstack-sign-in__spinner {
    display: inline-block;
    width: 16px;
    height: 16px;
    border: 2px solid color-mix(in oklch, currentColor 30%, transparent);
    border-top-color: currentColor;
    border-radius: 50%;
    animation: authstack-spin .6s linear infinite;
    flex-shrink: 0;
}
@keyframes authstack-spin { to { transform: rotate(360deg); } }

.authstack-sign-in__divider {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    color: var(--text-subtle);
    font-size: 0.75rem;
}
.authstack-sign-in__divider::before,
.authstack-sign-in__divider::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--border);
}
hr.authstack-sign-in__divider {
    border: none;
    border-top: 1px solid var(--border);
    margin: 0;
}
hr.authstack-sign-in__divider::before,
hr.authstack-sign-in__divider::after { display: none; }

/* Magic-link form */
.authstack-sign-in__ml { display: flex; flex-direction: column; gap: 0.625rem; }
.authstack-sign-in__ml-field { display: flex; flex-direction: column; gap: 0.375rem; }
.authstack-sign-in__ml-label { font-size: 0.8125rem; color: var(--text-muted); }
.authstack-sign-in__ml-input {
    width: 100%;
    height: 36px;
    padding: 0 0.75rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 7px;
    color: var(--text);
    font-size: 0.875rem;
    font-family: inherit;
    outline: none;
    box-sizing: border-box;
    transition: border-color .15s;
}
.authstack-sign-in__ml-input:focus { border-color: var(--brand); }
.authstack-sign-in__ml-input::placeholder { color: var(--text-subtle); }
.authstack-sign-in__ml-error { font-size: 0.8125rem; color: var(--wa-color-danger-primary, #dc2626); }
.authstack-sign-in__ml-sent {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    text-align: center;
    padding: 1rem 0;
    color: var(--text-muted);
}
.authstack-sign-in__ml-sent-icon { color: var(--success); }
.authstack-sign-in__ml-sent-title { margin: 0; font-size: 0.9375rem; font-weight: 600; color: var(--text); }
.authstack-sign-in__ml-sent-body { margin: 0; font-size: 0.8125rem; }
.authstack-sign-in__ml-retry {
    background: none;
    border: none;
    padding: 0;
    color: var(--brand);
    font-size: 0.8125rem;
    font-family: inherit;
    cursor: pointer;
    text-decoration: underline;
}

/* ── authstack-register (Light DOM — fully styleable here) ── */
authstack-register {
    display: block;
}
.authstack-register {
    display: flex;
    flex-direction: column;
    gap: 0.875rem;
}
.authstack-register--loading,
.authstack-register--error {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    padding: 1rem 0;
    color: var(--text-muted);
    font-size: 0.875rem;
    text-align: center;
}
.authstack-register__back {
    color: var(--brand);
    font-size: 0.8125rem;
    text-decoration: underline;
}
.authstack-register__field {
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
}
.authstack-register__label {
    font-size: 0.8125rem;
    color: var(--text-muted);
}
.authstack-register__input {
    width: 100%;
    height: 36px;
    padding: 0 0.75rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 7px;
    color: var(--text);
    font-size: 0.875rem;
    font-family: inherit;
    outline: none;
    box-sizing: border-box;
    transition: border-color .15s;
}
.authstack-register__input:focus { border-color: var(--brand); }
.authstack-register__input::placeholder { color: var(--text-subtle); }
.authstack-register__input--email { opacity: 0.6; cursor: not-allowed; }
.authstack-register__error {
    font-size: 0.8125rem;
    color: var(--wa-color-danger-primary, #dc2626);
}
.authstack-register__submit {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.625rem 1rem;
    background: var(--brand);
    border: none;
    border-radius: 8px;
    color: var(--brand-on);
    font-size: 0.9375rem;
    font-weight: 500;
    font-family: inherit;
    cursor: pointer;
    transition: opacity .15s;
    margin-top: 0.25rem;
}
.authstack-register__submit:hover:not(:disabled) { opacity: .85; }
.authstack-register__submit:disabled { opacity: .4; cursor: not-allowed; }
.authstack-register__spinner {
    display: inline-block;
    width: 16px;
    height: 16px;
    border: 2px solid color-mix(in oklch, currentColor 30%, transparent);
    border-top-color: currentColor;
    border-radius: 50%;
    animation: authstack-spin .6s linear infinite;
    flex-shrink: 0;
}

/* ── Footer links ── */
.sign-in-footer a {
    font-size: 0.875rem;
    color: var(--text-subtle);
    transition: color .15s;
}
.sign-in-footer a:hover { color: var(--text-muted); }

/* ── User dropdown trigger ── */
.user-dropdown-trigger {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.25rem 0.625rem;
    background: none;
    border: 1px solid var(--border);
    border-radius: 7px;
    color: var(--text);
    font-size: 0.875rem;
    font-family: inherit;
    cursor: pointer;
    transition: background .15s, border-color .15s;
    white-space: nowrap;
    height: 32px;
}
.user-dropdown-trigger:hover { background: var(--bg-sunken); border-color: var(--text-subtle); }

/* ── User avatar menu trigger (portal nav) ── */
.user-avatar-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: 100px;
    background: none;
    color: var(--text-muted);
    font-family: inherit;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    cursor: pointer;
    transition: background .15s, border-color .15s, color .15s;
}
.user-avatar-btn:hover,
.user-avatar-btn[aria-expanded="true"] { background: var(--bg-sunken); border-color: var(--text-subtle); color: var(--text); }

.user-dropdown-header {
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
    padding: 0.625rem 1rem;
    pointer-events: none;
}
.user-dropdown-name {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--text);
}
.user-dropdown-via {
    font-size: 0.75rem;
    color: var(--text-muted);
}
/* ── Nav ── */
.nav {
    position: fixed;
    top: 0; left: 0; right: 0;
    z-index: 100;
    height: var(--nav-h);
    overflow: hidden;
    /* Solid, on its own surface rather than glass: glass reads as a layer by sampling
       what scrolls under it, which is a different idea from standing on its own
       surface, and the two cancel each other out when both are applied. */
    background: var(--chrome);
    border-bottom: 1px solid var(--border);
}
.nav-inner { height: 100%; padding: 0 var(--portal-gutter); }
/* Layout (row, gap) is relayout's — see `wordmark.ts`. Only type and colour are set here. */
.nav-logo {
    font-family: 'Audiowide', var(--rl-font-sans, system-ui, sans-serif);
    font-size: 1rem;
    font-weight: 400;
    line-height: 1;
    color: var(--text);
    white-space: nowrap;
}
/* Sized off the wordmark beside it rather than in px, so the pair keeps its balance
   if the header type ever changes. The mark carries its own moss in the file. */
.nav-logo-mark  { height: 1.35em; width: auto; display: block; }
.nav-logo-code  { color: var(--text-muted); }
.nav-logo-sense { color: var(--text); }
.nav-links a { font-size: 0.9375rem; color: var(--text-muted); transition: color .15s; }
.nav-links a:hover { color: var(--text); }
.nav-links a.active { color: var(--text); font-weight: 500; }
.nav-section-label { font-size: 0.875rem; color: var(--text-subtle); letter-spacing: 0.08em; text-transform: uppercase; padding-left: 1rem; border-left: 1px solid var(--border); }

/* Multi-product switcher — same divider + all-caps treatment as .nav-section-label, but interactive. */
.nav-product-switcher {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0 0 0 1rem;
    border: none;
    border-left: 1px solid var(--border);
    background: none;
    font-family: inherit;
    font-size: 0.875rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-subtle);
    cursor: pointer;
    transition: color .15s;
}
.nav-product-switcher:hover { color: var(--text-muted); }

/* ══ Admin area ═══════════════════════════════════════════════════════════════
   The admin bundle ships without Web Awesome, so these classes give the internal
   tool its surface vocabulary. Structure, spacing, width and the type scale come
   from relayout (rl-*); what's here is only colour, border, and the form/table
   chrome relayout doesn't own — mirroring how the customer pages lean on
   .license-card / .share-row / .grant-chip. */

.admin-shell { height: 100dvh; padding-top: var(--nav-h); box-sizing: border-box; background: var(--bg); }
.admin-main { min-width: 0; color: var(--text); }

.admin-sidebar {
    flex-shrink: 0;
    width: 13rem;
    padding: 1.5rem 0.75rem;
    border-right: 1px solid var(--border);
    background: var(--bg-elev);
}
/* Pinned to the foot of a full-height sidebar by the auto margin, not by a fixed position:
   the shell is 100dvh and the nav is short, so the slack belongs above this group, not inside
   it -- the launchers and the version move as one unit rather than the version alone drifting
   away from them. */
.admin-sidebar-footer {
    margin-top: auto;
}

/* Aligned with the nav links' text, and in the footer's tone -- present when looked for,
   quiet otherwise. */
.admin-sidebar-version {
    padding: 0.5rem 0.625rem;
    font-size: 0.6875rem;
    color: var(--text-subtle);
    opacity: 0.7;
}

.admin-nav-link {
    display: block;
    padding: 0.5rem 0.625rem;
    border-radius: 6px;
    font-size: var(--rl-text-m);
    color: var(--text-muted);
    text-decoration: none;
}
.admin-nav-link:hover { color: var(--text); }
.admin-nav-link--active {
    background: color-mix(in srgb, var(--brand) 12%, transparent);
    color: var(--brand);
    font-weight: 600;
}

.admin-title { margin: 0; font-size: var(--rl-text-xl); font-weight: 700; color: var(--text); }
.admin-heading { margin: 0; font-size: var(--rl-text-l); font-weight: 600; color: var(--text); }
.admin-subhead { margin: 0; font-size: var(--rl-text-s); font-weight: 600; color: var(--text-muted); }

/* Uppercase section label — shared by the customer license list ("YOUR LICENSES") and
   every grouped section in the admin area, so the two portals read alike. */
.eyebrow {
    margin: 0;
    font-size: var(--rl-text-s);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
}

/* Section surface — matches .license-card so admin cards and portal license cards
   sit on the same shape. */
.admin-card {
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 1.5rem;
}
.admin-card--tight { padding: 0.875rem; }

/* One dashboard metric, on its own surface. A figure is a thing in its own right, so it gets
   its own edges rather than sitting loose on a section card with its neighbours — which left
   nothing but whitespace to say where one number stopped and the next began. Lighter padding
   than .admin-card: these tile densely, several to a row. */
.admin-stat-card {
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 1rem 1.125rem;
    transition: border-color .15s;
}

/* A stat card that navigates somewhere needs to look like it does — the border made the card
   an object, and an object that responds to the pointer reads as clickable. Neutral for the same
   reason as the licence card's hover edge — see the note there. */
.admin-stat-link:hover .admin-stat-card { border-color: var(--text-subtle); }

/* Two-column detail/form grid. relayout's grid is equal-width auto-fit, so a
   fixed 1fr/1fr grid stays custom — same reason as .account-fields. */
.admin-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }

/* Hairline-divided list. */
.admin-list { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.admin-list__row {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.625rem 0.75rem;
    border-bottom: 1px solid var(--border);
}
.admin-list__row:last-child { border-bottom: none; }
.admin-list__row--link { cursor: pointer; transition: background .12s; }
.admin-list__row--link:hover { background: var(--bg-sunken); }
.admin-arrow { color: var(--text-subtle); }

/* The growing, truncating cell in a flex row — relayout's row primitives don't
   own per-child grow, so this pairs with rl-text-truncate (same pattern the
   customer rows use inline). */
.admin-grow { flex: 1 1 0; min-width: 0; }

/* Data table. */
.admin-table { width: 100%; border-collapse: collapse; font-size: var(--rl-text-m); color: var(--text); }
.admin-table th {
    padding: 0.5rem 0.75rem;
    text-align: left;
    font-weight: 600;
    border-bottom: 2px solid var(--border);
}
.admin-table td { padding: 0.625rem 0.75rem; border-bottom: 1px solid var(--border); }
.admin-table__row { cursor: pointer; transition: background .12s; }
.admin-table__row:hover { background: var(--bg-sunken); }

/* A stat tile that navigates — a real link, so it is reachable and announced as one. */
/* Radius matches .admin-stat-card so the focus ring traces the card's corners, not a square
   around them — the link box and the card box are the same box. */
.admin-stat-link { display: block; color: inherit; text-decoration: none; border-radius: 10px; }

/* Every row/card that acts as a control is reached by keyboard (tabindex in the component),
   so each needs a focus ring of its own — none of them is a native button. */
.admin-table__row:focus-visible,
.admin-list__row--link:focus-visible,
.admin-stat-link:focus-visible,
.license-card--link:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: -2px;
}

/* Form controls. */
.admin-input,
.admin-select {
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    font: inherit;
    font-size: var(--rl-text-m);
    background: var(--bg-elev);
    color: var(--text);
    box-sizing: border-box;
    outline: none;
}
.admin-input:focus,
.admin-select:focus { border-color: var(--brand); }
.admin-field { font-size: var(--rl-text-s); font-weight: 500; color: var(--text); }

/* Buttons. */
.admin-btn {
    padding: 0.5rem 1rem;
    border: 1px solid transparent;
    border-radius: 6px;
    font: inherit;
    font-size: var(--rl-text-m);
    font-weight: 500;
    cursor: pointer;
    background: var(--bg-elev);
    color: var(--text);
    transition: opacity .15s, background .15s, border-color .15s;
}
.admin-btn:disabled { opacity: .5; cursor: default; }
.admin-btn--primary { background: var(--brand); color: var(--brand-on); }
.admin-btn--outline { background: transparent; border-color: var(--border); }
.admin-btn--danger { background: var(--wa-color-danger-primary, #dc2626); color: oklch(0.98 0 0); }
.admin-btn--warn { background: #f97316; color: oklch(0.98 0 0); }
.admin-btn--sm { padding: 0.25rem 0.625rem; font-size: var(--rl-text-s); }
.admin-btn--outline.admin-btn--danger { background: transparent; color: var(--wa-color-danger-primary, #dc2626); }

/* Chips & badges. */
.admin-chip {
    padding: 2px 7px;
    border-radius: 4px;
    font-size: var(--rl-text-xs);
    white-space: nowrap;
    background: var(--bg-sunken);
    color: var(--text-muted);
}
.admin-badge { padding: 2px 8px; border-radius: 4px; font-size: var(--rl-text-xs); white-space: nowrap; }
.admin-badge--brand { background: color-mix(in oklch, var(--brand) 18%, transparent); color: var(--brand); }
/* On --danger rather than the WebAwesome token, which nothing in the bundle defines, so its
   #dc2626 fallback painted both themes and sat under the contrast floor in dark. This badge
   shares a card with the licence term bar; the two have to be the same red. */
.admin-badge--danger { background: color-mix(in oklch, var(--danger) 18%, transparent); color: var(--danger); }
.admin-badge--success { background: color-mix(in oklch, var(--success) 18%, transparent); color: var(--success); }
.admin-badge--warn { background: color-mix(in oklch, oklch(0.75 0.15 85) 20%, transparent); color: oklch(0.62 0.15 75); }
/* A licence inside its renewal window. Its own modifier rather than --warn, which also carries
   stale keys: this one has to match the term bar directly below it, and key staleness does not. */
.admin-badge--expiring { background: color-mix(in oklch, var(--danger-soft) 18%, transparent); color: var(--danger-soft); }
.admin-badge--muted { background: color-mix(in oklch, var(--text-muted) 16%, transparent); color: var(--text-muted); }

.admin-mono { font-family: var(--rl-font-mono, ui-monospace, 'Cascadia Code', monospace); }

/* Native <dialog> — modal semantics, backdrop and Esc handling come free from the element. */
.admin-dialog {
    padding: 1.5rem;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--bg-elev);
    color: var(--text);
    max-width: 34rem;
    width: calc(100vw - 2rem);
    box-shadow: 0 12px 48px -12px rgba(0, 0, 0, 0.4);
}
.admin-dialog::backdrop { background: rgba(0, 0, 0, 0.45); }

.admin-audit-row { border: 1px solid var(--border); border-radius: 8px; padding: 0.75rem 0.875rem; background: var(--bg-sunken); }
.admin-audit-diff { padding: 6px; border-radius: 4px; overflow: auto; max-height: 80px; background: var(--bg-elev); }
.admin-audit-diff--after { background: color-mix(in oklch, var(--success) 12%, var(--bg-elev)); }

/* ── Container ── */
.container { max-width: 1120px; margin: 0 auto; }

/* ── Legal documents ──────────────────────────────────────────────────────────
   The three Codesense-wide documents here and the license and support policy on
   the product sites all render through md-html and share this treatment. The
   register is deliberately not the register of the rest of the site: a serif
   face, a narrow measure and a quiet heading scale, because these are
   instruments to be read rather than pages to be scanned.

   Keep this block identical to the one in the pages styles.css. The same reader
   crosses between the two — the license there, the terms here — and the seam
   shows. */
.page-legal {
    /* No webfont. The stack asks for whichever text serif the reader's platform
       already ships — Charter on macOS, Sitka on Windows — and lands on Georgia,
       which is on all of them. Self-hosting one face would make the three render
       identically, at the cost of the only blocking download on the page. */
    --legal-serif: Charter, 'Bitstream Charter', 'Sitka Text', Cambria, Georgia, 'Times New Roman', serif;
    /* A step down from --text: a wall of legal prose at full contrast is tiring,
       and the headings need somewhere to step up to. Mixed rather than fixed so
       it follows the palette instead of pinning two more hex values to it. */
    --legal-text: color-mix(in oklch, var(--text) 82%, var(--bg));
    /* Terms, Privacy and Refunds are Codesense-wide and belong to no product, so
       they take the house colour like every other link here. This was a standalone
       blue until 2026-09-13, for the good reason that --brand was then lpdf's orange
       and painting company documents in it said they were lpdf's — and for the
       accidental bad one that the blue chosen, #018cef, was cobalt at full chroma,
       15° from authstack's hue and louder than it. Moving --brand to the house
       colour removes the reason and the accident together. */
    --legal-link: var(--brand);
}

/* One column for the document and for whatever sits above it, so they share a
   left edge. Scoped under main: the footer's own container must stay page-width. */
.page-legal main .container { max-width: 46rem; }

.page-legal md-html {
    display: block;
    font-family: var(--legal-serif);
    font-size: 1rem;
    line-height: 1.75;
    color: var(--legal-text);
}

.page-legal md-html h1 {
    font-size: 2rem; font-weight: 600; letter-spacing: -0.015em; line-height: 1.2;
    color: var(--text); margin: 0 0 0.75rem;
}
/* Every one of these documents opens the same way: title, copyright, then an
   effective date or a contact address. Those two lines are masthead, not prose. */
.page-legal md-html h1 + p,
.page-legal md-html h1 + p + p {
    font-family: var(--rl-font-sans);
    font-size: 0.8125rem; line-height: 1.5; color: var(--text-subtle); margin: 0.125rem 0;
}
/* md-html strips the `---` rules the sources use between sections, so the rule
   comes back on the heading that followed each one. */
.page-legal md-html h2 {
    font-size: 1.25rem; font-weight: 600; letter-spacing: -0.01em; color: var(--text);
    margin: 2.75rem 0 0.875rem; padding-top: 2rem; border-top: 1px solid var(--border);
}
.page-legal md-html h3 {
    font-size: 1.0625rem; font-weight: 600; color: var(--text); margin: 1.75rem 0 0.5rem;
}
.page-legal md-html p  { margin: 0.875rem 0; }
.page-legal md-html ul,
.page-legal md-html ol { margin: 0.875rem 0; padding-left: 1.5rem; }
.page-legal md-html li { margin: 0.375rem 0; }
.page-legal md-html strong { font-weight: 700; color: var(--text); }
.page-legal md-html a {
    color: var(--legal-link); text-decoration: underline; text-underline-offset: 2px;
    text-decoration-color: color-mix(in oklch, var(--legal-link) 40%, transparent);
}
.page-legal md-html a:hover { text-decoration-color: var(--legal-link); }
.page-legal md-html code {
    font-family: var(--rl-font-mono); font-size: 0.8125rem;
    background: var(--bg-sunken); border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px;
}

/* Tables stay in the sans. The privacy policy's recipient and retention tables
   are read down their columns rather than across in sentences, and a serif at
   this size muddies them. */
.page-legal md-html table {
    width: 100%; border-collapse: collapse; margin: 1.5rem 0;
    font-family: var(--rl-font-sans); font-size: 0.875rem; line-height: 1.5;
}
.page-legal md-html th {
    text-align: left; font-weight: 600; color: var(--text);
    padding: 0 1rem 0.5rem 0; border-bottom: 1px solid var(--border);
}
.page-legal md-html td {
    padding: 0.625rem 1rem 0.625rem 0; border-bottom: 1px solid var(--border);
    vertical-align: top;
}
.page-legal md-html tr:last-child td { border-bottom: none; }
.page-legal md-html th:last-child,
.page-legal md-html td:last-child { padding-right: 0; }

/* Horizontal inset shared by the product header and the page body, so both line up with
   each other and — once the page column stops centering — with the fixed nav. */
.portal-gutter { padding-inline: var(--portal-gutter); }

/* ── Product header (marketing chrome above product-scoped pages) ── */
/* The bottom border spans the viewport; the inner column is rl-width-l with the gutter
   OUTSIDE it, matching the page body wrapper in portal.ts <main>. */
/* The product band is chrome, not content: it names which product you are looking at
   and never changes as you move around inside it. So it takes the surface the nav
   takes, and the page below it stays on --bg. A hairline alone had been carrying
   that boundary, which left the band and the content it introduced reading as one
   continuous block. */
site-header { display: block; background: var(--chrome); }
header.site-header { border-bottom: 1px solid var(--border); }
.site-header-tagline { color: var(--text-muted); line-height: 1.35; }
.site-header-links a { font-size: 0.875rem; color: var(--text-subtle); transition: color .15s; }
.site-header-links a:hover { color: var(--text-muted); }

/* ── Site footer ── */
/* `site-footer` is the portal's own thin bar — Terms, Privacy, Refunds — since the portal serves
   every product and has no single product row to show. The lpdf.io pages have their own footer,
   `lpdf-footer`, styled in their stylesheet. */
/* The footer is the same role as the headers — the frame the page sits in, not part of
   it — so it takes their surface. */
/* flex-shrink:0 because the footer is now a sibling of the scrolling <main> in every
   shell, and a flex column would otherwise squeeze it when the viewport is short. */
site-footer { display: block; background: var(--chrome); flex-shrink: 0; }
.footer-links a { font-size: 0.8125rem; color: var(--text-subtle); transition: color .15s; }
.footer-links a:hover { color: var(--text-muted); }
/* Spans the full width on --portal-gutter, the same inset the nav and page body use. No
   .container: the footer renders over content of unknown width, so a page column here would
   only misalign against whatever sits above it. */
.footer-company { border-top: 1px solid var(--border); padding: 0.625rem var(--portal-gutter); }
/* Matches .footer-links beside it — rl-text-s (0.75rem) reads as a mismatch. margin:0 because
   there is no `p { margin: 0 }` reset here, and the default 1em would add ~28px to the band. */
.footer-copy { font-size: 0.8125rem; color: var(--text-subtle); margin: 0; }
/* The deployed release (src/version.ts). A step smaller and fainter than the copyright: there
   for whoever looks for it, invisible to everyone else. */
.footer-version { font-size: 0.6875rem; color: var(--text-subtle); opacity: 0.7; }

/* ── WebAwesome token overrides ── */
/* Map WA brand colour to the portal brand token so tabs, badges, etc. match. These
   two held literal copies of lpdf's orange until 2026-09-13, which meant every
   WebAwesome control kept the old brand after --brand moved and had to be found by
   hand. They read the token now, so there is one value to change and not three.

   Accent is the same value rather than a louder step: the house colour is held at
   chroma 0.060 deliberately, and hue 190 tops out at 0.106, so there is no
   meaningfully more saturated version of it to escalate to. */
:root, .wa-light, .wa-dark {
    --wa-color-brand-primary: var(--brand);
    --wa-color-brand-accent:  var(--brand);
    --wa-color-surface-raised: var(--bg-elev);
}

.license-card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 1rem;
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: 12px;
    /* 1.5rem is the card inset everywhere else — .admin-card and .license-detail-card
       both use it. This one had drifted to 1.25rem, and since the list card and the
       detail card wrap the same header, the identical block sat 4px further in on one
       page than the other. */
    padding: 1.5rem;
}

/* The license number, first thing inside the panel. Quiet, and deliberately not truncating:
   at eight characters there is nothing to clip, and the owner address beside it wraps to its
   own line rather than losing its tail. */
.license-card__id {
    color: var(--text-subtle);
    white-space: nowrap;
    /* A step above the row it sits in, which is `rl-text-s`. This is the thing a customer reads
       out to support, so it outranks the owner address beside it rather than matching it. */
    font-size: var(--rl-text-m);
    letter-spacing: 0.02em;
}

/* The owner/origin line, when it's a link (admin's detail header → the account page). */
.license-card__origin-link {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    color: var(--brand);
    cursor: pointer;
    text-align: left;
}
.license-card__origin-link:hover { text-decoration: underline; }

/* ── Card title ──
   The license's name and description, above the panel rather than inside it: the panel holds
   the license as issued, these are the two parts of it the customer wrote. `<license-name>` is
   a custom element with no default display of its own, and it is the shrinking half of an
   `rl-split`, so it needs both the box and permission to shrink below its content. */
license-name {
    display: block;
    min-width: 0;
    overflow: hidden;
}

.license-name {
    font-size: var(--rl-text-l);
    font-weight: 700;
    color: var(--text);
}

/* Subtitle under the name: the customer's note about what this system is. Muted and one line,
   so it explains the title without competing with it — the API caps it at the same length the
   name gets, which is what a card can show on one line. */
.license-name__description {
    font-size: var(--rl-text-s);
    color: var(--text-muted);
}

/* Pencil beside the title, opening the edit dialog. Sized against the title rather than against
   the 30px `.icon-btn` used in toolbars — at that size it competes with the name it annotates. */
.license-name__edit {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 22px;
    height: 22px;
    padding: 0;
    background: none;
    border: none;
    border-radius: 4px;
    color: var(--text-subtle);
    cursor: pointer;
}
.license-name__edit:hover:not(:disabled) {
    background: var(--bg);
    color: var(--text);
}
.license-name__edit:disabled { cursor: default; opacity: 0.5; }


/* Row layout is relayout's (`rl-cluster` wraps, `rl-gap-m`, `rl-padding-*`) — only the surface
   is set here. Wrapping matters: the row carries a name, a token, a date and two buttons, and on
   a narrow card the buttons used to be pushed out past the card's edge rather than moving down. */
.license-key-row {
    background: var(--bg-sunken);
    border: 1px solid var(--border);
    border-radius: 8px;
}

/* The paste box for a license key. Monospace set here rather than through relayout's
   .rl-font-mono utility: .admin-input declares `font: inherit`, and the shorthand resets the
   family from portal.css, which loads after relayout and so wins the cascade. A key read in a
   proportional face is a key you cannot proofread. */
.key-verification-input {
    width: 100%;
    /* Roomier than .admin-input's single line: this one holds a wrapped block of text, and at
       the input's padding the key sits hard against the border on three sides. */
    padding: 0.75rem 0.875rem;
    border: 1px solid var(--border);
    /* The card's radius rather than the input's 6px: this is a block sitting inside the card,
       and a squarer box within a rounder one reads as a leftover rather than a choice. */
    border-radius: 10px;
    font-family: var(--rl-font-mono);
    font-size: var(--rl-text-m);
    line-height: 1.5;
    background: var(--bg-elev);
    color: var(--text);
    box-sizing: border-box;
    outline: none;
    resize: vertical;
    /* A token is one unbroken run of base64url with nowhere to wrap, so the break has to be
       allowed mid-"word". break-all rather than overflow-wrap: anywhere, which Safari did not
       take until 15.4 and this bundle still targets 14. */
    word-break: break-all;
}
.key-verification-input:focus { border-color: var(--brand); }

/* The PDF the engine rendered under the key being verified. Tall enough that the attribution
   line — which sits at the top right of the first page, and is the whole point of showing a
   document rather than a sentence about one — is visible without scrolling the frame. */
.key-verification-preview {
    width: 100%;
    height: 28rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg-sunken);
}

/* Work in progress, in the text's own colour so it belongs to whatever line it sits on. Sized in
   em for the same reason: it reads as a word of the sentence beside it. */
.spinner {
    display: inline-block;
    width: 1em;
    height: 1em;
    border: 2px solid color-mix(in oklch, currentColor 25%, transparent);
    border-top-color: currentColor;
    border-radius: 50%;
    animation: portal-spin .6s linear infinite;
    flex-shrink: 0;
}
@keyframes portal-spin { to { transform: rotate(360deg); } }

/* A ring that never turns still says "busy" next to text that says so too, and a spinning one
   is exactly the motion some people cannot look at. */
@media (prefers-reduced-motion: reduce) {
    .spinner { animation: none; }
}

/* The label column of a result row. Fixed, so the values line up down the card instead of each
   one starting wherever its label happened to end — a kid and a license number are read against
   each other, and a ragged left edge is what makes that hard. Sized in em rather than rem: the
   same rows render at two different text sizes, and the column has to follow the label. */
.key-verification-label {
    flex: 0 0 6.5em;
}

/* The key's number, in the license number's own face and tone: the two are quoted together to
   support, so they should look like one pair of identifiers rather than two unrelated labels. A
   fixed column, not a shrink-to-fit label, so every name on the card starts at the same x —
   including on rows from before key numbers, where the column is empty. */
.license-key-number {
    /* Room for the hash and three digits, which is far past any license's key ceiling. */
    flex: 0 0 2.25rem;
    color: var(--text-subtle);
    letter-spacing: 0.02em;
    /* On top of the row's own gap. An identifier and a name are two different kinds of thing,
       and at the row's gap alone they read as one run of text. */
    margin-inline-end: var(--rl-space-m);
}

/* Holds the names in a column while the row fits on one line, and keeps the name from being the
   part that shrinks when it doesn't — the token below has the room to give. */
.license-key-name {
    min-width: 8rem;
    flex-shrink: 0;
}

/* Takes the slack on a wide row and drops to its own full-width line below ~12rem, which is
   where the masked token would otherwise start losing characters to the ellipsis. */
.license-key-code {
    flex: 1 1 12rem;
    font-family: var(--rl-font-mono, ui-monospace, 'Cascadia Code', monospace);
    font-size: 0.875rem;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    letter-spacing: 0.03em;
}

/* An outlined capsule holding a shorter fill, rather than a single painted line. At 2px the bar
   had no inside and no outside, so it read as a rule between two rows of text; the border makes it
   an object and the padding leaves a visible margin of empty track around the fill, which is what
   says "this much used, this much left". box-sizing matters here — there is no global reset in
   this file, so the old 2px height plus its 1px border painted 4px of which half was border.

   The 12px capsule spends 1px on the border and 3px of padding each side, leaving a 4px fill. The
   fill is deliberately the thinner half: once the empty track is visible the fill no longer has to
   carry the whole reading, and a fill as tall as its container stops looking like a level. */
.license-progress-track {
    box-sizing: border-box;
    height: 12px;
    padding: 3px;
    background: var(--track);
    border: 1px solid var(--track-edge);
    border-radius: 100px;
}

.license-progress-fill {
    height: 100%;
    border-radius: 100px;
    transition: width .4s ease, background .4s ease;
}

/* ── License list rows ──
   Hover moves the edge, not the fill. A card is an object with a border, and
   repainting its whole face is both louder than the signal needs to be and easy to
   misread as a property of the licence rather than of the pointer.

   The edge goes neutral, not brand. Hover is a property of the pointer and says nothing about
   the licence, so it has no business borrowing a colour that means something elsewhere on the
   card — and a brand-coloured ring made whichever card the mouse happened to rest on look
   promoted. --text-subtle is what every other hover border here already uses, and it steps off
   the resting --border by about the same amount in both themes (light 3.34, dark 3.07), where
   the brand orange was uneven between them (2.22 and 3.73). */
.license-card--link {
    cursor: pointer;
    transition: border-color .15s;
}
.license-card--link:hover {
    border-color: var(--text-subtle);
}

/* Empty-state CTA: a single line, centred, but standing as tall as a populated row. */
.license-card--cta {
    min-height: 7.75rem;
    align-items: center;
    justify-content: center;
    text-align: center;
}

.license-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.2rem 0.6rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 100px;
    color: var(--text-muted);
    font-size: 0.8125rem;
    font-family: inherit;
    cursor: pointer;
    transition: color .15s, border-color .15s, background .15s;
}
.license-chip:hover { color: var(--text); border-color: var(--text-subtle); background: var(--bg-sunken); }
/* Non-interactive chip — a count shown on a card whose whole surface is the click target. */
.license-chip--static { cursor: default; }
.license-chip--static:hover { color: var(--text-muted); border-color: var(--border); background: var(--bg); }

/* ── Account card ── */
/* Aligned label/value pairs. relayout has no two-column definition-list primitive
   (rl-grid is equal-width auto-fit), so the grid stays custom. */
.account-fields {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 0.75rem 1.5rem;
    align-items: baseline;
    margin: 0;
}
.account-fields dt {
    color: var(--text-muted);
    font-size: 0.8125rem;
}
.account-fields dd {
    margin: 0;
    min-width: 0;
    font-size: 0.875rem;
    color: var(--text);
}

/* ── License detail page ── */
.license-detail-card {
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 1.5rem;
}
/* `.license-detail-header` itself carries no rules — `renderLicenseCardHeader` (shared by
   both license detail pages and the list card) styles it purely with relayout classes. */
/* The name/tier/progress/expiry block, set off from the id/origin strip above it — same
   rounding as the enclosing `.license-card` / `.license-detail-card`, `--bg` reads as a
   shade apart from their `--bg-elev` in both themes (light: --bg is the dimmer of the two;
   dark: --bg-elev is the dimmer — either way this block visibly nests inside the card). The
   border is darkened past the usual `--border` token so the nested card reads as a distinct
   surface rather than blending into it. */
.license-card__body {
    background: var(--bg);
    border: 1px solid color-mix(in oklch, var(--border) 100%, black 20%);
    border-radius: 12px;
    padding: 0.75rem 1rem;
}
/* Breadcrumb back to the list — sized to read as a peer of the list page's
   "Your licenses" / "Shared with you" section labels. */
.license-back {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    color: var(--text-muted);
    text-decoration: none;
    font-size: var(--rl-text-s);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
}
.license-back svg { width: 0.8125rem; height: 0.8125rem; }
.license-back:hover { color: var(--text); }

.license-tabs {
    border-bottom: 1px solid var(--border);
}
.license-tab {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.5rem 0.9rem;
    color: var(--text-muted);
    text-decoration: none;
    font-size: 0.9375rem;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    white-space: nowrap;
}
.license-tab:hover { color: var(--text); }
/* `.license-tab` styles both real `<a>` tabs (detail page) and `<button>` ones (the list
   card's compact tab-style action row) — the button needs its browser chrome reset. */
button.license-tab {
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    font: inherit;
    cursor: pointer;
}
.license-tab--active {
    color: var(--text);
    border-bottom-color: var(--brand);
    font-weight: 500;
}
.license-tab svg { flex-shrink: 0; }

.icon-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    flex-shrink: 0;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text-muted);
    cursor: pointer;
    transition: color .15s, background .15s, border-color .15s;
}
.icon-btn:hover { color: var(--text); background: var(--bg-sunken); }
.icon-btn:disabled { opacity: 0.5; cursor: default; }
.icon-btn.danger:hover { color: var(--wa-color-danger-primary, #dc2626); border-color: var(--wa-color-danger-primary, #dc2626); background: transparent; }

.key-name-input {
    flex: 1;
    min-width: 0;
    height: 32px;
    padding: 0 0.625rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text);
    font-size: 0.875rem;
    font-family: inherit;
    outline: none;
    box-sizing: border-box;
}
.key-name-input:focus { border-color: var(--brand); }
.key-name-input::placeholder { color: var(--text-subtle); }

.generate-key-btn {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    background: none;
    border: 1px solid var(--border);
    border-radius: 7px;
    padding: 0.375rem 0.75rem;
    color: var(--text-muted);
    font-size: 0.8125rem;
    font-family: inherit;
    cursor: pointer;
    transition: color .15s, border-color .15s, background .15s;
}
.generate-key-btn:hover { color: var(--text); border-color: var(--text-subtle); background: var(--bg-sunken); }

/* Borderless text button for a secondary control sitting beside a real one — the show/hide
   archived toggle. Not .icon-btn, which is a fixed 30px square and wraps a label to shreds. */
.text-btn {
    display: inline-flex;
    align-items: center;
    background: none;
    border: none;
    padding: 0.375rem 0;
    color: var(--text-muted);
    font-size: 0.8125rem;
    font-family: inherit;
    white-space: nowrap;
    cursor: pointer;
}
.text-btn:hover { color: var(--text); text-decoration: underline; }
.text-btn:disabled { opacity: 0.5; cursor: default; text-decoration: none; }

/* ── License sharing ── */
.share-row {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    background: var(--bg-sunken);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 0.5rem 0.75rem;
}

.grant-chip {
    display: inline-block;
    padding: 0.05rem 0.45rem;
    border: 1px solid var(--border);
    border-radius: 100px;
    font-size: 0.6875rem;
    color: var(--text-muted);
    white-space: nowrap;
}

.share-link-code {
    flex: 1;
    min-width: 0;
    font-family: var(--rl-font-mono, ui-monospace, 'Cascadia Code', monospace);
    font-size: 0.8125rem;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ── tier-card component ── */
.tier-card-wrap {
    position: relative;
    padding-top: 1.25rem;
    display: flex;
    flex-direction: column;
    flex: 1;
}

.tier-card__top-badge {
    position: absolute;
    top: 1.25rem;
    left: 50%;
    transform: translate(-50%, -50%);
    white-space: nowrap;
    z-index: 1;
}

.tier-card {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
    padding: 2.5rem 1.75rem 1.75rem;
    border-radius: 14px;
    box-sizing: border-box;
    border: 1px solid var(--border);
    background: var(--bg-elev);
    width: 320px;
    flex: 1;
}

.tier-label {
    align-self: flex-start;
    display: inline-block;
    padding: 0.2em 0.7em;
    border-radius: 100px;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    border: 1px solid transparent;
}
.tier-label--community    { border-color: var(--product); color: var(--text-muted); }
.tier-label--professional { border-color: var(--product); color: var(--product-text-light); }
.tier-label--enterprise   { background: var(--product); border-color: var(--product); color: var(--product-on); }

/* The theme branch the inline scope cannot make for itself — see --product-text-* above.
   It has to sit on the consuming rule rather than on :root, because the two tones arrive
   as inherited properties on the pill, not as anything :root can see. */
[data-theme="dark"] .tier-label--professional { color: var(--product-text-dark); }

.tier-top-pill {
    display: inline-block;
    /* It shares a non-wrapping row with the number and the owner address, and it is the one
       thing on that row that must never compress or break: the left side shrinks and wraps
       instead. */
    flex-shrink: 0;
    white-space: nowrap;
    padding: 0.2em 0.8em;
    border-radius: 100px;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    border: 1.5px solid;
    background: var(--bg);
}
.tier-top-pill--community    { color: var(--text-muted); border-color: var(--product); }
.tier-top-pill--professional { color: var(--product-text-light); border-color: var(--product); }
.tier-top-pill--enterprise   { background: var(--product); border-color: var(--product); color: var(--product-on); }

[data-theme="dark"] .tier-top-pill--professional { color: var(--product-text-dark); }

/* The pricing page's floating promo badge ("No card needed") borrows the community pill's
   class but is not a tier: it makes a claim about payment, not about which tier this is. It
   keeps the neutral edge, so the only brand-coloured things on that card are the tier it
   names and the one action it offers. */
.tier-card__top-badge .tier-top-pill { color: var(--text-muted); border-color: var(--border); }

/* ── price ──
   The product site's treatment, kept in step with `pages/lpdf/www/assets/css/theme.css`:
   the same card renders on both sides of the checkout and must not change face between
   them. A price is a number to be read, not a shape to be looked at, which is a different
   job from the wordmark's. The mono gives every digit one width, so a column of prices
   lines up whatever the amounts are, and a 9 can never be mistaken for anything else. The
   display face it replaced made "Free" and "Quote" read as small logos beside it. */
.tier-price {
    font-family: 'JetBrains Mono', ui-monospace, Consolas, monospace;
    font-weight: 700;
    font-size: 1.6rem;
    letter-spacing: -0.01em;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    color: var(--text);
}

/* The currency symbol used to be set in Tahoma, because the old display face drew a
   poor one. The mono's own is fine, so it inherits and the price is one face again. */
.tier-currency {
    font-weight: 500;
    font-size: 0.75em;
    vertical-align: top;
    padding-top: calc(0.18em + 1px);
    color: var(--text-muted);
}


.tier-card__cta {
    display: block;
    width: 100%;
    box-sizing: border-box;
    padding: 0.6875rem 1rem;
    font-size: 0.9375rem;
    font-weight: 500;
    text-align: center;
    border-radius: 8px;
    border: 1px solid;
    cursor: pointer;
    font-family: var(--rl-font-sans, system-ui, sans-serif);
    text-decoration: none;
    background: transparent;
    transition: background .15s, color .15s, transform .08s;
}
.tier-card__cta:active { transform: translateY(1px); }

/* One button treatment for all three, since the tier is already stated by the pill
   above it and a per-tier button colour only re-said it in a third hue.

   This is the one action in the portal painted in the product's colour rather than
   the house's — buying is the moment the customer is acting on the product, not on
   their account, and it is worth saying so. It shows on hover only: at rest the
   button is house-coloured like everything else, so the product colour arrives as
   the answer to reaching for it. --product falls back to --brand, so this is still
   correct on any page that never sets one. */
.tier-card__cta {
    color: var(--brand-text);
    border-color: color-mix(in oklch, var(--brand) 50%, var(--border));
}
.tier-card__cta:hover {
    background: var(--product);
    border-color: var(--product);
    color: var(--product-on);
}

.tier-card__cta--disabled {
    display: block;
    width: 100%;
    box-sizing: border-box;
    padding: 0.6875rem 1rem;
    font-size: 0.9375rem;
    font-weight: 500;
    text-align: center;
    border-radius: 8px;
    border: 1px solid var(--border);
    font-family: var(--rl-font-sans, system-ui, sans-serif);
    color: var(--text-subtle);
    background: transparent;
    cursor: default;
    opacity: 0.6;
}

/* The last control before Paddle's overlay opens, and that checkout is themed in the
   house colour — so the button that hands off to it is too. It used to fill in --product
   to match the tier CTA that led here, which meant reaching for it turned orange and
   clicking it landed on aqua. The product's colour now stops at the plan pane on the
   left; the action carries the colour the customer is about to arrive in.

   Filled at rest, not outlined. It was a ghost button that only filled on hover, which
   was borrowed from .tier-card__cta, where the point is that the product's colour arrives
   as the answer to reaching for it. Nothing is revealed here — the fill is the same house
   colour either way — so all the pattern bought was a primary action quieter than the
   filled Continue button on the Paddle page it hands off to. This is the call to action
   on the page; it looks like one.

   --brand-on is white on the fill at 4.65. That is the entire budget above the 4.5 a
   label needs, which decides the hover: every lighter variant was measured and breaks it
   (mix-white 96% already drops the label to 4.32), so hover darkens instead, in both
   themes rather than branching. At 92% toward black the label goes to 5.62.

   The button sits on --bg-sunken, the form pane. The fill reads against it at 4.22 light
   and 2.94 dark, going to 5.10 and 2.43 on hover — so light gains an edge and dark loses
   one. That dark number is the house colour against the lightest dark surface in the
   portal, and the rest state is already under the 3:1 a boundary wants; the label carries
   the button there. The fix, if it ever matters, is putting this pane on --bg instead of
   --bg-sunken in dark, which takes the rest state to 3.75 — not a second brand value. */
.confirm-cta {
    display: block;
    width: 100%;
    box-sizing: border-box;
    padding: 0.6875rem 1rem;
    font-size: 0.9375rem;
    font-weight: 500;
    text-align: center;
    border-radius: 8px;
    border: 1px solid var(--brand);
    cursor: pointer;
    font-family: var(--rl-font-sans, system-ui, sans-serif);
    background: var(--brand);
    color: var(--brand-on);
    transition: background .15s, border-color .15s, transform .08s;
}
.confirm-cta:hover:not(:disabled) {
    background: color-mix(in oklch, var(--brand) 92%, black);
    border-color: color-mix(in oklch, var(--brand) 92%, black);
}
.confirm-cta:active:not(:disabled) { transform: translateY(1px); }
.confirm-cta:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

/* A link to the product's own document — the licence the checkout acknowledgement points
   at — in the product's colour rather than the house's. It belongs with the tier pill on
   the list of things that are the product speaking: what is being agreed to is lpdf's
   licence, not something the portal issues. The house colour stays on every other link
   here, including the one in the site header, which leads to the same page as portal
   chrome rather than as part of a purchase.

   Same theme branch as .tier-label--professional, for the same reason: the inline scope
   hands both tones over and the rule picks one.

   Measured on --bg-sunken, the form pane's ground, which is the harder of the two the
   --product-text-* pair was walked against (--bg and --bg-elev): lpdf lands at 4.38 light
   and 4.06 dark, short of the 4.5 small text wants. Taken deliberately, because the aqua
   it replaces measured 4.22 and 2.94 on that same ground — this is the contrast moving
   the right way, not a new debt. If it ever needs to clear the bar outright, the fix is a
   tone walked against --bg-sunken specifically, not a second hue. */
.product-link {
    color: var(--product-text-light);
}
[data-theme="dark"] .product-link {
    color: var(--product-text-dark);
}

/* The tick that agrees to the licence .product-link points at — one statement, so one
   colour. Left on its own it was the browser's blue, the only hue on the checkout that
   belonged to neither the product nor the house.

   The raw --product, not a --product-text-* tone: a checkbox is a graphic held to 3:1,
   the same bar as the tier pill's outline, and lpdf's fill clears it against the page in
   both themes. The tick itself is drawn by the browser, which picks black or white by
   contrast — on lpdf's orange that is the dark one at 6.17 against white's 3.41, which is
   the same reason --product-on is a dark tone for every product here. Both clear 3:1
   anyway, so a browser that chooses differently is still legible.

   Checkboxes elsewhere in the portal are chrome and stay the browser's own; give them
   `accent-color: var(--brand)` at :root if that ever wants settling in one place. */
.product-check {
    accent-color: var(--product);
}
