/* ============================================================================
   clevelanddevstudio.com — the stylesheet. One file, for the whole domain.

   Served from /css/studio.css and linked by all five pages: the three static
   ones, and both Blazor apps. It replaced three separate sheets on 2026-08-27 -
   assets\site.css, backend\inventory\css\app.css and backend\projecthub\css\
   app.css - the last two of which were about 95% the same file.

   WHAT IS STILL SEPARATE, AND HAS TO BE. Each Blazor app also loads its own
   *.Client.styles.css. Those are Blazor's CSS ISOLATION output: per-component
   rules carrying a generated scope attribute (.sidebar[b-z9bweumn08]) that only
   matches markup the compiler stamped. They cannot be merged here, because the
   attribute is minted at build time and is different per component and per app.

   ------------------------------------------------------------------------
   THE THREE SECTIONS, AND WHY THE FIRST ONE IS SCOPED

   1. Tokens and fonts       global - the palette, once
   2. Website page chrome    SCOPED to body.site
   3. Application theming    global - Bootstrap's variables, for the two apps

   Section 2 is scoped and section 3 is not, and that asymmetry is deliberate.

   The website rules are page furniture: they set padding on <body>, style bare
   <header>, <section> and <h1>, and assume a document that is a page rather than
   an application shell. Unscoped, `body { padding: clamp(1.75rem, 5vw, 4rem) ...}`
   would indent both apps' entire layouts, and the bare element rules would reach
   into any markup that happens to use those tags. So every rule in that section
   is prefixed with body.site, and the three static pages carry <body class="site">.
   Nothing in section 2 can touch an app.

   Section 3 only ever redefines --bs-* variables and styles Bootstrap's own class
   names. The static pages do not load Bootstrap and have no elements with those
   classes, so those rules match nothing there and cost only bytes.

   ⚠ IF YOU ADD A PAGE RULE, PUT IT IN SECTION 2 AND SCOPE IT. An unscoped
   `.card` or `.row` here would collide with Bootstrap inside both apps. Two names
   already came close: site.css styles `a.row` - saved only by being element-
   qualified, since Bootstrap's grid `.row` is a bare class - and `.mark`, which
   Bootstrap also defines.

   ------------------------------------------------------------------------
   FONT PATHS ARE RELATIVE TO THIS FILE

   The three IBM Plex faces stayed in /assets/ where they have always been, so
   the @font-face URLs below climb out of /css/ to reach them. Moving this file
   without moving them, or the reverse, silently drops the brand font back to the
   system stack - it fails as a 404 on a woff2, not as a visible error.
   ============================================================================ */

@font-face {
    font-family: 'IBM Plex Sans';
    font-weight: 400;
    font-style: normal;
    font-display: swap;
    src: url(../assets/IBMPlexSans-Regular.woff2) format('woff2');
}

@font-face {
    font-family: 'IBM Plex Sans';
    font-weight: 600;
    font-style: normal;
    font-display: swap;
    src: url(../assets/IBMPlexSans-SemiBold.woff2) format('woff2');
}

@font-face {
    font-family: 'IBM Plex Mono';
    font-weight: 400;
    font-style: normal;
    font-display: swap;
    src: url(../assets/IBMPlexMono-Regular.woff2) format('woff2');
}

/* ---------------------------------------------------------------- 1. tokens

   THE PALETTE, DECLARED ONCE. It used to be written out in five places - this
   file's ancestor, both apps' app.css, and both copies of SiteMenu.razor.css -
   with all five agreeing by hand. Now the values live here and everything else
   is an alias.

   The --studio-* names are canonical. The rest exist because their consumers
   cannot be changed to use the canonical names:

     --void, --surface, ...   the website's own names, used throughout section 2
     --ci-*                   ControlsInventory.Client.styles.css
     --ph-*                   ProjectHub.Client.styles.css - COMPILED-ONLY, and
                              therefore permanently unable to be renamed

   Deliberately not pure black: dense tables get read for long stretches and #000
   maximises contrast fatigue. See docs\DESIGN-BASELINE.md for what each value is
   for, and for the component measurements that go with them. */

:root {
    --studio-void: #0c1118;      /* page field */
    --studio-surface: #172230;   /* cards, panels - one clear step above the field */
    --studio-raised: #202c3c;    /* inputs, chips - things you can act on */
    --studio-line: #253141;      /* hairlines */
    --studio-ink: #e3e9f1;       /* primary text */
    --studio-dim: #8a9bb1;       /* labels, captions, secondary text */
    --studio-signal: #4d9bff;    /* active state, links, focus */

    /* TWO TYPEFACE STACKS, AND THEY ARE NOT INTERCHANGEABLE.

       The website serves IBM Plex: it is three woff2 files on pages that cache
       normally. The apps are offline-capable PWAs whose service workers precache
       a fixed asset manifest, where a web font would either miss that cache or
       bloat it - so they stay on the system stack, which resolves to Segoe UI
       Variable and Cascadia Mono on Windows 11 and is no compromise.

       Do not "unify" these two. The split is the point. */
    --studio-font-page: 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
    --studio-font-page-mono: 'IBM Plex Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;
    --studio-font-app: system-ui, 'Segoe UI Variable Text', 'Segoe UI', Roboto, -apple-system, sans-serif;
    --studio-font-app-mono: ui-monospace, 'Cascadia Mono', 'Cascadia Code', Consolas, 'SF Mono', monospace;

    /* --- aliases: the website's names --- */
    --void: var(--studio-void);
    --surface: var(--studio-surface);
    --raised: var(--studio-raised);
    --line: var(--studio-line);
    --ink: var(--studio-ink);
    --dim: var(--studio-dim);
    --signal: var(--studio-signal);
    --font-ui: var(--studio-font-page);
    --font-mono: var(--studio-font-page-mono);

    /* --- aliases: Controls Inventory's scoped component sheet --- */
    --ci-void: var(--studio-void);
    --ci-surface: var(--studio-surface);
    --ci-raised: var(--studio-raised);
    --ci-line: var(--studio-line);
    --ci-ink: var(--studio-ink);
    --ci-dim: var(--studio-dim);
    --ci-signal: var(--studio-signal);
    --ci-font-ui: var(--studio-font-app);
    --ci-font-mono: var(--studio-font-app-mono);

    /* --- aliases: ProjectHub's scoped component sheet (cannot be renamed) --- */
    --ph-void: var(--studio-void);
    --ph-surface: var(--studio-surface);
    --ph-raised: var(--studio-raised);
    --ph-line: var(--studio-line);
    --ph-ink: var(--studio-ink);
    --ph-dim: var(--studio-dim);
    --ph-signal: var(--studio-signal);
    --ph-font-ui: var(--studio-font-app);
    --ph-font-mono: var(--studio-font-app-mono);
}

/* Feed the palette into Bootstrap, for the two apps. Both the hex and the -rgb
   form are needed: Bootstrap composes several utilities as
   rgba(var(--bs-*-rgb), opacity). Harmless on the static pages, which never load
   Bootstrap and so have nothing reading these. */
[data-bs-theme="dark"] {
    --bs-body-bg: var(--studio-void);
    --bs-body-bg-rgb: 12, 17, 24;
    --bs-body-color: var(--studio-ink);
    --bs-body-color-rgb: 227, 233, 241;

    --bs-emphasis-color: #ffffff;
    --bs-heading-color: var(--studio-ink);
    --bs-secondary-color: var(--studio-dim);
    --bs-secondary-bg: var(--studio-raised);
    --bs-tertiary-bg: var(--studio-surface);

    --bs-border-color: var(--studio-line);
    --bs-border-color-translucent: var(--studio-line);

    --bs-link-color: var(--studio-signal);
    --bs-link-color-rgb: 77, 155, 255;
    --bs-link-hover-color: #7db4ff;
    --bs-link-hover-color-rgb: 125, 180, 255;

    --bs-code-color: #9fc7ff;
    --bs-highlight-bg: rgba(77, 155, 255, 0.22);
}

/* ==========================================================================
   2. WEBSITE PAGE CHROME — every rule scoped to body.site

   The three static pages carry <body class="site">. Neither app does, which is
   what keeps all of this off them. See the note at the top of this file.
   ========================================================================== */


/* Already deployed for the root page's tagline, so --font-mono may as well be the
   brand's mono rather than whatever the OS supplies. */
/* NAV CHROME GEOMETRY - written as literal lengths on purpose.
   These used to be custom properties combined with calc(), which is tidier to read
   and has one nasty failure mode: if a var fails to resolve, `top` and `right` fall
   back to `auto`, and a position:fixed element with auto offsets renders at its
   STATIC position - the top LEFT of the content flow, stacked on the logo. A menu
   button that silently relocates to the wrong corner is not worth the elegance.

   The numbers, so they can be changed coherently:
     18px   inset from the corner - the logo and the panel share it
     44px   button box: the smallest touch target a fingertip hits reliably
     26px   visible icon, centred in that box
      9px   button offset = 18 - (44 - 26) / 2, so the ICON lands on 18px
     57px   panel top = 9 + 44 + 4 */
body.site * { box-sizing: border-box; }

body.site {
    margin: 0;
    padding: clamp(1.75rem, 5vw, 4rem) clamp(1rem, 5vw, 3rem) 4rem;
    background: var(--void);
    color: var(--ink);
    font-family: var(--font-ui);
    font-size: 16px;
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
}

body.site .wrap {
    max-width: 52rem;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 2.5rem;
}

body.site header { display: flex; flex-direction: column; gap: 1.1rem; }

body.site .logo { display: block; width: min(100%, 21rem); height: auto; }

body.site .mark { display: block; width: 3.25rem; height: auto; }

body.site header p { margin: 0; color: var(--dim); max-width: 34rem; }

body.site h1 {
    margin: 0;
    font-size: clamp(1.4rem, 3vw, 1.85rem);
    font-weight: 600;
    letter-spacing: -0.02em;
}

body.site h2 {
    margin: 0;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--dim);
    font-family: var(--font-mono);
}

body.site section { display: flex; flex-direction: column; gap: 0.75rem; }

body.site .list { display: flex; flex-direction: column; gap: 0.5rem; }

/* The whole row is the link target, so the click area is the card and not just
   the label. */
body.site a.row {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 0.3rem 1rem;
    align-items: baseline;
    padding: 0.85rem 1.05rem;
    background: var(--surface);
    border: 1px solid var(--line);
    border-left: 3px solid var(--line);
    border-radius: 6px;
    text-decoration: none;
    color: inherit;
    transition: border-color 120ms ease, background 120ms ease;
}

body.site a.row:hover,
body.site a.row:focus-visible {
    background: var(--raised);
    border-left-color: var(--signal);
    outline: none;
}

body.site a.row:focus-visible { border-color: var(--signal); }

body.site a.row .name { font-weight: 600; }

body.site a.row .path {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    color: var(--dim);
    justify-self: end;
    white-space: nowrap;
}

body.site a.row .what {
    grid-column: 1 / -1;
    font-size: 0.88rem;
    color: var(--dim);
}

body.site .tag {
    display: inline-block;
    font-family: var(--font-mono);
    font-size: 0.62rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 0.1rem 0.4rem;
    border-radius: 3px;
    border: 1px solid var(--line);
    color: var(--dim);
    margin-left: 0.5rem;
    vertical-align: 0.1em;
}

body.site .tag.live { color: var(--signal); border-color: var(--signal); }

body.site .back {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    color: var(--dim);
    text-decoration: none;
}

body.site .back:hover,
body.site .back:focus-visible { color: var(--signal); }

body.site .empty {
    padding: 1.05rem;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 6px;
    color: var(--dim);
    font-size: 0.92rem;
}

/* ---- Launcher tiles -------------------------------------------------------
   For /backend/, where the job is "open an application" rather than "read a list".
   a.row is the right shape for an inventory of things; this is the shape for a
   small number of destinations you came here to click, so the target is a tile
   rather than a line and the name is the loudest thing in it.

   auto-fit rather than a fixed two columns: with two apps it is two side by side,
   with three it reflows, and on a phone it is one - without a media query, and
   without anyone editing this rule when an app is added. The 15rem floor is set by
   the longest name plus its status line, not by taste. */
body.site .launch-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 0.6rem;
}

body.site .launch {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    padding: 1.15rem 1.25rem 1rem;
    background: var(--surface);
    border: 1px solid var(--line);
    /* Same left edge a.row uses, so hovering a tile and hovering a row mean the
       same thing on the same site. */
    border-left: 3px solid var(--line);
    border-radius: 6px;
    text-decoration: none;
    color: inherit;
    transition: border-color 120ms ease, background 120ms ease;
}

body.site .launch:hover,
body.site .launch:focus-visible {
    background: var(--raised);
    border-left-color: var(--signal);
    outline: none;
}

body.site .launch:focus-visible { border-color: var(--signal); }

body.site .launch-name { font-weight: 600; font-size: 1.05rem; letter-spacing: -0.01em; }

body.site .launch-what { font-size: 0.88rem; color: var(--dim); }

/* Pushed to the bottom of the tile so the status sits on the same line in every
   tile regardless of how long the description above it ran. */
body.site .launch-meta {
    margin-top: auto;
    padding-top: 0.6rem;
    display: flex;
    align-items: baseline;
    gap: 0.45rem;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    color: var(--dim);
}

body.site .launch-path { margin-left: auto; white-space: nowrap; }

/* The dot is never the only signal - the word beside it says the same thing. Colour
   alone would leave the state unreadable to anyone who cannot separate red from
   green, and would say nothing at all to a screen reader. */
body.site .dot {
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    background: var(--line);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
    /* Aligns the dot with the x-height of the text it labels rather than the
       baseline, where a round shape looks like it is sinking. */
    align-self: center;
    flex: none;
}

body.site .dot[data-state="checking"] { background: var(--dim); }
body.site .dot[data-state="up"] { background: #2fbf6a; }
body.site .dot[data-state="degraded"] { background: #d99a2b; }
body.site .dot[data-state="down"] { background: #d1444d; }

body.site .launch-stat:not(:empty) {
    font-family: var(--font-ui);
    font-size: 0.8rem;
    color: var(--ink);
}

body.site footer {
    padding-top: 1.5rem;
    border-top: 1px solid var(--line);
    font-size: 0.85rem;
    color: var(--dim);
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem 1.25rem;
}

body.site footer code { font-family: var(--font-mono); font-size: 0.92em; color: var(--ink); }

/* ---- Corner lockup --------------------------------------------------------
   The mark with the studio name stacked under it, in the corner opposite the menu
   button and on the same 18px inset. Identification only - not a link, and not
   interactive. Everything you can go to is in the menu.

   flex rather than block, which also disposes of the descender gap an inline <img>
   would leave under itself - a flex item has no baseline to sit on. */
/* ⚠ THE LOCKUP MOVED TO THE RIGHT on 2026-08-31, and it moved because the button
   moved. The menu button took the left corner when the two menus became one, and
   these two cannot share a corner - a 44px button on a 9px inset lands squarely on
   the mark. "Opposite the menu button" is the rule; which corner each of them holds
   is not. align-items follows the side, or the name would hang off the mark's left
   edge and point away from the page. */
body.site .home {
    position: fixed;
    top: 1.125rem;
    right: 1.125rem;
    z-index: 3;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.55rem;
}

body.site .home-name {
    font-weight: 600;
    font-size: 0.95rem;
    letter-spacing: -0.015em;
    line-height: 1.2;
}

/* ---- Hamburger and menu ---------------------------------------------------
   The same site nav the root page carries, sized for these pages. It is declared
   here rather than shared with the root index because that page is a title card
   driven by a 4x scale lever - every size in its copy is a fraction of that
   lever, which would be meaningless on a 16px page.

   Three bars, all one colour. An earlier version tinted the middle bar with the
   brand accent to echo the mark's one blue blade; it read as a highlighted line
   rather than as a menu icon, so this is conventional now and the accent stays
   where it belongs, on the mark.

   Opening turns the box a quarter turn while the outer bars swing to 45deg. A
   quarter turn lands the X back at 45deg either way, so the end state is still the
   universal close affordance. Behaviour lives in assets/menu.js. */
/* ⚠ LEFT, NOT RIGHT. This was the right-hand corner until 2026-08-31, opposite each
   app's own rail hamburger on the left. The rails are gone and this is the only menu
   button on the domain, so it took the left corner they vacated - and the apps' copy
   of it in SiteMenu.razor.css did the same, on the same numbers, so the button does
   not move when you cross from a page into an app. On /content/ the corner lockup
   moved to the right to get out of the way. */
body.site .burger {
    position: fixed;
    top: 0.5625rem;    /* 9px  - see the geometry note at the top */
    left: 0.5625rem;
    width: 2.75rem;    /* 44px touch target */
    height: 2.75rem;
    display: grid;
    place-items: center;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    z-index: 3;
}

/* The visible icon inside the touch target: --icon wide, and tall enough to give
   three bars real gaps rather than a solid block. */
body.site .bars { position: relative; display: block; width: 1.625rem; height: 1.25rem; }

body.site .bars i {
    position: absolute;
    left: 0;
    width: 100%;
    height: 3px;
    /* Half the bar height, so each `top` centres its bar rather than hanging it.
       translateY(-50%) would do the same until the open state needs `transform`
       for the rotation. */
    margin-top: -1.5px;
    border-radius: 1.5px;
    background: var(--ink);
}

body.site .bars i:nth-child(1) { top: 10%; }
body.site .bars i:nth-child(2) { top: 50%; }
body.site .bars i:nth-child(3) { top: 90%; }

body.site .bars,
body.site .bars i { transition: transform 320ms cubic-bezier(.2, .8, .2, 1), opacity 160ms ease; }

body.site .burger[aria-expanded="true"] .bars { transform: rotate(90deg); }
body.site .burger[aria-expanded="true"] .bars i:nth-child(1) { top: 50%; transform: rotate(45deg); }
body.site .burger[aria-expanded="true"] .bars i:nth-child(2) { opacity: 0; transform: scaleX(0.2); }
body.site .burger[aria-expanded="true"] .bars i:nth-child(3) { top: 50%; transform: rotate(-45deg); }

body.site .burger:focus-visible { outline: 1px solid var(--signal); outline-offset: 0.25rem; }

/* Dims the page behind the panel. The panel is the only thing to attend to while
   it is open, and the dimmed area doubles as a dismiss target. */
body.site .scrim {
    position: fixed;
    inset: 0;
    z-index: 1;
    background: rgba(12, 17, 24, 0.8);
    opacity: 0;
    visibility: hidden;
    transition: opacity 200ms ease, visibility 200ms;
}

body.site .scrim.open { opacity: 1; visibility: visible; }

body.site .menu {
    position: fixed;
    top: 3.5625rem;   /* 57px - just under the button box */
    left: 1.125rem;   /* 18px - same inset as the button's icon and the logo */
    z-index: 2;
    display: flex;
    flex-direction: column;
    min-width: 13.5rem;

    /*  THE PANEL CAN NOW OUTGROW A SHORT WINDOW, so it is allowed to scroll.
        Absorbing both apps' rails took it from 5 rows to 13 - about 533px - which
        still fits a phone in portrait but not a short laptop window or a phone in
        landscape. Without this the last rows would be unreachable off the bottom of
        the viewport with no indication they were there.

        `hidden auto` rather than plain `auto`: horizontal stays clipped, so the
        rounded corners keep clipping the first and last rows. */
    max-height: calc(100vh - 5rem);
    overflow: hidden auto;

    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 6px;
    box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.5);
    /* visibility, not display: it can transition, and a visibility:hidden subtree
       is untabbable without needing inert or tabindex juggling. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(-0.35rem);
    transition: opacity 160ms ease, transform 260ms cubic-bezier(.2, .8, .2, 1), visibility 260ms;
}

body.site .menu.open { opacity: 1; visibility: visible; transform: none; }

body.site .item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.25rem;
    padding: 0.7rem 0.9rem;
    border-left: 3px solid transparent;
    text-decoration: none;
    color: inherit;
    font-weight: 600;
    font-size: 0.95rem;
    white-space: nowrap;
    transition: background 120ms ease, border-left-color 120ms ease;
}

body.site .item + .item { border-top: 1px solid var(--line); }

/* Same lean toward the seam colour that a.row uses, so the menu introduces no
   new hover language. */
body.site a.item:hover,
body.site a.item:focus-visible {
    background: var(--raised);
    border-left-color: var(--signal);
    outline: none;
}

/* The page you are already on: still listed, so the nav does not change shape
   from page to page, but not a link and not competing for attention. */
body.site .item.current { color: var(--dim); font-weight: 400; }

/* An application, which lives inside /backend/ rather than beside it. Indented by
   the difference so the LABEL steps in while the border-left marker stays on the
   same vertical line as every other item - the hover accent is a single edge down
   the panel, and a nested item that moved it would break that line. */
body.site .item.sub { padding-left: 1.9rem; }

/*  A screen INSIDE an application - the third level, added 2026-08-31 when each
    app's rail moved into this menu. One more step of indent, and lighter than the
    two levels above it: these are the most numerous rows in the panel and reading
    them as a block under their app matters more than reading each one as a
    destination in its own right.

    ⚠ NO "active" RULE HERE, deliberately. The apps' copy of this menu marks the
    screen you are on, because there a NavLink knows the current route. A static page
    is not inside either app, so no screen row can be current on one - and a hand-
    maintained class saying otherwise is exactly the kind of thing that goes stale.
    The three site rows still mark themselves with .current, as they always did. */
body.site .item.screen {
    padding-left: 2.9rem;
    font-weight: 400;
    font-size: 0.9rem;
    color: var(--dim);
}

/* ---- Root page: centred brand over a panel grid ---------------------------
   For a page that IS its layout rather than a document in a column.
   `body.full` drops the document padding, so the 10px on .home-layout is the only
   margin on the page and it matches the gutter between panels exactly - one even
   field of gaps rather than a box inside a differently-sized margin.

   The panels fill whatever height the brand leaves, so they are equal rectangles
   rather than literal squares; a 2x2 of true squares cannot fill a 16:9 window
   without leaving dead space. Add `aspect-ratio: 1` to .panel if squares matter
   more than filling the page. */
body.site.full { padding: 0; }

body.site .home-layout {
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 10px;
}

/* The identity, centred and on its own - not sitting inside one of the panels.
   Sized at twice what it was there. */
body.site .brand {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 1rem;
    padding: 2.5rem 1rem 1.5rem;
}

body.site .brand h1 {
    /* 4rem is twice what it was inside a panel. The FLOOR is the part that matters:
       at 3rem "Cleveland Dev Studio" is about 600px wide, so on a 430px phone it
       overflowed and dragged the whole layout past the right edge - the panels
       looked clipped, but the headline was the cause. A vw-based middle term lets
       it shrink to the screen; the floor is now small enough to fit one. */
    font-size: clamp(1.75rem, 6.5vw, 4rem);
    line-height: 1.05;
    /* Last resort on a very narrow screen: wrap rather than overflow. */
    overflow-wrap: break-word;
    max-width: 100%;
}

body.site .brand .tagline {
    margin: 0;
    font-family: var(--font-mono);
    font-size: clamp(1rem, 1.4vw, 1.35rem);
    letter-spacing: 0.02em;
    color: var(--dim);
}

body.site .grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr 1fr;
    gap: 10px;
    /* Takes the height the brand does not. min-height:0 because a flex item
       defaults to min-height:auto, which would let tall panel content push the
       grid past the bottom of the window instead of the tracks sharing it out. */
    flex: 1;
    min-height: 0;
}

/* One column on a phone or a narrow window: two panels side by side stop being
   readable long before the viewport runs out. The 10px stays - it is the page's
   margin as well as its gutter. */
@media (max-width: 46rem) {
    body.site .grid {
        grid-template-columns: 1fr;
        grid-template-rows: repeat(4, minmax(9rem, auto));
    }

    body.site .brand { padding: 1.5rem 0.5rem 1rem; }
}

body.site .panel {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    padding: 1.6rem;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 6px;
    /* A grid item defaults to min-width:auto, so one long unbreakable string in a
       panel would push the whole column wider than its track. */
    min-width: 0;
}

body.site .panel p { margin: 0; }

body.site .panel .note { color: var(--dim); font-size: 0.92rem; }

/* One quarter turn on load, then it stops. A pinwheel settling into place is
   finite and earned; a mark that spins forever is decoration.

   6.5rem is twice the 3.25rem it was when it lived inside a panel. */
body.site .brand-mark {
    display: block;
    width: 6.5rem;
    height: auto;
    animation: settle 620ms cubic-bezier(.2, .8, .2, 1) both;
}

@keyframes settle {
    from { transform: rotate(-90deg); }
    to { transform: none; }
}

/* ---- Content column -------------------------------------------------------
   For pages built from _page-template.html. The corner lockup is fixed, roughly
   18 + 79px tall, so content has to start below it on a narrow window where the
   two would otherwise share the same horizontal band. */
body.site .page {
    max-width: 52rem;
    margin: 0 auto;
    padding-top: 5rem;
}

@media (prefers-reduced-motion: reduce) {
    body.site * { transition: none !important; }
    body.site .brand-mark { animation: none; }
}

/* ==========================================================================
   3. APPLICATION THEMING — Bootstrap's variables, for the two Blazor apps

   Global, not scoped: these only redefine --bs-* or style Bootstrap's own class
   names, and the static pages do not load Bootstrap, so none of it matches
   anything there.

   Shared by both apps unless a rule says otherwise. The two app.css files this
   came from were about 95% identical; what follows is that shared body, then the
   handful of rules only one app has.
   ========================================================================== */

/* Feed the palette into Bootstrap. Both the hex and the -rgb form are needed:
   Bootstrap composes several utilities as rgba(var(--bs-*-rgb), opacity). */
/* ------------------------------------------------------------------ base */

/* :not(.site) so these cannot reach the static pages, which set their own font and
   field in section 2. Without it this would be a specificity race that the website
   only happens to win, and the prize for losing is the whole site silently
   switching off IBM Plex. */
html,
body:not(.site) {
    font-family: var(--studio-font-app);
    background-color: var(--studio-void);
}

body:not(.site) {
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
    letter-spacing: -0.011em;
}

h1:focus {
    outline: none;
}

.content {
    padding-top: 1.5rem;
    padding-bottom: 3rem;
}

::selection {
    background: rgba(77, 155, 255, 0.3);
}

/* Keyboard focus must stay visible everywhere. The template shipped a white
   ring, which disappears against a dark field. */
:focus-visible {
    outline: 2px solid var(--studio-signal);
    outline-offset: 2px;
}

/* --------------------------------------------------------------- surfaces */

.card {
    --bs-card-bg: var(--studio-surface);
    --bs-card-border-color: var(--studio-line);
    --bs-card-cap-bg: transparent;
}

.list-group {
    --bs-list-group-bg: var(--studio-surface);
    --bs-list-group-border-color: var(--studio-line);
    --bs-list-group-color: var(--studio-ink);
    --bs-list-group-action-hover-bg: var(--studio-raised);
    --bs-list-group-action-hover-color: #fff;
    --bs-list-group-action-active-bg: var(--studio-raised);
}

/* ----------------------------------------------------------------- tables */

.table {
    --bs-table-color: var(--studio-ink);
    --bs-table-bg: transparent;
    --bs-table-border-color: var(--studio-line);
    --bs-table-hover-color: #fff;
    --bs-table-hover-bg: rgba(77, 155, 255, 0.07);
}

/* Column labels are metadata, not content: set them apart by weight and case
   rather than by adding a rule or a fill. */
.table > thead th {
    color: var(--studio-dim);
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    border-bottom: 1px solid var(--studio-line);
    white-space: nowrap;
}

/* Numeric columns get tabular figures so digits line up down the column - the
   whole point of right-aligning money and quantities. */
.num {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}

/* ---- expansion rows (inline editor, delete confirm, part usage) -----------
   These attach to the row above them and must not behave like data rows.

   Bootstrap 5.3 tints cells with `box-shadow: inset 0 0 0 9999px` rather than a
   background, so setting box-shadow or background-color here would be fought by
   its own rule. Set --bs-table-bg for the fill, use a real border for the
   accent, and neutralise the hover tint. */
.editor-row,
.confirm-row,
.usage-row {
    --bs-table-hover-bg: transparent;
    --bs-table-hover-color: var(--studio-ink);
}

.editor-row > td {
    --bs-table-bg: var(--studio-surface);
    border-left: 3px solid var(--studio-signal);
    padding: 1rem 1.25rem 1.25rem;
}

.confirm-row > td {
    --bs-table-bg: #35191d;
    border-left: 3px solid #d1444d;
    padding: 0.85rem 1.25rem;
}

.usage-row > td {
    --bs-table-bg: var(--studio-surface);
    border-left: 3px solid var(--studio-line);
    padding: 0.85rem 1.25rem;
}

/* Nested tables inside an expansion row sit on the panel, not the page. */
.usage-row .table {
    --bs-table-bg: transparent;
}

/* A note is typed prose: keep the author's line breaks, and stop a pasted URL
   or part number from pushing the card wider than the page. */
.note-body {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* The two-step delete confirmation outside a table. Same meaning as
   .confirm-row, but it cannot borrow --bs-table-bg, so the fill is set
   directly. */
.confirm-panel {
    background: #35191d;
    border-left: 3px solid #d1444d;
    border-radius: 6px;
    padding: 0.85rem 1.1rem;
}

/* ------------------------------------------------------------------ forms */

.form-control,
.form-select {
    background-color: var(--studio-raised);
    border-color: var(--studio-line);
    color: var(--studio-ink);
}

.form-control::placeholder {
    color: #63768d;
}

.form-control:focus,
.form-select:focus {
    background-color: var(--studio-raised);
    border-color: var(--studio-signal);
    color: var(--studio-ink);
    box-shadow: 0 0 0 0.2rem rgba(77, 155, 255, 0.25);
}

.form-control:disabled {
    background-color: var(--studio-surface);
    color: var(--studio-dim);
}

.form-label {
    color: var(--studio-dim);
    font-size: 0.78rem;
    font-weight: 500;
    margin-bottom: 0.25rem;
}

.form-text {
    color: var(--studio-dim);
}

/* Native date pickers render a dark glyph on a dark field otherwise. */
input[type="date"]::-webkit-calendar-picker-indicator {
    filter: invert(0.75);
    cursor: pointer;
}

/* ---------------------------------------------------------------- buttons */

.btn-primary {
    --bs-btn-color: #fff;
    --bs-btn-bg: #2d7ae0;
    --bs-btn-border-color: #2d7ae0;
    --bs-btn-hover-color: #fff;
    --bs-btn-hover-bg: #4d9bff;
    --bs-btn-hover-border-color: #4d9bff;
    --bs-btn-active-bg: #1f66c4;
    --bs-btn-active-border-color: #1f66c4;
}

.btn:focus-visible {
    box-shadow: 0 0 0 0.2rem rgba(77, 155, 255, 0.35);
}

/* ------------------------------------------------------------------ chips */

/* Status and priority badges are one RANKED system, not five independent
   Bootstrap variants. Bootstrap's stock colours rank correctly on a light page
   but invert on a dark one: text-bg-warning (#ffc107 on black) becomes the
   brightest object on screen, so "High" shouts louder than "Urgent" and the eye
   lands on the wrong row first.
   Deliberate order of visual weight, loudest first:
     danger (Urgent)  >  warning (High / short on parts)
       >  success, info (Active, Planning - status, not alarm)
         >  secondary, light, dark (Normal, Low, dates, closed states)
   Bootstrap sets these with !important, so matching it is required. */
.badge {
    font-weight: 600;
    letter-spacing: 0.02em;
    padding: 0.3em 0.55em;
}

/* Urgent must out-rank High on luminance, not just on hue. Note that darkening
   amber alone cannot achieve this: the luminance formula weights green at 0.7152
   against red's 0.2126, so amber is intrinsically brighter than red at any
   comparable depth. Both values move - red up, amber down.
   Measured relative luminance: red 0.182, amber 0.123.
   Text is pure white on the red to clear 4.5:1 at 12px bold (#ffe3e5 gave 3.7). */
.badge.text-bg-danger {
    background-color: #d1444d !important;
    color: #ffffff !important;
}

.badge.text-bg-warning {
    background-color: #8a5710 !important;
    color: #ffe7c2 !important;
}

.badge.text-bg-success {
    background-color: #1d6b45 !important;
    color: #c9f2dc !important;
}

.badge.text-bg-info {
    background-color: #1d5a78 !important;
    color: #cbe9f7 !important;
}

.badge.text-bg-secondary {
    background-color: var(--studio-raised) !important;
    color: var(--studio-dim) !important;
}

/* Quiet chips: due dates, and closed/cancelled states. */
.badge.text-bg-light {
    background-color: var(--studio-raised) !important;
    color: var(--studio-dim) !important;
    font-weight: 500;
}

.badge.text-bg-dark {
    background-color: #101620 !important;
    color: #7f91a7 !important;
    box-shadow: inset 0 0 0 1px var(--studio-line);
}

/* ------------------------------------------------------------ identifiers */

/* Project codes and part numbers are the app's primary lookup keys. Monospace
   with a cool tint makes LUB-4021-SS and LUB-4021-S5 distinguishable at a
   glance - functional, not ornamental. Inputs keep the normal ink colour so
   they still read as editable. */
.font-monospace {
    font-family: var(--studio-font-app-mono);
    font-size: 0.92em;
}

.font-monospace:not(.form-control) {
    color: #a9c6e8;
    letter-spacing: 0.01em;
}

code {
    font-family: var(--studio-font-app-mono);
    color: var(--bs-code-color);
}

/* -------------------------------------------------------------- validation */

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #2fbf6a;
}

.invalid {
    outline: 1px solid #ff6b6b;
}

.validation-message {
    color: #ff8f8f;
}

/* ------------------------------------------------------- boot + error UI */

/* The splash paints before Blazor starts, so it must already be dark or the
   app opens with a white flash. */
.loading-progress {
    position: absolute;
    display: block;
    width: 8rem;
    height: 8rem;
    inset: 20vh 0 auto 0;
    margin: 0 auto;
}

.loading-progress circle {
    fill: none;
    stroke: var(--studio-line);
    stroke-width: 0.5rem;
    transform-origin: 50% 50%;
    transform: rotate(-90deg);
}

.loading-progress circle:last-child {
    stroke: var(--studio-signal);
    stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
    transition: stroke-dasharray 0.05s ease-in-out;
}

.loading-progress-text {
    position: absolute;
    text-align: center;
    font-weight: 600;
    color: var(--studio-dim);
    inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}

.loading-progress-text:after {
    content: var(--blazor-load-percentage-text, "Loading");
}

/* Stays loud on purpose - it only appears when something is broken - but tinted
   to the theme rather than flashing a white-yellow band. */
#blazor-error-ui {
    background: #3a2a12;
    color: #ffd899;
    border-top: 2px solid #d99a2b;
    bottom: 0;
    box-sizing: border-box;
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

#blazor-error-ui a {
    color: #ffd899;
    text-decoration: underline;
}

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

.blazor-error-boundary {
    background: #5a1a1a;
    padding: 1rem;
    color: #ffd7d7;
    border-radius: 6px;
}

.blazor-error-boundary::after {
    content: "An error has occurred."
}

/* --------------------------------------------------- Controls Inventory only

   Rules for features ProjectHub does not have: sortable column headings, the
   vendor-lines chip editor, and the row-count caption under the filters.
   -------------------------------------------------------------------------- */

.view-summary {
    font-size: 0.85rem;
    color: var(--studio-dim);
}

/* ---- sortable headings ----------------------------------------------------
   A real <button> inside the <th>, not a click handler on the cell. It is
   reachable by Tab, activates on Enter and Space for free, and announces itself
   as a control - none of which a clickable <th> does. The button is stripped back
   to inherit the heading's own type so the affordance is the pointer and the
   arrow, not a box. */
.table > thead th.sortable { padding: 0; }

.sort-btn {
    display: inline-flex;
    align-items: baseline;
    gap: 0.3rem;
    width: 100%;
    padding: 0.5rem;
    background: none;
    border: 0;
    color: inherit;
    font: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    text-align: inherit;
    cursor: pointer;
    border-radius: 4px;
}

/* Right-aligned columns need their button contents right-aligned too, or the
   label detaches from the numbers underneath it. */
.table > thead th.text-end .sort-btn { justify-content: flex-end; }

.sort-btn:hover { color: var(--studio-ink); background: var(--studio-surface); }

/* Reserved on every heading, so a column does not shift sideways when it becomes
   the sorted one. */
.sort-arrow {
    display: inline-block;
    min-width: 0.7em;
    color: var(--studio-signal);
    font-size: 0.9em;
}

/* ---- expansion rows (inline editor, delete confirm) -----------------------
   These attach to the row above them and must not behave like data rows.

   Bootstrap 5.3 tints cells with `box-shadow: inset 0 0 0 9999px` rather than a
   background, so setting box-shadow or background-color here would be fought by
   its own rule. Set --bs-table-bg for the fill, use a real border for the accent,
   and neutralise the hover tint. */
.editor-row,
/* ------------------------------------------------------------------ forms */

.form-control,
.form-control:focus,
.form-check-input {
    background-color: var(--studio-raised);
    border-color: var(--studio-line);
}

.form-check-input:checked {
    background-color: #2d7ae0;
    border-color: #2d7ae0;
}

/* The vendors page. The button that opens a vendor's lines editor, sitting at
   the end of that vendor's chips. Sized down to the chips beside it rather than
   to the row's other buttons - it belongs to the set of chips, and a full-height
   button there reads as a second Edit and pulls the eye off the actual one. */
.chip-add {
    --bs-btn-padding-y: 0.1rem;
    --bs-btn-padding-x: 0.4rem;
    --bs-btn-font-size: 0.75rem;
    line-height: 1.4;
}

/* One line inside that editor: chip, note, Remove. The hairline is what keeps a
   vendor carrying a dozen lines from reading as one paragraph. */
.line-row {
    padding: 0.3rem 0;
    border-bottom: 1px solid var(--studio-line);
}

.line-row:last-child { border-bottom: none; }