/* AL Workbench - the app's global foundation.

   Was base.css, and was the last of the three legacy sheets; PR 17e emptied it
   of everything that duplicated or fought the design layer, and what is left is
   the layer *underneath* it:

   - the reset, the document defaults and the body type
   - link colour (#525 - the design layer styles no bare `a`, so this is ours)
   - the two utilities the app still leans on, .muted and .caption
   - the content column's own scroll and spacing
   - two patches the design layer needs and does not carry
   - #blazor-error-ui and Blazor's .valid/.invalid, both framework chrome

   Not one rule here names a class the design layer also defines - that was the
   test for whether the file could keep existing, and the whole design-layer
   bridge retired when it passed. Loads BEFORE the design layer would be wrong
   (a reset must lose to a component); it loads after, and stays out of the way
   by not repeating any of its names. Keep it that way. */

* { box-sizing: border-box; }

/* The root is 16px and the body is 14px, and the split is deliberate.
   `rem` resolves against the ROOT element, and tokens.css declares the type
   scale in rem "against a 16px root" -- so while html was 14px, every one of
   --text-2xs..--text-3xl rendered at 87.5% of its documented size, app-wide.
   A page title was 19.25px where the hand-off specifies 22px (#544).
   Separating the two puts the scale back on the root it was drawn for while
   leaving the inherited default at 14px exactly as before.

   The scale is the ONLY thing in rem: the hand-off sheets use px everywhere
   outside tokens.css, and our own remaining rem values were pinned to their
   14px-root pixel equivalents in the same change. Keep it that way -- a new
   rem elsewhere now means something different than the author intends. */
html { font-size: 16px; }

html, body {
    margin: 0;
    padding: 0;
    /* Bound the viewport-level scroll so the shell grid can rely on
       .app__content (overflow-y: auto) as the actual scroll container.
       shell.css sizes .shell-root and .app with height: 100%, which only
       resolves if their ancestors are height-bounded too. Without this the
       page grows taller than the viewport and BODY scrolls instead — which
       silently breaks position: sticky on anything that expects
       .app__content to be its scrolling ancestor. */
    height: 100%;
    overflow: hidden;
    /* --sans aliases --font-sans: Segoe UI first (resolved from the user's own
       Windows install), then the self-hosted Selawik fallback declared in
       fonts.css, then system-ui. See .design/design-migration.md. */
    font-family: var(--font-sans);
    line-height: 1.5;
    color: var(--ink);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

body { font-size: 14px; }

/* Link colour, and the answer to #525: the design layer styles no bare `a`, so
   this is the app's to own. --primary-ink is the token the system designates
   for teal text on a light surface (6.5:1 on --bg, 6.25:1 on --primary-weak);
   --primary itself is 2.5:1 and is for accents and fills, never for words. */
a { color: var(--primary-ink); text-decoration: none; }
a:hover { text-decoration: underline; }

/* Controls that happen to be anchors must not pick up the prose underline.
   components.css and shell.css declare `text-decoration: none` on the element
   itself, but `a:hover` is (0,1,1) and app.css loads after both, so on hover
   it won. That underlined the user button, every sidebar item and the brand —
   the whole shell, not just one control. .btn re-declares it on :hover for
   exactly this reason; these are the components that don't.
   Retires with this sheet. */
a.nav-item:hover,
a.nav-link-btn:hover,
a.brand__link:hover,
a.user-btn:hover,
a.browse-card:hover,
a.tool-tile:hover,
a.cue:hover,
a.tag:hover,
a.audit__sum:hover,
a.activity__row:hover,
a.toc-link:hover,
a.errlink:hover,
a.menu__item:hover,
a.check:hover { text-decoration: none; }

/* <FocusOnNavigate Selector="h1"> in Routes.razor moves focus to the page's
   heading after every navigation, so a screen reader announces the new page
   instead of leaving the user where the old one was. That is right and stays.

   What it also does is stamp `tabindex="-1"` on the heading — and on a full
   page load Chrome then matches :focus-visible and paints its own default
   `outline: auto 1px` in near-black, which is neither a design-system colour
   nor, to a sighted user, a signal about anything. It reads as a selection box
   round the title of every page you open.

   So: keep the focus, drop the ring. The usual "never remove focus rings" rule
   protects things you can operate with a keyboard; a heading with tabindex="-1"
   is not in the tab order and there is nothing to do to it. Interactive focus
   styling everywhere else is untouched.

   NOTE: this rule does NOT retire with the rest of this sheet — it belongs to our router, not
   to the pages. Move it, don't delete it. */
h1[tabindex="-1"]:focus, h1[tabindex="-1"]:focus-visible { outline: none; }

h1 { font-size: 22px; font-weight: 600; margin: 0 0 12px; }
h2 { font-size: 16px; font-weight: 600; margin: 0 0 8px; }

/* ---------- Shell: transition remainders ----------

   The shell itself is now shell.css (.app / .app__nav / .app__top /
   .app__content), a verbatim copy of the design hand-off. What stays here is
   the scaffolding the *unmigrated* pages still depend on, and it retires with
   the last of them.

   The hand-off's .app__content-inner is a plain grid, because in the design
   system every page is a .page that fills its column. Ours aren't yet: pages
   still on tools.css/admin.css set their own max-width (.admin-page,
   .audit-page, .workspace-page) and relied on .content > * to centre them.

   `width: 100%` is load-bearing, not belt-and-braces. The old .content was a
   BLOCK, where margin-inline: auto is a no-op for an auto-width child. On a
   GRID it is not: auto margins make the item shrink to fit-content and centre,
   which collapsed every page to its content width — and any .page narrower
   than 1080px silently tripped the container query in pages-forms.css, so the
   generator's preview pane stacked under the form. Pinning the width first
   leaves auto margins with only the max-width leftover to distribute.

   It applies to PAGE ROOTS, though, and a page can also mount an out-of-flow
   overlay at its root. Those are not grid items and never needed the fix, but
   they were getting it: `width: 100%` on a `position: fixed` box resolves
   against the VIEWPORT, so the toast stack -- 400px, anchored bottom-right --
   rendered as a full-width bar across the bottom of the window on both pages
   that had one. Exclude out-of-flow roots; CSS can't select on `position`, so
   they're named. Add to the list when a page mounts a new one.            */

.app__content-inner > *:not(.toast-stack) { width: 100%; margin-inline: auto; }

/* The scroller has to be the containing block for what it scrolls. The
   visually-hidden helpers (.u-sr, .u-sr-only, the real checkbox inside .switch
   and .check) are `position: absolute`, and without this the nearest positioned
   ancestor is .app. An absolute box escapes a scroller that is not its
   containing block, so a hidden label 900px down a page became 900px of
   scrollable overflow on .app itself -- which is `overflow: hidden`, so it
   shows no scrollbar but still scrolls when asked. An in-page anchor link asks:
   the jump scrolls every scroll container up the chain, and the whole shell
   slid up, leaving a dead band under the sidebar and the content. */
.app__content { position: relative; }

/* Full-height tools (Compare, Translator, the Object Explorer source viewer)
   size themselves with `height: 100%`. That used to resolve against `.content`,
   which was a flex item with a definite height. The hand-off's
   `.app__content-inner` sits between them and is auto-height, so the
   percentage had nothing to resolve against and the CodeMirror panes collapsed
   to ZERO pixels — present in the DOM, invisible on screen. min-height gives
   the inner a definite used height without capping it, so it still grows and
   scrolls for ordinary pages.

   But `min-height` alone is not enough, and is actively harmful on its own: a
   grid with leftover space and the default `align-content` STRETCHES its auto
   rows. Every ordinary page then stretched to the full scrollport — and since
   `.page` is itself a grid, its own rows stretched too, so the Cookbook's head,
   filter bar and cards all ballooned to several times their height.

   So: start-aligned by default, and stretch only when the page asks for it. */
.app__content-inner { min-height: 100%; align-content: start; }


.data-table__actions .btn + .btn { margin-left: 8px; }


/* "Live" / "Ready" pill with a glowing green dot, per the design hand-off
   (.design/translator/styles/screens.css). A modifier so the existing blue
   pills are untouched. */

.status-pill--live .status-pill__dot {
    width: 7px;
    height: 7px;
    border-radius: 999px;
    background: var(--success);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 22%, transparent);
}

/* ---------- Errors ---------- */

#blazor-error-ui {
    background: var(--danger);
    color: white;
    padding: 8.4px 14px;
    box-shadow: 0 -1px 2px var(--shadow-color);
    display: none;
    position: fixed;
    bottom: 0; left: 0; right: 0;
    z-index: 1000;
}
#blazor-error-ui .reload, #blazor-error-ui .dismiss {
    color: white;
    margin-left: 12px;
    cursor: pointer;
}

.valid.modified:not([type=checkbox]) { outline: 1px solid var(--success); }
.invalid { outline: 1px solid var(--danger-text); }

/* ---------- Form fields keep their rows at the top ---------- */

/* A .field is a grid of label, control and hint. Side by side in a .form-grid,
   the shorter column is stretched to the height of the taller one, and a grid
   whose rows are all auto shares that spare height out between them - so the
   field with the shorter hint grows its own label row and drops its box below
   the one beside it. That is the 18px the two New Workspace boxes were out by
   (issue #813), and it moves with whatever the hints happen to say. The rows
   stay the size of their content; the spare height collects at the bottom of
   the field, where nothing is looking.

   Written against .form-grid rather than .field: sharing a row with a taller
   field is the only thing that stretches one, and a bare .field here would be
   this sheet redefining a component class, which is the collision the component
   guard exists to catch. */
.form-grid .field { align-content: start; }


/* ---------- Tile grid (admin dashboard + tools home) ---------- */


/* Keep a token + its trailing word together (e.g. ".app files") across line wraps. */

/* ---------- Accessibility helpers (M21) ---------- */

/* Text for screen readers only, where a purely visual treatment carries
   meaning a sighted user gets for free - e.g. the struck-through rows in the
   generated-folder preview. Clipped rather than hidden, so it is still in the
   accessibility tree. */

.u-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ---------- Generated-folder preview: rows the ZIP will not contain ---------- */

/* A file the generator will skip stays in the tree, so the "Include example
   files" toggle in the preview card head visibly costs something rather than
   the tree quietly shrinking. Muted and struck through; the row keeps its
   shape so nothing reflows when the toggle flips.

   Global rather than scoped to FolderTreePreview because the row icon is an
   <svg> the Icon component renders, which a scoped rule cannot reach; it also
   has to land after pages-forms.css to beat .tree__row--file > .tree__ico.

   --ink-3, not --ink-4: tokens.css marks ink-4 as large-text-only (3.9:1) and
   these rows are small mono text. */

.tree__row--excluded .tree__name,
.tree__row--excluded .tree__ico {
    color: var(--ink-3);
}

.tree__row--excluded .tree__name {
    text-decoration: line-through;
}

/* The legend key for that treatment, shown only while something is struck
   through. The sample carries the strike, so the key shows the thing it
   names rather than describing it. */

.tree__key--excluded svg {
    color: var(--ink-3);
}

.tree__key-sample {
    margin-right: var(--space-1);
    font-family: var(--font-mono);
    color: var(--ink-3);
    text-decoration: line-through;
}

/* ---------- Live preview card head (New Workspace / New Extension) ---------- */

/* The example-files switch sits beside the Live badge in the preview card
   head, so the option is next to the tree it changes. Both pages use it. */

.preview__head-tools {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex: none;
}

.preview__head-tools .switch {
    font-size: var(--text-xs);
    white-space: nowrap;
}

/* ── Classes handed to a shared component's root (Alert Class="...") ──────────
   These would normally live in the page's own scoped stylesheet, and cannot: a
   scoped rule only reaches elements the page itself renders, and the element
   that wears one of these classes is rendered by the component. ::deep does not
   help where the component is the page's outermost element, or sits in a
   frame's slot with no page-rendered ancestor. Each name is page-prefixed, so
   being global costs nothing. */

/* Frame, tint and icon slot are .alert .alert--warn from components.css; this
   only turns it into a full-width strip above the shell — square corners, no
   side border, and never squeezed by the flex column it sits in. */
.site-banner {
    flex: none;
    border-width: 0 0 1px 0;
    border-radius: 0;
    align-items: center;
    padding: var(--space-2) var(--space-5);
}

/* Layout only. The frame, tint and icon slot are .alert .alert--info; the bar
   is .progress, which this component is the reason exists (#586). What is left
   is that .alert is a flex row (icon | text) and this banner has no icon, so
   the body has to claim the full width itself. */

.oe-import-progress:not([hidden]) { display: flex; }

/* What the last "Refresh from repositories" run did, and where a learned pair
   came from - the repository, linked to the file itself on GitHub. */
.tr-mem-refresh { margin-bottom: 14px; }

.tr-mem-load-error { margin-bottom: 14px; }

.env-detail__overlap {
    margin-top: var(--space-4);
}

/* Alerts inside the settings card sit inside its padding, above the setting list. */
.env-detail__warn {
    margin: var(--space-4) var(--space-4) 0;
}

/* An upgrade's own page (#984) hands DetailPage the page class "upd". Its sheet
   (UpgradeBody.dc.html) lets the head wrap - the name, then the ribbon under it on a
   narrow window rather than squeezing the name to a word a line. The .detail-head, its
   title block and the .page-head__actions box are DetailPage's own elements, so the
   page's scoped sheet cannot reach them. */
.page.upd .detail-head { flex-wrap: wrap; gap: var(--space-3); }
.page.upd .detail-head > div:first-child { flex: 1 1 260px; min-width: 0; }
.page.upd .detail-head > .page-head__actions { flex-wrap: wrap; min-width: 0; max-width: 100%; }

/* A solution's page (#1077) hands DetailPage the page class "sol". On a phone its head
   held the name, Follow and All solutions on one line and pushed the buttons off
   screen, and its seven tabs ran past the edge with the last ones unreachable. The
   head wraps like the upgrade page's, and the tab row scrolls sideways inside the
   page instead of widening it - a label never breaks onto two lines. An environment's
   page ("env-detail") has five tabs and takes the tab row's rules. */
.page.sol .detail-head { flex-wrap: wrap; gap: var(--space-3); }
.page.sol .detail-head > div:first-child { flex: 1 1 260px; min-width: 0; }
.page.sol .detail-head > .page-head__actions { flex-wrap: wrap; min-width: 0; max-width: 100%; }
.page.sol .settings > * { min-width: 0; }
.page.sol .settings__tabs,
.page.env-detail .settings__tabs { min-width: 0; }
.page.sol .header-tabs,
.page.env-detail .header-tabs { min-width: 0; max-width: 100%; overflow-x: auto; scrollbar-width: thin; }
.page.sol .header-tab,
.page.env-detail .header-tab { flex: none; white-space: nowrap; }
/* A scrolling row clips to inside its own border, so the active tab's underline sits
   on the border's inner edge rather than over it. */
.page.sol .header-tab::after,
.page.env-detail .header-tab::after { bottom: 0; }
.page.sol .header-tab:focus-visible,
.page.env-detail .header-tab:focus-visible { outline-offset: -2px; }

/* The modal scrim in the dark theme. The hand-off's recipe mixes --ink into
   transparent, which dims a light page and does the opposite to a dark one: --ink is
   near-white there, so the page behind a dialog went pale grey and the dialog lost
   the contrast it was opened for. In dark the scrim is mixed from --bg, the darkest
   surface the theme has, so it darkens like the light one does. Both of tokens.css's
   dark selectors, because either can be what put the page in dark. To go upstream
   to the design project with the next hand-off; this is the stopgap, in our own sheet.
   Reported upstream in the design project's briefs/2026-09-shipped-without-a-sheet.md;
   delete this once components.css is fixed there and re-pulled. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .modal-backdrop { background: color-mix(in oklab, var(--bg) 72%, transparent); }
}
[data-theme="dark"] .modal-backdrop { background: color-mix(in oklab, var(--bg) 72%, transparent); }

/* The Customer tab's short hand-kept lists (contacts, colleagues, integrations): one
   row each, the name first, what to click second, the two actions last. Shared by the
   sections under Components/Pages/Projects/Customer, so it lives here rather than in
   three scoped sheets. See .design/solution-customer-info.md. */
.cust-list { list-style: none; margin: 0; padding: 0; }
.cust-list__row { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.5fr) auto; align-items: center; gap: var(--space-4); padding: var(--space-3) var(--space-4); border-top: 1px solid var(--border); }
.cust-list__main, .cust-list__detail { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); min-width: 0; overflow-wrap: anywhere; }
.cust-list__name { font-weight: var(--fw-semibold); color: var(--ink); }
.cust-list__version { font-family: var(--font-mono); font-size: var(--text-sm); }
.cust-list__actions { display: flex; gap: var(--space-1); }
@container (max-width: 720px) { .cust-list__row { grid-template-columns: minmax(0, 1fr); } }
.cust-notes { display: grid; gap: var(--space-4); }
.cust-notes__editor { display: grid; gap: var(--space-4); }
.cust-notes__label { margin: 0 0 var(--space-1); font-size: var(--text-xs); font-weight: var(--fw-semibold); letter-spacing: .04em; text-transform: uppercase; color: var(--ink-3); }
.cust-notes__text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }

/* A table row someone was sent to by address - the search box lands on a person's row
   of the Users tab as /admin/administration/users?user=12. Marked with the system's own
   selection keyline, without the fill: the fill is what a ticked row wears, and this
   row is not ticked. The page focuses the row to bring it into view; the keyline is the
   mark, so the browser's focus outline on a whole table row is not drawn as well. See
   .design/command-palette.md, "Sources". */
.data-table tbody tr.is-target { scroll-margin-top: var(--space-7); }
.data-table tbody tr.is-target:focus { outline: none; }
.data-table tbody tr.is-target td:first-child { box-shadow: inset 2px 0 0 var(--primary); }

/* The run monitors' heads wrap as the release pipeline's sheet draws it
   (ReleasePipelineBody.dc.html: flex-wrap on the head and the actions, the title
   block 300px before it wraps), so on a phone the actions drop under the title
   instead of squeezing it to one word a line (#929) or running off the right edge
   (#978). The actions box also has to be allowed to shrink: components.css gives it
   `flex: none`, and a box that cannot shrink below one line of buttons never wraps
   them, however much room the head gives it. Here and not in a scoped sheet because
   DetailPage renders the head, and a page's ::deep can only reach into markup under
   an element the page renders itself; on the page, not the frame, because
   DetailPage is shared. */
.rp-page .detail-head,
.pb-page .detail-head { flex-wrap: wrap; }
.rp-page .detail-head > div:first-child,
.pb-page .detail-head > div:first-child { min-width: 0; flex: 1 1 300px; }
.rp-page .detail-head__title-row,
.pb-page .detail-head__title-row { flex-wrap: wrap; }
.rp-page .page-head__actions,
.pb-page .page-head__actions { flex: 0 1 auto; flex-wrap: wrap; }

/* The cookbook suggestion review's head holds four actions while rejecting
   (reason, Cancel, Confirm reject, Approve), too wide for a narrow window; a
   head that cannot wrap widens the page and clips Approve off screen. */
.suggestion-review .page-head { flex-wrap: wrap; }
.suggestion-review .page-head > div:first-child { min-width: 0; flex: 1 1 300px; }
.suggestion-review .page-head__actions { flex: 0 1 auto; flex-wrap: wrap; min-width: 0; }

/* A solution's short name after its name, quieter: it is how colleagues say the
   customer aloud, and a second way to spot the row. Shared by every list that names a
   solution - the Solutions list and Upgrades so far (#966) - so they read alike. */
.sol-list__short { margin-left: var(--space-2); color: var(--ink-3); font-size: var(--text-sm); }
