/**
 * HUD components — account menu, save state, upgrade and billing panels, and
 * the sign-in sheet the auth module deliberately ships unstyled.
 *
 * Loaded after site.css, into the layers site.css already declares, so nothing
 * here depends on source order. It is a separate file rather than an addition
 * to site.css because site.css is generated by tools/build_site.mjs and this is
 * hand-written; editing a generated file is how the generator gets overwritten
 * and blamed.
 *
 * Visual language (docs/decisions/visual-language.md) applies as lint:
 *   - no coloured accent line on any card, ever
 *   - no badge that restates something already visible
 *   - hierarchy from type and space; colour only carries meaning
 *   - one hairline border throughout; shadows only on things that genuinely float
 *   - accent appears two or three times per screen, no more
 */

@layer tokens {
  :root {
    /**
     * Component tokens for the HUD.
     *
     * These belong in design-tokens.json alongside the others — they are here
     * only because that file's generator is mid-change in a parallel branch and
     * regenerating site.css now would clobber it. Move them when it settles;
     * the names are already final.
     *
     * Every one resolves to a semantic token. Components below reference only
     * these, never a semantic token and never a ramp step — reaching past the
     * layer is what makes a design system unretheme-able six months later.
     */
    --hud-bg: var(--panel-bg);
    --hud-border: var(--panel-border);
    --hud-text: var(--text);
    --hud-text-muted: var(--text-muted);
    --hud-text-faint: var(--text-faint);

    --hud-item-bg-hover: var(--control-bg-hover);
    --hud-item-bg-active: var(--control-bg-active);

    /* Save-state and status tones. Each is always paired with words — colour
       never carries the meaning on its own. */
    --hud-tone-ok: var(--success);
    --hud-tone-warn: var(--warning);
    --hud-tone-error: var(--danger);
    --hud-tone-busy: var(--text-muted);
    --hud-tone-locked: var(--text-muted);

    /* Destructive action. --danger is semantic; a button needs its own. */
    --hud-danger-bg: light-dark(var(--red-600), var(--red-500));
    --hud-danger-bg-hover: light-dark(var(--red-700), var(--red-400));
    --hud-danger-text: light-dark(#fff, var(--neutral-1000));

    /*
     * Spring easing, for direct manipulation only.
     *
     * visual-language.md allows motion for state change at 120-180ms ease-out.
     * A press is a state change, and overshoot is the one place it carries
     * information — it says "physical control" rather than "look at me". The
     * doc is amended to permit up to 240ms with overshoot for press, drag
     * release and toggle. Still nothing loops, still nothing on scroll.
     *
     * A CSS linear() easing: Baseline, and the reason wiring-plan.md said no to
     * Motion and GSAP for a product whose animations are all state changes.
     *
     * Named --motion-spring, not --hud-motion-spring: it belongs to the motion
     * scale beside --motion-fast and --motion-base, and it lives here only
     * until design-tokens.json is safe to touch. Namespacing it would have
     * meant renaming every use later — and the visual-language lint checks for
     * `var(--motion-` precisely so a duration cannot bypass the scale, which
     * it correctly flagged when this was misnamed.
     */
    --motion-spring: linear(0, 0.5 7.5%, 0.9 15%, 1.05 22%, 1.02 30%, 1);
    --motion-spring-duration: 220ms;

    /* Light catching an edge. Not a shadow (rule 6 is about elevation) and not
       a gradient fill — a 1px inset highlight is what makes a surface read as
       a surface. */
    --hud-sheen: inset 0 1px 0 0 oklch(1 0 0 / 0.06);

    /* A soft ring on the one primary action per screen. Adds no new accent
       use; it makes the existing one feel pressable. */
    --hud-accent-glow: 0 0 0 4px oklch(0.664 0.175 245.42 / 0.18);

    --hud-selected-ring: var(--swatch-ring-selected);
    --hud-menu-shadow: var(--shadow-lg);
  }
}

@layer components {
  /* ------------------------------------------------------------ shared */

  .hud-account,
  .hud-upgrade,
  .hud-billing {
    font-family: var(--type-family-ui);
    color: var(--hud-text);
  }

  /* Every interactive thing in the HUD gets the same focus treatment. */
  .hud-account :focus-visible,
  .hud-upgrade :focus-visible,
  .hud-billing :focus-visible,
  .signin-sheet :focus-visible {
    outline: var(--focus-width) solid var(--focus-ring);
    outline-offset: var(--focus-offset);
    border-radius: var(--radius-sm);
  }

  /* --------------------------------------------------------- save state */

  /**
   * Text, not an icon. "Saved" is unambiguous; a green dot needs a legend, and
   * the states that matter most — offline, read-only — have no obvious glyph.
   */
  .hud-save {
    font-size: var(--type-size-sm);
    color: var(--hud-tone-busy);
    white-space: nowrap;
    transition: opacity var(--motion-base) var(--motion-ease);
  }
  .hud-save:empty { opacity: 0; }
  .hud-save[data-tone="ok"] { color: var(--hud-tone-ok); }
  .hud-save[data-tone="warn"] { color: var(--hud-tone-warn); }
  .hud-save[data-tone="error"] { color: var(--hud-tone-error); }
  .hud-save[data-tone="locked"] { color: var(--hud-tone-locked); }
  .hud-save[data-tone="local"] { color: var(--hud-text-muted); }

  /* The generator's slot. Layout only — the control inside owns its looks. */
  .bar-account { display: inline-flex; align-items: center; }

  /* ------------------------------------------------------ account menu */

  .hud-account { position: relative; display: inline-flex; align-items: center; }

  .hud-account__signin {
    min-height: var(--control-height-md);
    padding: 0 var(--space-md);
    border: 1px solid var(--control-border);
    border-radius: var(--radius-md);
    background: var(--control-bg);
    color: var(--control-text);
    font: inherit;
    font-size: var(--type-size-sm);
    cursor: pointer;
    transition: background var(--motion-fast) var(--motion-ease);
  }
  .hud-account__signin:hover:not(:disabled) { background: var(--hud-item-bg-hover); }
  .hud-account__signin:disabled { opacity: var(--disabled-opacity); cursor: not-allowed; }

  .hud-account__trigger {
    display: grid;
    place-items: center;
    /* 44px hit target on touch even though the avatar reads smaller. */
    min-width: var(--control-height-md);
    min-height: var(--control-height-md);
    padding: 0;
    border: 0;
    border-radius: var(--radius-full);
    background: none;
    cursor: pointer;
  }

  .hud-account__avatar {
    width: 28px;
    height: 28px;
    border-radius: var(--radius-full);
    object-fit: cover;
    box-shadow: 0 0 0 1px var(--swatch-ring);
  }
  .hud-account__avatar--initial {
    display: grid;
    place-items: center;
    /* Background is set inline from the face palette, per user. Dark text
       because every face colour is a bright, saturated mid-tone. */
    background: var(--hud-item-bg-active);
    color: oklch(0.18 0 0);
    font-size: var(--type-size-sm);
    font-weight: var(--type-weight-medium);
  }

  /* The menu genuinely floats, so it is one of the few things allowed a shadow. */
  .hud-account__menu {
    position: absolute;
    top: calc(100% + var(--space-xs));
    right: 0;
    z-index: var(--z-popover);
    min-width: 220px;
    padding: var(--space-2xs);
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-lg);
    background: var(--hud-bg);
    box-shadow: var(--hud-menu-shadow), var(--hud-sheen);
  }
  .hud-account__menu[hidden] { display: none; }

  .hud-account__identity {
    display: grid;
    gap: var(--space-3xs);
    padding: var(--space-xs) var(--space-sm);
  }
  .hud-account__name {
    font-size: var(--type-size-sm);
    font-weight: var(--type-weight-medium);
    /* A display name is user-controlled and can be very long. */
    overflow-wrap: anywhere;
  }
  .hud-account__email { font-size: var(--type-size-xs); color: var(--hud-text-muted); overflow-wrap: anywhere; }
  .hud-account__tier { font-size: var(--type-size-xs); color: var(--hud-text-faint); }

  .hud-account__rule { margin: var(--space-2xs) 0; border: 0; border-top: 1px solid var(--hud-border); }

  .hud-account__item {
    display: block;
    width: 100%;
    padding: var(--space-xs) var(--space-sm);
    border: 0;
    border-radius: var(--radius-md);
    background: none;
    color: inherit;
    font: inherit;
    font-size: var(--type-size-sm);
    text-align: left;
    text-decoration: none;
    cursor: pointer;
  }
  .hud-account__item:hover { background: var(--hud-item-bg-hover); }

  .hud-account__error {
    margin: var(--space-2xs) var(--space-sm) var(--space-xs);
    font-size: var(--type-size-xs);
    color: var(--hud-tone-error);
  }
  .hud-account__error[hidden] { display: none; }

  /* ------------------------------------------------------ upgrade panel */

  /*
    One card, filling the column.

    It was a 420px card nested inside a 560px card inside the page column, so
    it read as boxed twice and hugged the left. Removing the card was an
    over-correction — the offer then had nothing holding it together and the
    page became a flat run of paragraphs. The fix was the nested max-widths;
    the card was never the problem.
  */
  .hud-upgrade {
    display: grid;
    gap: var(--space-md);
    padding: var(--space-lg);
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-lg);
    background: var(--hud-bg);
    box-shadow: var(--hud-sheen);
  }

  .hud-upgrade__heading {
    margin: 0;
    font-size: var(--type-size-md);
    font-weight: var(--type-weight-medium);
  }
  /* Sits directly under the heading, so no gap of its own. */
  .hud-upgrade__tagline { margin-block-start: calc(-1 * var(--space-xs)); }
  .hud-upgrade__tagline { margin: 0; color: var(--hud-text-muted); font-size: var(--type-size-sm); }

  /*
    Side by side, so the two prices are compared rather than scrolled past.
    Stacks below 30rem, where two columns would each be too narrow to read the
    saving line without wrapping.
  */
  /*
    A list, not three cards. Icons are the editor's own, so each one points at a
    control the reader will actually meet — which is the only justification for
    an icon next to a line of text.
  */
  .hud-upgrade__benefits {
    display: grid;
    gap: var(--space-xs);
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--type-size-sm);
    color: var(--hud-text-muted);
  }
  .hud-upgrade__benefits li {
    display: grid;
    grid-template-columns: var(--icon-size-sm) 1fr;
    gap: var(--space-xs);
    align-items: start;
  }
  .hud-upgrade__benefits .icon {
    inline-size: var(--icon-size-sm);
    block-size: var(--icon-size-sm);
    /* Optically centred on the first line rather than the box. */
    margin-block-start: 0.15em;
    color: var(--hud-text-muted);
  }

  /*
    Two choices, capped. auto-fit alone stretched each one to half the card, so
    on a wide page "$4.99 / month" sat alone in a 650px box — a price tag the
    size of a billboard, and the pair read as two panels rather than as one
    either/or. 34rem holds both at a size you can compare in a glance.
  */
  .hud-upgrade__intervals {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
    gap: var(--space-xs);
    max-inline-size: 34rem;
  }

  /**
   * Both intervals get identical structure and weight. The selected one is
   * marked by a ring, not by making the other one smaller or greyer — a toggle
   * where one side is visually discouraged is a decoy, not a choice.
   */
  .hud-upgrade__interval {
    display: grid;
    gap: var(--space-3xs);
    align-content: start;
    min-height: var(--control-height-lg);
    padding: var(--space-sm) var(--space-md);
    border: 1px solid var(--control-border);
    border-radius: var(--radius-md);
    /* Transparent rather than --control-bg: a filled box reads as an input
       waiting for text. The ring is what marks the choice. */
    background: none;
    color: var(--control-text);
    font: inherit;
    text-align: left;
    cursor: pointer;
    transition:
      background var(--motion-fast) var(--motion-ease),
      box-shadow var(--motion-fast) var(--motion-ease);
  }
  .hud-upgrade__interval:hover { background: var(--hud-item-bg-hover); }
  .hud-upgrade__interval[data-selected="true"] {
    box-shadow: 0 0 0 var(--focus-width) var(--hud-selected-ring);
  }
  .hud-upgrade__interval-label { font-size: var(--type-size-md); font-weight: var(--type-weight-medium); }
  /* The saving, in monospace — it is arithmetic the reader can check. */
  .hud-upgrade__interval-detail {
    font-family: var(--type-family-mono);
    font-size: var(--type-size-xs);
    color: var(--hud-text-muted);
  }

  /* Same size as the reassurance beneath it. The amount charged is never
     smaller than the amount advertised. */
  /* Beside the button, same size. The amount and the commitment together. */
  .hud-upgrade__charge { margin: 0; font-size: var(--type-size-sm); color: var(--hud-text-muted); }
  .hud-upgrade__reassurance { margin: 0; font-size: var(--type-size-sm); color: var(--hud-text-muted); }
  .hud-upgrade__note { margin: 0; font-size: var(--type-size-xs); color: var(--hud-text-faint); }
  .hud-upgrade__unavailable { margin: 0; color: var(--hud-text-muted); font-size: var(--type-size-sm); }

  .hud-upgrade__error {
    margin: 0;
    color: var(--hud-tone-error);
    font-size: var(--type-size-sm);
  }
  .hud-upgrade__error[hidden] { display: none; }

  .hud-upgrade__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs) var(--space-md);
    align-items: center;
  }

  /* The one accent on this panel.

     Styling lives in .btn-glow (site.css) now, and the button carries that
     class — there is one glowing call to action in this app and it should have
     one implementation. Deleted rather than left to lose quietly: this file
     loads after site.css, so anything declared here wins ties and a leftover
     `background` would silently beat the shared component.

     The busy rule went with it, and that was the bug. It set opacity to 0.45,
     and actionButton() sets [data-busy] synchronously on click and clears it
     when the work resolves — so every click flashed the button to 45% for a
     frame or two. On a bright button against a dark backdrop that reads as a
     dark blink. .btn-glow keeps its colour while busy and fades a spinner in
     after 140ms, so fast work shows nothing at all. */

  .hud-upgrade__cancel {
    min-height: var(--control-height-lg);
    padding: 0 var(--space-md);
    border: 0;
    border-radius: var(--radius-md);
    background: none;
    color: var(--hud-text-muted);
    font: inherit;
    font-size: var(--type-size-sm);
    cursor: pointer;
  }
  .hud-upgrade__cancel:hover { background: var(--hud-item-bg-hover); }

  /* ---------------------------------------------------------- site header */

  /*
    A full-bleed band, edge to edge.

    Constraining it to its own measure put the brand 110px left of the page's
    heading — near-alignment, which reads as a mistake rather than as a choice.
    Different pages carry different measures (prose is narrower than this
    settings column), so no single header width can align with all of them.
    Spanning the viewport makes the header a distinct layer instead, and the
    offset becomes obviously intentional.
  */
  .site-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-lg);
    padding: var(--space-md) var(--space-xl);
    border-block-end: 1px solid var(--hud-border);
  }
  @media (width < 40rem) {
    .site-header { padding-inline: var(--space-lg); }
  }

  .site-brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    color: inherit;
    text-decoration: none;
  }
  .site-brand .wordmark {
    font-size: var(--type-size-sm);
    font-weight: var(--type-weight-medium);
    letter-spacing: -0.01em;
  }
  /* Hidden below the smallest sizes: the mark alone still identifies the site,
     and a wrapped wordmark costs the header its single line. */
  @media (width < 26rem) {
    .site-brand .wordmark { display: none; }
  }

  .site-nav {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    font-size: var(--type-size-sm);
  }
  .site-nav > a:not(.btn) {
    color: var(--hud-text-muted);
    text-decoration: none;
    white-space: nowrap;
  }
  .site-nav > a:not(.btn):hover { color: var(--hud-text); }

  /*
    Phone: four items do not fit on one line at 375px, and the header answered
    by wrapping "Open editor" into two lines and doubling its own height — the
    one element that has to feel dependable, visibly breaking.

    About goes, because every page's footer links to it under the byline, and
    it is the only item here nobody is on their way to. Designs stays: it is
    the way back to your work, and it was added precisely because the account
    dropdown is not a place people look for their files.
  */
  @media (width < 30rem) {
    .site-nav { gap: var(--space-sm); }
    .site-nav > a[href="/about"] { display: none; }
  }

  /* The one accent on a content page, and it is the primary action. */
  .site-nav .btn-primary {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--control-height-md);
    padding-inline: var(--space-md);
    border-radius: var(--radius-md);
    background: var(--btn-primary-bg);
    color: var(--btn-primary-text);
    font-weight: var(--type-weight-medium);
    text-decoration: none;
    /* Two words; wrapping them is what made the header two lines tall. */
    white-space: nowrap;
    transition: background var(--motion-fast) var(--motion-ease);
  }
  .site-nav .btn-primary:hover {
    background: var(--btn-primary-bg-hover);
    box-shadow: var(--hud-accent-glow);
  }
  .site-nav .btn-primary:active { transform: scale(0.98); }

  /* The face strip, as the header's underline. */
  .face-rule {
    display: flex;
    block-size: 2px;
  }
  .face-rule > i { flex: 1; }

  /*
    The mark unfolds on hover.
    
    Not decoration: the product is a flat net becoming a body, and the mark
    performing that in miniature is the whole idea in one gesture. The arms
    push out from centre and settle. prefers-reduced-motion already disables
    every transition globally.
  */
  .site-brand svg rect {
    transform-box: fill-box;
    transform-origin: center;
    transition: transform var(--motion-spring-duration) var(--motion-spring);
  }
  .site-brand:hover svg rect:nth-of-type(1) { transform: translateY(-1.5px); }
  .site-brand:hover svg rect:nth-of-type(2) { transform: translateX(-1.5px); }
  .site-brand:hover svg rect:nth-of-type(4) { transform: translateX(1.5px); }
  .site-brand:hover svg rect:nth-of-type(5) { transform: translateY(1.5px); }

  /* --------------------------------------------------------- settings page */

  /*
    A reading column, wider than prose because the plan cards sit in it.
    `main` in site.css is capped at 760px for text; this is the one page that
    wants a little more and says so locally rather than changing that global.
  */
  .settings {
    /* `main` in site.css already supplies the horizontal gutter, so this only
       caps the measure. Subtracting the gutter here as well double-counted it
       and pushed the column off centre on narrow screens. */
    /*
      Two measures, because the pages are two shapes.

      Rows used to read badly past ~64rem — a label on the left and a button
      three hundred pixels away on the right is a worse row, not a wider one.
      Sections are panels now, and a bordered surface holds a row together at
      a width bare text cannot, so the measure is set by the page rather than
      by the weakest row in it. A grid of cards wants more still and gets
      --wide.
    */
    max-inline-size: var(--page-measure, 72rem);
    margin-inline: auto;
    padding-block: var(--space-xl) var(--space-3xl);
    display: grid;
    gap: var(--space-lg);
  }

  /*
    Set on the body so the footer — a sibling of main, not a descendant — can
    read it too. site.css defaults --page-measure to the 760px prose column;
    these two say what the account pages want instead, and the footer follows
    without either file knowing about the other's selectors.

    The --wide rule comes second on purpose: an element carrying both classes
    matches both selectors at equal specificity, so source order decides.
  */
  body:has(.settings) { --page-measure: 72rem; }
  body:has(.settings--wide) { --page-measure: 88rem; }

  .settings-back {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
  }
  .settings-back a {
    color: var(--hud-text-muted);
    font-size: var(--type-size-sm);
    text-decoration: none;
  }
  .settings-back a:hover { color: var(--hud-text); }

  .settings h1 { margin: 0; }
  /* The lede is the page's one-line promise; the panel repeats nothing of it. */
  .settings .lede { margin: 0; }

  /* ------------------------------------------------------ billing panel */

  /*
    No max-width. The page column already decides how wide this is, and a
    narrower max-width here made the panel a 560px block sitting at the left
    edge of a wider column — with the plans nested inside it at 420px, boxed
    and left-aligned a second time. That is what "squished" was.
  */
  .hud-billing { display: grid; gap: var(--space-lg); }

  .hud-billing__status {
    padding: var(--space-sm) var(--space-md);
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-md);
    font-size: var(--type-size-sm);
  }
  .hud-billing__status[hidden] { display: none; }
  .hud-billing__status[data-tone="ok"] { color: var(--hud-tone-ok); }
  .hud-billing__status[data-tone="warn"] { color: var(--hud-tone-warn); }
  .hud-billing__status[data-tone="error"] { color: var(--hud-tone-error); }
  .hud-billing__status[data-tone="busy"] { color: var(--hud-tone-busy); }

  .hud-billing__body { display: grid; gap: var(--space-lg); }

  .hud-billing__plan { display: grid; gap: var(--space-2xs); justify-items: start; }
  .hud-billing__tier {
    margin: 0;
    font-size: var(--type-size-lg);
    font-weight: var(--type-weight-medium);
    letter-spacing: -0.01em;
  }
  .hud-billing__detail { margin: 0; color: var(--hud-text-muted); font-size: var(--type-size-sm); }
  /* Colour on the text, never a tinted panel or a stripe down the side. */
  .hud-billing__detail[data-urgent="true"] { color: var(--hud-tone-warn); }

  /*
    Placeholder while /api/me answers. A shape, not a spinner — this resolves in
    one round trip, and a spinner that appears and vanishes is worse than a
    space that fills.
  */
  .hud-billing__loading {
    block-size: 4.5rem;
    border-radius: var(--radius-md);
    background: var(--hud-item-bg-hover);
  }

  .hud-billing__manage-block { display: grid; gap: var(--space-sm); justify-items: start; }

  .hud-billing__manage {
    min-height: var(--control-height-lg);
    padding: 0 var(--space-lg);
    border: 1px solid var(--control-border);
    border-radius: var(--radius-md);
    background: var(--control-bg);
    color: var(--control-text);
    font: inherit;
    font-size: var(--type-size-sm);
    cursor: pointer;
  }
  .hud-billing__manage:hover:not(:disabled) { background: var(--hud-item-bg-hover); }

  .hud-billing__note { margin: 0; font-size: var(--type-size-sm); color: var(--hud-text-muted); }

  /**
   * The dunning / cancellation line.
   *
   * Colour on the text, not a tinted panel or a coloured left border — rule 1.
   * It sits above the actions because it is the reason to reach for them.
   */
  .hud-billing__notice { margin: 0; font-size: var(--type-size-sm); }
  .hud-billing__notice[data-tone="warn"] { color: var(--hud-tone-warn); }
  .hud-billing__notice[data-tone="info"] { color: var(--hud-text-muted); }

  /* Numbers, with what they are measured against. Monospace, muted, small. */
  .hud-billing__usage {
    margin: 0;
    font-family: var(--type-family-mono);
    font-size: var(--type-size-xs);
    color: var(--hud-text-faint);
  }
  .hud-billing__usage[hidden] { display: none; }
  .hud-billing__usage dt { color: var(--hud-text-muted); }
  /* Numbers are monospace. The whole positioning is "we measured this". */
  .hud-billing__usage dd { margin: 0; font-family: var(--type-family-mono); }

  /* ----------------------------------------------------- migration notice */

  .hud-migration {
    display: grid;
    gap: var(--space-xs);
    padding: var(--space-md);
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-md);
    font-size: var(--type-size-sm);
  }
  .hud-migration__summary { margin: 0; color: var(--hud-tone-warn); }
  .hud-migration__detail summary { cursor: pointer; color: var(--hud-text-muted); }
  .hud-migration__list {
    margin: var(--space-xs) 0 0;
    padding-left: var(--space-lg);
    color: var(--hud-text-muted);
  }

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

  .hud-settings { display: grid; gap: var(--space-2xl); }

  .hud-settings__status {
    margin: 0;
    padding: var(--space-sm) var(--space-md);
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-md);
    font-size: var(--type-size-sm);
  }
  .hud-settings__status[hidden] { display: none; }
  .hud-settings__status[data-tone="ok"] { color: var(--hud-tone-ok); }
  .hud-settings__status[data-tone="error"] { color: var(--hud-tone-error); }

  /* Panels carry their own edges, so the 48px that used to stand in for a
     boundary is now just a hole between them. */
  .hud-settings__body { display: grid; gap: var(--space-md); }

  /*
    A section is a panel.

    This was a heading and a hairline, on the reasoning that five bordered
    boxes read as a control panel while rules read as one document with parts.
    Rendered, that was wrong: with no surface under them the rows had nothing
    holding them together, and a name at the left margin with its plan chip
    against the far right of a 64rem column read as loose text rather than as
    one thing you could act on. The rule was drawing a line *between* rows
    while the surface is what groups them.

    Still one hairline and still no shadow — rule 6 keeps elevation for things
    that genuinely float. The sheen is a 1px inset highlight, which is what
    makes a surface read as a surface rather than as a hole.
  */
  .hud-section {
    display: grid;
    gap: var(--space-sm);
    padding: var(--space-lg);
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-lg);
    background: var(--hud-bg);
    box-shadow: var(--hud-sheen);
  }
  .hud-section__title {
    margin: 0;
    font-size: var(--type-size-sm);
    font-weight: var(--type-weight-medium);
    color: var(--hud-text-muted);
    /* Small caps rather than large bold: these are labels for regions, not
       headlines competing with the page title. */
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }
  .hud-section__body { display: grid; }
  /*
    Rows inside a panel are separated, not spaced. Gaps let a list of devices
    dissolve into unrelated lines; a hairline says these are the same kind of
    thing, and gives the hover target an edge to fill.
  */
  .hud-section__body > .hud-row + .hud-row { border-block-start: 1px solid var(--hud-border); }
  .hud-section__body > .hud-section__note:not(:first-child) { margin-block-start: var(--space-sm); }
  .hud-section__note {
    margin: 0;
    font-size: var(--type-size-sm);
    color: var(--hud-text-muted);
  }
  .hud-section__action { display: grid; gap: var(--space-2xs); justify-items: start; }

  /*
    The identity card: face, then the row that would otherwise stand alone.
    The row still grows, so the plan chip stays at its right edge rather than
    hard against the face.
  */
  .hud-identity { display: flex; align-items: center; gap: var(--space-md); }
  .hud-identity > .hud-row { flex: 1 1 auto; }
  .hud-identity__face {
    flex: none;
    inline-size: 2.75rem;
    block-size: 2.75rem;
    border-radius: var(--radius-full);
    object-fit: cover;
    box-shadow: var(--hud-sheen);
  }
  .hud-identity__face--initial {
    display: grid;
    place-items: center;
    /* Background is set inline from the face palette, per user. Dark text
       because every face colour is a bright, saturated mid-tone — the same
       reasoning, and the same value, as the app bar's avatar. */
    color: oklch(0.18 0 0);
    font-size: var(--type-size-lg);
    font-weight: var(--type-weight-medium);
  }

  /* Label and detail on the left, one action on the right. */
  .hud-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-xs) var(--space-lg);
    padding-block: var(--space-sm);
    /*
      Rows fill their panel, and the panel's edge is what the trailing action
      aligns to. Capping the row instead left the button floating in the middle
      of a wide card with nothing under it — further from the label *and* from
      any edge, which is the worst of both. The panel border is what pairs them
      now, so the page can be wide without the rows coming apart.
    */
  }
  .hud-row__text { display: grid; gap: var(--space-3xs); min-inline-size: 0; }
  .hud-row__label { font-size: var(--type-size-sm); overflow-wrap: anywhere; }
  .hud-row__detail {
    font-size: var(--type-size-xs);
    color: var(--hud-text-muted);
    overflow-wrap: anywhere;
  }
  /*
    The plan name, as a chip.

    Not a badge restating something visible (rule 2) — the plan appears nowhere
    else on this page, and "Free" set in faint 12px beside a name read as a
    stray word rather than as the answer to "what am I on".
  */
  .hud-row__note {
    padding: var(--space-3xs) var(--space-xs);
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-full);
    font-size: var(--type-size-xs);
    color: var(--hud-text-muted);
    white-space: nowrap;
  }

  /* The one button style the settings page needs. */
  .hud-btn {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--control-height-md);
    padding-inline: var(--space-md);
    border: 1px solid var(--control-border);
    border-radius: var(--radius-md);
    background: var(--control-bg);
    color: var(--control-text);
    font: inherit;
    font-size: var(--type-size-sm);
    text-decoration: none;
    cursor: pointer;
    transition:
      background var(--motion-fast) var(--motion-ease),
      transform var(--motion-spring-duration) var(--motion-spring);
  }
  .hud-btn:hover:not(:disabled) { background: var(--hud-item-bg-hover); }
  /* A press is a state change, and overshoot on release is the one place motion
     carries information: it says physical control. Same treatment the account
     menu and the plan buttons already use. */
  .hud-btn:active:not(:disabled) { transform: scale(0.97); }
  .hud-btn:disabled { opacity: var(--disabled-opacity); cursor: not-allowed; }

  .hud-btn--danger {
    border-color: transparent;
    background: var(--hud-danger-bg);
    color: var(--hud-danger-text);
  }
  .hud-btn--danger:hover:not(:disabled) { background: var(--hud-danger-bg-hover); }

  .hud-delete__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
    align-items: center;
  }
  .hud-delete__input { max-inline-size: 12rem; }

  /* ------------------------------------------------------------- projects */

  .hud-projects { display: grid; gap: var(--space-lg); }

  .hud-projects__head {
    display: flex;
    flex-wrap: wrap;
    align-items: end;
    justify-content: space-between;
    gap: var(--space-md);
  }
  .hud-projects__headings { display: grid; gap: var(--space-2xs); }
  /* The answer to "is my work safe" — quiet, present, not a banner. */
  .hud-projects__assurance {
    margin: 0;
    font-size: var(--type-size-sm);
    color: var(--hud-text-muted);
    max-inline-size: 46ch;
  }

  /*
    Still monospace — the whole positioning is "we measured this" — but not
    faint 12px. It is the one number on the page and it was set smaller and
    dimmer than every other line, which read as debug output rather than as
    the answer to "how much room is left".
  */
  .hud-projects__quota {
    margin: 0;
    font-family: var(--type-family-mono);
    font-size: var(--type-size-sm);
    color: var(--hud-text-muted);
  }

  /*
    auto-fill, not auto-fit: with one design, auto-fit would stretch a single
    card across the whole page. A grid that changes shape with its contents
    reads as a bug.
  */
  .hud-projects__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: var(--space-md);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .hud-project {
    display: grid;
    /* Rows sized to content, then a spacer, so every card's buttons sit on the
       same line however many lines its title took. A two-line title used to
       push one card's actions below its neighbours'. */
    grid-template-rows: auto auto 1fr;
    gap: var(--space-xs);
    padding: var(--space-sm);
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-lg);
    background: var(--hud-bg);
    box-shadow: var(--hud-sheen);
  }
  .hud-project__actions { align-self: end; }

  /*
    The frame exists before thumbnails do, at the template's own aspect ratio
    (585x559), so the grid does not reflow when they land.
  */
  .hud-project__thumb {
    aspect-ratio: 585 / 559;
    border-radius: var(--radius-md);
    background: var(--hud-item-bg-hover);
    /* The template is pixel art; anything shown here should say so. */
    image-rendering: pixelated;
  }

  /* Sits over the colour tint, which stays as the backdrop while this loads and
     as the whole answer for a design that has no picture yet. */
  .hud-project__shot {
    display: block;
    inline-size: 100%;
    block-size: 100%;
    object-fit: contain;
    /* The template is pixel art; anything shown here should say so. */
    image-rendering: pixelated;
  }

  .hud-project__meta { display: grid; gap: var(--space-3xs); }
  .hud-project__title {
    font-size: var(--type-size-sm);
    font-weight: var(--type-weight-medium);
    overflow-wrap: anywhere;
  }
  .hud-project__detail { font-size: var(--type-size-xs); color: var(--hud-text-muted); }
  /* A state, not a failure: nothing was deleted and the wording must not imply it. */
  .hud-project__locked { font-size: var(--type-size-xs); color: var(--hud-tone-locked); }

  .hud-project__actions { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
  .hud-project__actions .hud-btn {
    min-block-size: var(--control-height-sm);
    padding-inline: var(--space-sm);
    font-size: var(--type-size-xs);
  }

  /* A local design is on this device only — a dashed edge says so without a badge. */
  .hud-project[data-kind="local"] { border-style: dashed; }

  /*
    Centred, because the frame is the full width of the grid it stands in for.
    Left-aligned, the sentence and its button sat in the top-left corner of a
    1200px dashed rectangle with nothing else in it, which reads as a layout
    that failed rather than as a page waiting for its first design.
  */
  .hud-projects__empty {
    grid-column: 1 / -1;
    display: grid;
    gap: var(--space-md);
    justify-items: center;
    padding: var(--space-3xl) var(--space-xl);
    border: 1px dashed var(--hud-border);
    border-radius: var(--radius-lg);
    text-align: center;
  }

  /* The one primary action on an otherwise empty page. */
  .hud-btn--go {
    border-color: transparent;
    background: var(--btn-primary-bg);
    color: var(--btn-primary-text);
    font-weight: var(--type-weight-medium);
    transition:
      background var(--motion-fast) var(--motion-ease),
      box-shadow var(--motion-fast) var(--motion-ease),
      transform var(--motion-spring-duration) var(--motion-spring);
  }
  .hud-btn--go:hover {
    background: var(--btn-primary-bg-hover);
    box-shadow: var(--hud-accent-glow);
  }
  .hud-btn--go:active { transform: scale(0.98); }

  /* -------------------------------------------------------- sign-in sheet */

  /**
   * The auth module emits these class names with no CSS of its own — inventing
   * styles there would have fought the token system. This is that styling.
   */
  .signin-sheet {
    width: min(360px, calc(100vw - 2 * var(--space-lg)));
    padding: var(--space-lg);
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-lg);
    background: var(--hud-bg);
    color: var(--hud-text);
    box-shadow: var(--hud-menu-shadow);
  }
  .signin-sheet::backdrop { background: var(--scrim); }

  .signin-sheet__providers { display: grid; gap: var(--space-xs); margin: var(--space-md) 0; }

  .signin-sheet__provider {
    min-height: var(--control-height-lg);
    padding: 0 var(--space-md);
    border: 1px solid var(--control-border);
    border-radius: var(--radius-md);
    background: var(--control-bg);
    color: var(--control-text);
    font: inherit;
    font-size: var(--type-size-sm);
    cursor: pointer;
    transition: background var(--motion-fast) var(--motion-ease);
  }
  .signin-sheet__provider:hover { background: var(--hud-item-bg-hover); }

  /**
   * The provider used on this device last time.
   *
   * It is already ordered first; this makes it visually primary. That pairing
   * is what stops someone signing up with Google, returning a month later,
   * tapping Discord, and landing in an empty account.
   */
  .signin-sheet__provider[data-remembered="true"] {
    border-color: transparent;
    background: var(--btn-primary-bg);
    color: var(--btn-primary-text);
    font-weight: var(--type-weight-medium);
  }
  .signin-sheet__provider[data-remembered="true"]:hover { background: var(--btn-primary-bg-hover); }

  .signin-sheet__hint { margin: 0; font-size: var(--type-size-xs); color: var(--hud-text-muted); }
  .signin-sheet__note { margin: var(--space-sm) 0 0; font-size: var(--type-size-xs); color: var(--hud-text-faint); }
  .signin-sheet__error { margin: var(--space-sm) 0 0; font-size: var(--type-size-sm); color: var(--hud-tone-error); }
  .signin-sheet__error[hidden] { display: none; }

  /* --------------------------------------------------------- export sheet */

  /*
    A receipt, not a form.

    Every question is answered before it opens and there is one primary action,
    so the layout leads with the picture of what you are about to get and puts
    the four answers beside it. Nothing here is a wizard step you must clear.
  */
  .export-sheet {
    inline-size: min(56rem, calc(100vw - 2 * var(--space-lg)));
    max-block-size: calc(100dvh - 2 * var(--space-lg));
    padding: 0;
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-lg);
    background: var(--hud-bg);
    color: var(--hud-text);
    box-shadow: var(--hud-menu-shadow), var(--hud-sheen);
    overflow: hidden;
  }
  .export-sheet::backdrop {
    background: oklch(0 0 0 / 0.55);
    backdrop-filter: blur(2px);
  }

  .xs-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    padding: var(--space-md) var(--space-lg);
    border-block-end: 1px solid var(--hud-border);
  }
  .xs-title { margin: 0; font-size: var(--type-size-lg); font-weight: var(--type-weight-medium); }
  .xs-close {
    min-inline-size: var(--control-height-md);
    min-block-size: var(--control-height-md);
    border: 0;
    border-radius: var(--radius-md);
    background: none;
    color: var(--hud-text-muted);
    font: inherit;
    cursor: pointer;
  }
  .xs-close:hover { background: var(--hud-item-bg-hover); color: var(--hud-text); }

  /* Picture and answers side by side; the picture goes on top when there is
     not room for both, because it is the thing being confirmed. */
  .xs-body {
    display: grid;
    grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
    gap: var(--space-lg);
    padding: var(--space-lg);
    overflow-y: auto;
  }
  @media (width < 44rem) {
    .xs-body { grid-template-columns: minmax(0, 1fr); }
  }

  /*
    The preview column: the image, the body-colour strip that reads it, and the
    one caption worth printing under it. Grouped so the strip stays with the
    thing it changes — it used to be a checklist row of its own, asking for a
    decision about a percentage nobody can act on.
  */
  .xs-side { display: grid; align-content: start; gap: var(--space-xs); }

  /*
    Drawn over the preview it describes.

    Under the tone strip this read as a caption about the swatches. A scrim
    rather than plain text: it has to stay legible over the chequerboard *and*
    over any of the ten body colours somebody may have put behind it.
  */
  .xs-empty {
    position: absolute;
    inset: 0;
    display: grid;
    place-content: center;
    justify-items: center;
    gap: var(--space-3xs);
    padding: var(--space-md);
    text-align: center;
    background: oklch(0 0 0 / 0.55);
    color: var(--hud-text);
    /* The preview underneath is still the subject; nothing here is clickable. */
    pointer-events: none;
  }
  /*
    Explicit, and it must be: `display: grid` above beats the UA's
    `[hidden] { display: none }`, so without this the overlay renders on every
    export. Same trap the disclosures hit.
  */
  .xs-empty[hidden] { display: none; }
  .xs-empty strong { font-size: var(--type-size-sm); }
  .xs-empty span {
    font-size: var(--type-size-xs);
    line-height: var(--type-leading-normal);
    color: var(--hud-text-muted);
  }
  .xs-empty__icon {
    inline-size: 28px;
    block-size: 28px;
    margin-block-end: var(--space-3xs);
    stroke: currentColor;
    fill: none;
    opacity: 0.75;
  }

  /*
    The exported PNG itself, not a re-render — so what is on screen and what
    lands in Downloads cannot disagree. Chequerboard behind it because
    transparency in a garment is what shows the avatar underneath.
  */
  .xs-preview {
    align-self: start;
    aspect-ratio: 585 / 559;
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-md);
    background-color: var(--hud-item-bg-hover);
    background-image:
      linear-gradient(45deg, oklch(1 0 0 / 0.045) 25%, transparent 25%),
      linear-gradient(-45deg, oklch(1 0 0 / 0.045) 25%, transparent 25%),
      linear-gradient(45deg, transparent 75%, oklch(1 0 0 / 0.045) 75%),
      linear-gradient(-45deg, transparent 75%, oklch(1 0 0 / 0.045) 75%);
    background-size: 16px 16px;
    background-position: 0 0, 0 8px, 8px -8px, -8px 0;
    overflow: hidden;
  }
  /* A body colour replaces the chequerboard, so transparency is shown as the
     skin it will actually be rather than as "nothing". */
  .xs-preview[data-tone] { background-image: none; }

  /*
    Magnified, because the edge work is 2px wide.

    Dilation fills the gutters between faces and the colour bleed sits under
    transparent pixels — at preview scale both are sub-pixel, so the toggle
    looked like it did nothing. `pixelated` matters here: the whole point is to
    see individual pixels appear in the gutter, and smoothing them away would
    defeat the exercise.
  */
  .xs-preview[data-zoom] .xs-preview__img {
    transform: scale(8);
    transition: transform var(--motion-base) var(--motion-ease);
  }
  .xs-preview { position: relative; }
  .xs-zoomtag {
    position: absolute;
    inset-block-end: var(--space-2xs);
    inset-inline-start: var(--space-2xs);
    padding: var(--space-3xs) var(--space-xs);
    border-radius: var(--radius-sm);
    background: oklch(0 0 0 / 0.6);
    color: var(--hud-text);
    font-size: var(--type-size-xs);
    pointer-events: none;
  }
  .xs-zoomtag[hidden] { display: none; }

  .xs-preview__img {
    display: block;
    inline-size: 100%;
    block-size: 100%;
    object-fit: contain;
    /* The template is pixel art; anything shown here should say so. */
    image-rendering: pixelated;
  }

  /* `align-content: start` because the rows must not stretch to fill the
     preview's height. With four steps they happened to fill it; at three the
     default spread them down the column with dead gaps between, which reads as
     three unrelated things rather than one receipt. */
  .xs-steps {
    display: grid;
    align-content: start;
    gap: var(--space-2xs);
    margin: 0; padding: 0; list-style: none;
  }

  .xs-step {
    border: 1px solid transparent;
    border-radius: var(--radius-md);
  }
  .xs-step:has(.xs-step__body:not([hidden])) {
    border-color: var(--hud-border);
    background: var(--hud-item-bg-hover);
  }

  .xs-step__head {
    display: grid;
    grid-template-columns: auto auto minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-xs);
    padding: var(--space-xs) var(--space-sm);
  }
  .xs-icon { inline-size: 16px; block-size: 16px; color: var(--hud-text-faint); flex: none; }
  .xs-step__label {
    font-size: var(--type-size-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--hud-text-faint);
  }
  /* The answer, and the reason the sheet needs no reading. */
  .xs-step__summary {
    font-size: var(--type-size-sm);
    overflow-wrap: anywhere;
  }
  .xs-step__toggle {
    padding: var(--space-3xs) var(--space-xs);
    border: 1px solid var(--control-border);
    border-radius: var(--radius-sm);
    background: var(--control-bg);
    color: var(--control-text);
    font: inherit;
    font-size: var(--type-size-xs);
    cursor: pointer;
  }
  .xs-step__toggle:hover { background: var(--hud-item-bg-active); }

  .xs-step__body {
    display: grid;
    gap: var(--space-sm);
    padding: 0 var(--space-sm) var(--space-sm) calc(var(--space-sm) + 16px + var(--space-xs));
  }
  /*
    `display: grid` beats the user agent's `[hidden] { display: none }`, so
    without this every panel rendered open and the sheet arrived as a form
    rather than the receipt it is meant to be. Any rule that sets `display` on
    something that can be hidden owes it this line.
  */
  .xs-step__body[hidden] { display: none; }
  .xs-step__detail { margin: 0; font-size: var(--type-size-xs); color: var(--hud-text-muted); }

  /* A note on a step nobody can change. Inline, because a disclosure around one
     short line costs a click to reveal something already worth showing. */
  .xs-step__aside {
    margin: 0;
    padding: 0 var(--space-sm) var(--space-xs) calc(var(--space-sm) + 16px + var(--space-xs));
    font-size: var(--type-size-xs);
    color: var(--hud-text-muted);
  }

  .xs-choices { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
  .xs-tones { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2xs); }
  .xs-choice--sm { min-block-size: 28px; padding-inline: var(--space-sm); font-size: var(--type-size-xs); }
  /* Swatches are the control and the value at once, so they carry no label —
     the name lives in `title` and `aria-label`. */
  .xs-tone {
    inline-size: 28px;
    block-size: 28px;
    padding: 0;
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-full);
    cursor: pointer;
    transition: transform var(--motion-spring-duration) var(--motion-spring);
  }
  .xs-tone:hover { transform: scale(1.12); }
  .xs-tone[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--accent); }
  .xs-choice {
    min-block-size: var(--control-height-md);
    padding-inline: var(--space-md);
    border: 1px solid var(--control-border);
    border-radius: var(--radius-md);
    background: var(--control-bg);
    color: var(--control-text);
    font: inherit;
    font-size: var(--type-size-sm);
    cursor: pointer;
    transition:
      background var(--motion-fast) var(--motion-ease),
      transform var(--motion-spring-duration) var(--motion-spring);
  }
  .xs-choice:hover { background: var(--hud-item-bg-hover); }
  .xs-choice:active { transform: scale(0.97); }
  /* Selected reads as pressed, not as a second primary action. */
  .xs-choice[aria-pressed="true"] {
    background: var(--hud-item-bg-active);
    box-shadow: inset 0 0 0 1px var(--accent);
  }

  /* The export's own refusal: the two things Roblox rejects outright. */
  .xs-problems {
    margin: 0;
    padding: var(--space-sm) var(--space-lg);
    color: var(--hud-tone-error);
    font-size: var(--type-size-sm);
  }
  .xs-problems[hidden] { display: none; }

  .xs-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    padding: var(--space-md) var(--space-lg);
    border-block-start: 1px solid var(--hud-border);
    background: var(--hud-item-bg-hover);
  }
  /* Worth more than any step above it, and it costs a paragraph. */
  .xs-after {
    display: grid;
    gap: var(--space-3xs);
    max-inline-size: 34rem;
    font-size: var(--type-size-xs);
    color: var(--hud-text-muted);
  }
  .xs-after strong { color: var(--hud-text); font-weight: var(--type-weight-medium); }
  .xs-after__cost { color: var(--hud-text-faint); }
  /* The note leads somewhere. Without this it was a fee and a requirement and
     no way to act on either. */
  .xs-after__link {
    justify-self: start;
    margin-block-start: var(--space-3xs);
    color: var(--accent);
    font-size: var(--type-size-xs);
    text-decoration-color: var(--border-strong);
  }

  .xs-go {
    flex: none;
    min-block-size: var(--control-height-lg);
    padding-inline: var(--space-xl);
    border: 0;
    border-radius: var(--radius-md);
    background: var(--btn-primary-bg);
    color: var(--btn-primary-text);
    font: inherit;
    font-size: var(--type-size-sm);
    font-weight: var(--type-weight-medium);
    cursor: pointer;
    transition:
      background var(--motion-fast) var(--motion-ease),
      transform var(--motion-spring-duration) var(--motion-spring);
  }
  .xs-go:hover:not(:disabled) { background: var(--btn-primary-bg-hover); box-shadow: var(--hud-accent-glow); }
  .xs-go:active:not(:disabled) { transform: scale(0.98); }
  .xs-go:disabled { opacity: var(--disabled-opacity); cursor: not-allowed; }
}
