﻿/* Schibsted Grotesk, as on the marketing site (www.youfixit.ai, owner 2026-10-11), served from
   lib/schibsted-grotesk (App.razor). Bootstrap's variable is set too, so its own components follow. */
:root {
    --yf-font: "Schibsted Grotesk", "Segoe UI", system-ui, -apple-system, sans-serif;
    --bs-body-font-family: var(--yf-font);
    --bs-font-sans-serif: var(--yf-font);
}

html, body {
    font-family: var(--yf-font);
    font-variant-numeric: lining-nums;
}

a, .btn-link {
    color: var(--brand-blue-fg);
}

.btn-primary {
    color: var(--yf-on-color);
    background-color: var(--brand-blue);
    border-color: var(--brand-blue);
}

/* Bootstrap's own buttons in the scheme's colors (owner, 2026-10-07), so a btn-danger or
   btn-secondary matches the app's own red and plain buttons and follows the color editor,
   instead of Bootstrap's fixed #dc3545 and #6c757d. Hover, press and disabled stay the same
   colors; hover is a brightness step, as on the app's own buttons. */
.btn-primary, .btn-danger, .btn-secondary, .btn-outline-secondary {
    --bs-btn-disabled-opacity: .5;
}

.btn-primary:hover:not(:disabled), .btn-danger:hover:not(:disabled), .btn-secondary:hover:not(:disabled) {
    filter: brightness(1.1);
}

.btn-primary {
    --bs-btn-hover-bg: var(--brand-blue);
    --bs-btn-hover-border-color: var(--brand-blue);
    --bs-btn-hover-color: var(--yf-on-color);
    --bs-btn-active-bg: var(--brand-blue);
    --bs-btn-active-border-color: var(--brand-blue);
    --bs-btn-active-color: var(--yf-on-color);
    --bs-btn-disabled-bg: var(--brand-blue);
    --bs-btn-disabled-border-color: var(--brand-blue);
    --bs-btn-disabled-color: var(--yf-on-color);
}

.btn-danger {
    --bs-btn-color: var(--yf-on-color);
    --bs-btn-bg: var(--yf-danger);
    --bs-btn-border-color: var(--yf-danger);
    --bs-btn-hover-color: var(--yf-on-color);
    --bs-btn-hover-bg: var(--yf-danger);
    --bs-btn-hover-border-color: var(--yf-danger);
    --bs-btn-active-color: var(--yf-on-color);
    --bs-btn-active-bg: var(--yf-danger);
    --bs-btn-active-border-color: var(--yf-danger);
    --bs-btn-disabled-color: var(--yf-on-color);
    --bs-btn-disabled-bg: var(--yf-danger);
    --bs-btn-disabled-border-color: var(--yf-danger);
}

/* The app's plain button: page background, body text, a border line. */
.btn-secondary, .btn-outline-secondary {
    --bs-btn-color: var(--bs-body-color);
    --bs-btn-bg: var(--bs-body-bg);
    --bs-btn-border-color: var(--yf-border);
    --bs-btn-hover-color: var(--bs-body-color);
    --bs-btn-hover-bg: var(--yf-border);
    --bs-btn-hover-border-color: var(--yf-muted);
    --bs-btn-active-color: var(--bs-body-color);
    --bs-btn-active-bg: var(--yf-border);
    --bs-btn-active-border-color: var(--yf-muted);
    --bs-btn-disabled-color: var(--bs-body-color);
    --bs-btn-disabled-bg: var(--bs-body-bg);
    --bs-btn-disabled-border-color: var(--yf-border);
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
  box-shadow: 0 0 0 0.1rem var(--yf-on-color), 0 0 0 0.25rem var(--brand-blue);
}

.content {
    padding-top: .75rem;
}

/* ---- Density pass: smaller type, tighter spacing ------------------------------------------
   Nearly every size in this app is in rem, so the root size below scales all text and most
   spacing at once. The rest trims Bootstrap's roomy defaults. */
html { font-size: 15px; }
/* Phones 18px (owner, 2026-10-07; was 14px): easier to read on the counter. */
@media (max-width: 600px) { html { font-size: 18px; } }

body { line-height: 1.4; }

h1, .h1 { font-size: 1.6rem; }
h2, .h2 { font-size: 1.35rem; }
h3, .h3 { font-size: 1.15rem; }
h4, .h4 { font-size: 1.05rem; }
h1, h2, h3, h4, h5, h6 { margin-bottom: .5rem; }
p { margin-bottom: .6rem; }

.table { --bs-table-cell-padding-y: .35rem; --bs-table-cell-padding-x: .4rem; }
.btn { --bs-btn-padding-y: .3rem; --bs-btn-padding-x: .65rem; }
.form-control, .form-select { padding-top: .3rem; padding-bottom: .3rem; }

/* Dropdowns: the box and the list that opens from it, in their own color (Dropdown background,
   --yf-dropdown-bg in themes.css; owner, 2026-10-05). Ships the same as the page background.
   The open list is drawn by the browser: Chrome and Edge on Windows and Firefox take the
   option colors, Safari keeps its own. */
select, .form-select { background-color: var(--yf-dropdown-bg); color: var(--bs-body-color); }
select option, select optgroup { background-color: var(--yf-dropdown-bg); color: var(--bs-body-color); }
.card { --bs-card-spacer-y: .75rem; --bs-card-spacer-x: .75rem; }
.mb-3 { margin-bottom: .75rem !important; }
.mb-4 { margin-bottom: 1rem !important; }

/* Phones: a data table wider than the screen scrolls itself instead of widening the page.
   Settings tables are excluded -- they restack into blocks (SettingsTable.razor.css). */
@media (max-width: 800px) {
    .shell-content table:not(.settings-grid):not(.ep-form) {
        display: block;
        max-width: 100%;
        overflow-x: auto;
    }
}

h1:focus {
    outline: none;
}

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

.invalid {
    outline: 1px solid var(--yf-danger-fg);
}

.validation-message {
    color: var(--yf-danger-fg);
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: var(--yf-on-color);
}

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

.darker-border-checkbox.form-check-input {
    border-color: var(--yf-muted);
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}
/* Sign in, sign out, and the invite and reset pages. Shared rather than scoped: they are
   the same narrow form three times over. */
.signin {
    max-width: 24rem;
    margin: 3rem auto;
}

.signin h1 {
    font-size: 1.5rem;
    margin-bottom: 1rem;
}

.signin label {
    display: block;
    margin-top: 1rem;
    font-weight: 600;
}

.signin input {
    display: block;
    width: 100%;
    margin-top: 0.25rem;
    padding: 0.5rem;
    font-size: 1rem;
    border: 1px solid var(--yf-border);
    border-radius: 0.25rem;
}

.signin button {
    margin-top: 1.5rem;
    padding: 0.5rem 1.25rem;
    font-size: 1rem;
    border: 0;
    border-radius: 0.25rem;
    background-color: var(--brand-blue);
    color: var(--yf-on-color);
    cursor: pointer;
}

/* "Sign in with Google", to Google's branding guidelines rather than to taste.
   Light theme: #FFFFFF background, 1px #747775 border inside, #1F1F1F text at 14/20, and
   12px / 10px / 12px of padding before the logo, after the logo and after the text. Google Sans
   is not a public font, so the stack falls back through Roboto to the system sans, which is what
   every other implementation does.

   Hover is an elevation rather than a colour change, deliberately: the guidelines ask that the
   button's colours and the mark are not altered, and a shadow says "pressable" without touching
   either. */
.gsi-button {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    box-sizing: border-box;
    width: 100%;
    min-height: 40px;
    padding: 0 12px;
    border: 1px solid var(--yf-muted);
    border-radius: 4px;
    background-color: var(--bs-body-bg);
    color: var(--bs-body-color);
    font-family: "Google Sans", Roboto, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
    font-size: 14px;
    font-weight: 500;
    line-height: 20px;
    text-decoration: none;
    cursor: pointer;
    -webkit-font-smoothing: antialiased;
}

    .gsi-button:hover {
        box-shadow: 0 1px 2px var(--yf-shadow), 0 1px 3px 1px var(--yf-shadow);
        text-decoration: none;
        color: var(--bs-body-color);
    }

    .gsi-button:focus-visible {
        outline: 2px solid var(--brand-blue);
        outline-offset: 2px;
    }

/* The mark keeps its own 18px box whatever the text does around it: the guidelines are explicit
   that it is not resized or recoloured on its own. */
.gsi-icon {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
}

/* "or", with a rule either side, between the password form and the Google button. */
.signin-or {
    display: flex;
    align-items: center;
    gap: .75rem;
    margin: 1.5rem 0 1rem;
    color: var(--yf-muted);
    font-size: .8rem;
}

    .signin-or::before,
    .signin-or::after {
        content: "";
        flex: 1 1 auto;
        border-top: 1px solid var(--yf-border);
    }

.signin-error {
    padding: 0.6rem 0.75rem;
    border-radius: 0.25rem;
    background-color: var(--bs-body-bg);
    color: var(--yf-danger-fg);
}

.signin .validation-message {
    display: block;
    margin-top: 0.25rem;
    color: var(--yf-danger-fg);
    font-size: 0.875rem;
}

.signin-help {
    margin-top: 1.5rem;
    font-size: 0.875rem;
}

/* A line of explanation attached to the control above it, rather than a link at the foot of the
   page like .signin-help. Sits close on purpose: it is about the button it follows.

   Not .muted, which these pages use in a few places and which is defined nowhere they can see
   it -- the only rule for it is inside Diagnose.razor.css, and scoped CSS does not leave its own
   component. Every .muted outside Diagnose renders at full strength today. */
.signin-note {
    margin-top: 0.5rem;
    font-size: 0.875rem;
    color: var(--yf-muted);
}

.nav-signed-in {
    margin-top: 1.5rem;
    border-top: 1px solid var(--yf-on-color-soft);
    padding-top: 0.5rem;
}

.nav-who {
    display: block;
    padding: 0 1rem;
    font-size: 0.8rem;
    color: var(--yf-on-color);
}

/* The technicians page. */
.techs {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 2.5rem;
}

.techs th,
.techs td {
    text-align: left;
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--yf-border);
    vertical-align: middle;
}

.techs th {
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--yf-muted);
}

.techs tr.inactive td {
    color: var(--yf-muted);
}

.techs .actions {
    white-space: nowrap;
    text-align: right;
}

.techs .actions button {
    margin-left: 0.5rem;
    padding: 0.25rem 0.6rem;
    font-size: 0.85rem;
    border: 1px solid var(--yf-border);
    border-radius: 0.25rem;
    background-color: var(--bs-body-bg);
    cursor: pointer;
}

.techs .actions button:disabled {
    opacity: 0.5;
    cursor: default;
}

.tag {
    display: inline-block;
    padding: 0.1rem 0.5rem;
    border-radius: 1rem;
    font-size: 0.75rem;
}

.tag-on {
    background-color: var(--bs-body-bg);
    color: var(--yf-success-fg);
}

.tag-off {
    background-color: var(--bs-body-bg);
    color: var(--yf-muted);
}

.tag-wait {
    background-color: var(--bs-body-bg);
    color: var(--yf-danger-fg);
}

.add-tech {
    max-width: 28rem;
}

.add-tech label {
    display: block;
    margin-top: 0.75rem;
    font-weight: 600;
}

.add-tech input,
.add-tech select {
    display: block;
    width: 100%;
    margin-top: 0.25rem;
    padding: 0.4rem;
    font-size: 1rem;
    border: 1px solid var(--yf-border);
    border-radius: 0.25rem;
}

/* A form whose labels sit on the same line as their boxes (the customer pages): label text in
   one column, the box in the next, so every box on the page lines up. A checkbox row keeps its
   own layout. Stacks again on a phone. */
.add-tech.inline-fields {
    max-width: 40rem;
}

.add-tech.inline-fields label:not(.check-row) {
    display: grid;
    grid-template-columns: 12rem minmax(0, 1fr);
    align-items: center;
    gap: 1rem;
}

.add-tech.inline-fields label:not(.check-row) input,
.add-tech.inline-fields label:not(.check-row) select {
    margin-top: 0;
}

.add-tech.inline-fields .check-row,
.add-tech.inline-fields > p {
    margin-left: 13rem;
}

@media (max-width: 600px) {
    .add-tech.inline-fields label:not(.check-row) {
        grid-template-columns: minmax(0, 1fr);
        gap: .25rem;
    }

    .add-tech.inline-fields .check-row,
    .add-tech.inline-fields > p {
        margin-left: 0;
    }
}

.add-tech button {
    margin-top: 1.25rem;
    padding: 0.5rem 1.25rem;
    font-size: 1rem;
    border: 0;
    border-radius: 0.25rem;
    background-color: var(--brand-blue);
    color: var(--yf-on-color);
    cursor: pointer;
}

.add-tech button:disabled {
    opacity: 0.6;
    cursor: default;
}

.notice {
    padding: 0.6rem 0.75rem;
    border-radius: 0.25rem;
    margin-bottom: 1.25rem;
}

.notice-ok {
    background-color: var(--bs-body-bg);
    color: var(--yf-success-fg);
}

.notice-error {
    background-color: var(--bs-body-bg);
    color: var(--yf-danger-fg);
}

.techs .actions .edit-link {
    margin-left: 0.5rem;
    font-size: 0.85rem;
}

.cancel-link {
    margin-left: 1rem;
    font-size: 0.9rem;
}

.danger {
    max-width: 34rem;
    margin-top: 1rem;
    padding: 1rem 1.25rem;
    border: 1px solid var(--yf-border);
    border-radius: 0.25rem;
    background-color: var(--bs-body-bg);
}

.danger p {
    margin-bottom: 0.75rem;
}

.danger hr {
    margin: 1.25rem 0;
    border: 0;
    border-top: 1px solid var(--yf-border);
}

.danger button {
    padding: 0.4rem 0.9rem;
    font-size: 0.9rem;
    border: 1px solid var(--yf-border);
    border-radius: 0.25rem;
    background-color: var(--bs-body-bg);
    cursor: pointer;
    margin-right: 0.5rem;
}

.danger button:disabled {
    opacity: 0.5;
    cursor: default;
}

.danger .danger-button {
    border-color: var(--yf-danger);
    background-color: var(--yf-danger);
    color: var(--yf-on-color);
}

/* --------------------------------------------------------------------------
   Waiting indicator: three dots bouncing in order.

   Global rather than scoped to Diagnose, for two reasons. It is a generic "still
   working" widget that any page with a wait will want, and Blazor's CSS isolation
   rewriter has no business anywhere near a @keyframes block: it appends a scope
   attribute to selectors, and keyframe stops are not selectors.

   currentColor throughout, so it takes the colour of whatever it sits inside: the
   state pill in the header, the muted grey on the working line, without either
   being restated here.
   -------------------------------------------------------------------------- */
.dots {
    display: inline-flex;
    align-items: flex-end;
    gap: 0.2em;
    margin-left: 0.35em;
}

.dots span {
    width: 0.32em;
    height: 0.32em;
    border-radius: 50%;
    background: currentColor;
    animation: yfi-dot-bounce 1.05s infinite ease-in-out both;

    /* Promoted to its own compositor layer, which is what keeps these moving while a turn
       streams. transform and opacity on an unpromoted element are animated by the main thread,
       and the main thread is busy typing the reply in -- so the dots froze exactly when they
       were most needed, which is the one thing they exist to rule out. On a layer of their own
       they run on the compositor and are unaffected by however busy the page is. Three dots is
       a cheap layer. */
    will-change: transform, opacity;
}

/* In order, not together. The stagger is the whole effect. */
.dots span:nth-child(2) { animation-delay: 0.15s; }
.dots span:nth-child(3) { animation-delay: 0.3s; }

@keyframes yfi-dot-bounce {
    0%, 70%, 100% { transform: translateY(0);        opacity: 0.4; }
    35%           { transform: translateY(-0.45em);  opacity: 1; }
}

/* The dots bounce even when the system asks for reduced motion (owner, 2026-10-02). Windows
   asks for it whenever Animation effects is off, and over Remote Desktop, and still dots there
   read as a hung page -- the one thing they exist to rule out. Small, slow and a short distance,
   so they are not the kind of motion the setting is for. */

/* ==========================================================================
   The shell: a bar across the top, then menu / content / handle / panel.

   Global rather than scoped to MainLayout, because the script that drives it sets classes on
   <html> and scoped CSS cannot reach those, and because the right panel's visibility is
   decided with :has(), which the CSS isolation rewriter does not survive predictably.
   ========================================================================== */

:root {
    --nav-w: 250px;
    --right-w: 340px;

    /* The brand colors (--brand-blue, --brand-blue-fg, --brand-orange) are in themes.css with every other color, one value per scheme,
       and the operator can change them on Server Settings > Appearance. */
}

/* height, not min-height: the shell fills exactly one viewport and never grows past it, so the
   document itself never scrolls. That is what lets shell-content and shell-right each get their
   own scrollbar below (overflow-y: auto) instead of one long page-level scroll that drags the
   customer/machine panel out of view every time the chat gets long. */
.shell {
    display: flex;
    flex-direction: column;
    height: 100vh;
}

.shell-bar {
    display: flex;
    align-items: center;
    gap: .5rem;
    height: 3rem;
    padding: 0 .75rem;
    background: var(--brand-blue);
    color: var(--yf-on-color);
    position: sticky;
    top: 0;
    z-index: 3;
}

.shell-brand {
    color: var(--yf-on-color);
    font-size: 1.1rem;
    text-decoration: none;
}

/* The shop name (ShopName.razor). Positioned over the main panel's left edge plus the same
   1.5rem the content is padded by, so it lines up with the page heading underneath it rather
   than trailing the brand. The left offset is the menu column (--nav-w, which the resize handle
   drives) plus the 6px handle, so dragging the menu moves the name with the panel.

   With no menu column to the left -- collapsed, or the narrow-screen overlay -- the panel
   starts at the window edge, where the burger and brand already are, so it drops back into the
   flow after the brand instead. */
.shell-shop {
    position: absolute;
    left: calc(var(--nav-w) + 6px + 1.5rem);
    max-width: 40vw;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--yf-on-color);
}

/* The Diagnose page's session strip (.bar-session, pinned over the bar) carries its own copy of
   the shop name as its first item, so the strip's machine name, state and buttons sit after it
   in one row and can never overlap it. The strip's background covers the bar's copy. */
.shell .bar-session .shell-shop {
    position: static;
    margin: 0 .5rem 0 0;
}

html.nav-collapsed .shell-shop {
    position: static;
    margin-left: 1rem;
}

@media (max-width: 800px) {
    .shell-shop {
        position: static;
        margin-left: 1rem;
    }
}

.shell-spacer {
    flex: 1;
}

.shell-body {
    display: flex;
    flex: 1;
    min-height: 0;
}

.shell-nav {
    flex: 0 0 var(--nav-w);
    width: var(--nav-w);
    overflow: hidden;
    /* The menu's own colors (themes.css, Menu group; owner, 2026-10-11): light on Light and dark
       on Dark, where it was brand blue in both. The top bar above it stays brand blue. The line
       down the right edge is what separates a light menu from a white page. */
    background: var(--yf-menu-bg);
    color: var(--yf-menu-fg);
    box-shadow: inset -1px 0 0 var(--yf-menu-line);
    transition: flex-basis .15s ease, width .15s ease;
}

html.nav-collapsed .shell-nav {
    flex-basis: 0;
    width: 0;
}

/* The menu's contents track the column's width rather than filling it.

   Not width: 100%, and not left to reflow. The column animates to zero when it collapses, and
   a percentage would squeeze the links through every width on the way, which reads as the menu
   breaking rather than closing. --nav-w holds still through that -- nav-collapsed sets the
   column's own width, not the variable -- so the contents keep their shape and slide out.

   Here rather than in NavMenu.razor.css, which is where it used to be as a flat 250px. That
   file is scoped, so its rules carry an extra attribute and its <link> comes after this one:
   anything it says about the column's width wins over what the column actually is, and the
   resize handle drags a column whose contents stay whatever that file last said. Geometry the
   handle drives belongs in one place, with the variable it drives. */
.shell-nav .nav-scrollable {
    width: var(--nav-w);
}

/* min-width: 0 is load-bearing on every flex child here. Without it a long unbroken line in a
   transcript, or a wide JSON blob, sets the column's minimum to its content width and pushes
   the panel off screen instead of scrolling inside it. */
.shell-content {
    flex: 1 1 auto;
    min-width: 0;
    /* Its own scrollbar now that .shell is capped to one viewport tall: the middle panel
       scrolls independently of the right panel (shell-right, overflow: auto below) instead of
       both riding one page-level scroll together. */
    overflow-y: auto;
    padding: 1rem 1.5rem 2rem;
}

/* The Diagnose page, with its dock pinned to the bottom (js/diagnose.js sets the class): half
   the bottom padding, so less empty space under the compose box (owner, 2026-10-06). The
   dock's blue reaches down through it (Diagnose.razor.css, .dock::before). */
.shell-content.has-dock {
    /* None (owner, 2026-10-07): the dock sits on the bottom edge, 5px of blue under the box. */
    padding-bottom: 0;
}

/* 6px of target, and the cursor is the affordance: col-resize is the left-right arrows. */
.shell-resizer {
    flex: 0 0 6px;
    cursor: col-resize;
    background: var(--bs-body-bg);
}

.shell-resizer:hover,
.shell-resizer:focus-visible {
    background: var(--brand-blue);
    outline: none;
}

.shell-right {
    position: relative;
    flex: 0 0 var(--right-w);
    width: var(--right-w);
    min-width: 0;
    /* Was already here, but only starts scrolling anything now that .shell is capped to one
       viewport tall above — before that, the panel just grew as long as its content did. */
    overflow: auto;
    border-left: 1px solid var(--yf-border);
    background: var(--bs-body-bg);
}

.right-body {
    padding: .9rem;
}

.right-close {
    position: absolute;
    top: .4rem;
    right: .4rem;
    color: var(--yf-muted);
}

/* The panel exists only when a page put something in it. :has() asks the DOM that question
   directly, which is what lets the layout stay static: there is no state to hold and nothing
   to tell it. */
.shell:not(:has(.right-body > *)) .shell-resizer-right,
.shell:not(:has(.right-body > *)) .shell-right,
.shell:not(:has(.right-body > *)) .js-right-toggle {
    display: none;
}

html.right-closed .shell-resizer-right,
html.right-closed .shell-right {
    display: none;
}

/* Each handle goes with the column it resizes. A collapsed menu has no width to drag, and a
   handle left behind sits between the content and nothing. */
html.nav-collapsed .shell-resizer-nav {
    display: none;
}

.icon-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    padding: 0;
    border: none;
    border-radius: .25rem;
    background: none;
    color: inherit;
    cursor: pointer;
}

.icon-button:hover {
    background: var(--yf-on-color-soft);
}

.right-close:hover {
    background: var(--bs-body-bg);
}

.icon-button svg {
    width: 1.15rem;
    height: 1.15rem;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Set on <body> for the duration of a drag. Without it the cursor reverts the moment the
   pointer leaves the 6px handle, which it does instantly, and the drag selects text across the
   whole page instead of resizing. */
body.resizing {
    cursor: col-resize;
    user-select: none;
}

/* The menu animates its width so collapsing it slides rather than jumps. During a drag that
   same transition makes the column trail the pointer by .15s, which reads as the handle being
   sticky. Off for the duration; the collapse animation is unaffected. */
body.resizing .shell-nav {
    transition: none;
}

/* Narrow screens: the menu overlays rather than taking a column, so collapsing it is the
   difference between a usable page and an unusable one rather than a preference. */
@media (max-width: 800px) {
    .shell-nav {
        position: fixed;
        top: 3rem;
        bottom: 0;
        left: 0;
        z-index: 10;
        flex-basis: 0;
        box-shadow: 2px 0 8px var(--yf-shadow);
    }

    html.nav-collapsed .shell-nav {
        width: 0;
        box-shadow: none;
    }

    /* The menu is fixed and floats over the content here, so nothing sits between the two for
       a handle to be the boundary of. */
    .shell-resizer-nav {
        display: none;
    }
}

/* Left-panel search (NavMenu + js/search.js). Global, not NavMenu.razor.css: the result rows
   are created by script, so they never carry Blazor's CSS-isolation attribute. */
.nav-search input { width: 100%; padding: .3rem .5rem; border-radius: .25rem; border: 1px solid var(--yf-muted); }
.nav-search-results { list-style: none; margin: .25rem 0 0; padding: 0; max-height: 50vh; overflow-y: auto; overflow-x: hidden; }
.nav-search-results li a { display: flex; align-items: center; flex-wrap: nowrap; gap: .4rem; padding: .25rem .4rem; color: var(--yf-on-color); text-decoration: none; font-size: .85rem; border-radius: .25rem; }
.nav-search-results li a:hover, .nav-search-results li a:focus { background: var(--yf-on-color-soft); }

/* One row, one line. A wrapped hostname pushes every row below it down and turns a ten-row
   dropdown into a wall, and it wraps at a width the menu may not even be at any more now that
   the menu is resizable: widening it should buy more of the name, not the same two lines with
   slack on the right. So the label takes whatever width is left and ellipsises past it, and
   the state badge keeps its own (flex-shrink: 0, below) at the end of the row.

   min-width: 0 is what makes that work at all. A flex item's floor is its content width by
   default, so without this the longest hostname sets the row's width, the row refuses to
   shrink to the column, and it overflows sideways instead of ellipsising. */
.nav-search-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-search-results .nav-search-empty { color: var(--yf-on-color); font-size: .8rem; padding: .25rem .4rem; }
.conn-mini { font-size: .65rem; line-height: 1; padding: .15rem .35rem; border-radius: .6rem; color: var(--yf-on-color); flex-shrink: 0; }
/* In the blue left menu, so a translucent white pill rather than Brand blue, which vanished (owner, 2026-10-07). */
.conn-in { background: var(--yf-on-color-soft); box-shadow: inset 0 0 0 1px var(--yf-on-color); }
.conn-avail { background: var(--yf-success); }
.conn-off { background: var(--yf-muted); }

/* Signed-in name + Sign out, in the blue shell bar (MainLayout). */
.bar-user { display: inline-flex; align-items: center; gap: .75rem; font-size: .85rem; color: var(--yf-on-color); margin-right: .5rem; }
.bar-user a { color: var(--yf-on-color); text-decoration: underline; }
.bar-signout { display: inline; margin: 0; }
.bar-signout button { padding: 0; border: 0; background: none; color: var(--yf-on-color); font: inherit; text-decoration: underline; cursor: pointer; }
.bar-user-name { font-weight: 600; }
.nav-search { margin-top: .5rem; }

/* A button that reads and behaves as an inline link. For an action that is styled like the
   link it replaces but must NOT navigate -- Blazor's enhanced-navigation click listener runs
   in the capture phase on document, before any @onclick on the element, so an <a href> cannot
   be stopped from navigating by @onclick:preventDefault. A <button type="button"> is never
   intercepted, and this makes it look like the link it stands in for. */
.link-button {
    padding: 0;
    border: 0;
    background: none;
    color: var(--brand-blue-fg);
    font: inherit;
    font-size: 0.9rem;
    text-decoration: underline;
    cursor: pointer;
}

.link-button:hover {
    color: var(--brand-blue-fg);
}

/* A secondary action sitting beside a value in a spec sheet -- Reassign next to the customer
   name. Without this it inherits nothing: .edit-link is styled under .techs and .detail-head
   only, so in a definition list the two anchors rendered butted together as one word. */
.detail dd .edit-link {
    margin-left: 0.5rem;
    font-size: 0.85rem;
}

/* The app's data table (class="work"): the machine list, and the tables on the machine page and
   its panels (Products, PCScan, DailyDelta, Access), Diagnose and the technician page. Global,
   not MachineTable.razor.css: scoped CSS reached only MachineTable, so every other "work" table
   had unpadded headers that ran together ("ProductPaid by Since"). The machine list's sticky
   header and row states stay in MachineTable.razor.css. */
.work {
    width: 100%;
    border-collapse: collapse;
}

.work th,
.work td {
    text-align: left;
    padding: 0.4rem 0.75rem;
    border-bottom: 1px solid var(--yf-border);
    vertical-align: middle;
    white-space: nowrap;
}

.work th {
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--yf-muted);
}

/* Customer and machine detail pages. A spec sheet: labels and values aligned in two columns. */
.detail-head {
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
}

.detail-head h1 {
    margin-bottom: 0;
}

.detail-head .edit-link {
    font-size: 0.9rem;
}

/* The link under the heading rather than beside it (the customer page, owner 2026-10-03). */
.detail-head.stacked {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.2rem;
}

.detail {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 0.3rem 1.25rem;
    margin: 1rem 0 1.75rem;
    max-width: 48rem;
}

.detail dt {
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--yf-muted);
    align-self: center;
}

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

/* A detail list that fills its box, so a value runs to the box's right edge beside its label
   (DailyDelta's Last check on the machine page, owner 2026-10-11), and a field that takes the
   whole row under its label (the DailyDelta check page's Summary). */
.detail.detail-full { max-width: none; }
.detail dt.detail-wide { grid-column: 1 / -1; }
.detail dd.detail-wide { grid-column: 1 / -1; }

.add-tech .check-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-weight: 600;
}

.add-tech .check-row input {
    display: inline-block;
    width: auto;
    margin: 0;
}

/* Machine page: spec sheet on the left, Machine Notes pane on the right. */
.machine-layout {
    display: grid;
    grid-template-columns: minmax(0, 48rem) minmax(18rem, 28rem);
    gap: 1.5rem;
    align-items: start;
    margin: 1rem 0 1.75rem;
}

.machine-layout .detail {
    margin: 0;
}

@media (max-width: 64rem) {
    .machine-layout {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* Server Settings hub: one link per section page. */
.settings-links {
    list-style: none;
    padding: 0;
}

.settings-links li {
    padding: 0.4rem 0;
}

/* ---- Model text: the small Markdown subset the transcript renders --------------------------

   Two renderers produce these elements and they must look identical: Components/Markup/RichText
   (Razor, every finished message) and js/diagnose.js (the turn currently streaming, typed in a
   character at a time). Unscoped for that reason -- see RichText.razor.css. */
/* Tables from the model are data, not layout: narrow, ruled, readable at a glance in a
   transcript column that is already competing with the machine panel for width. */
.rt-table-scroll {
  /* A wide table scrolls itself rather than widening the transcript. Without this, one
     six-column table from the model stretches .shell-content and puts a horizontal scrollbar
     under the entire chat — including every message that fits perfectly well. */
  overflow-x: auto;
  margin: .5rem 0;
}

.rt-table {
  border-collapse: collapse;
  font-size: .85rem;
  /* auto, not 100%: a two-column table stretched across the full width separates its own
     columns by an expanse of nothing, which is harder to read than a compact one. */
  width: auto;
  max-width: 100%;
}

.rt-table th,
.rt-table td {
  border: 1px solid var(--yf-border);
  padding: .3rem .55rem;
  vertical-align: top;
}

.rt-table thead th {
  background: var(--bs-body-bg);
  font-weight: 600;
  /* Not sticky. A sticky header here would stick to .shell-content's scrollport, not to the
     table, so it would float over the messages above it once the table scrolled past — which
     looks like a bug rather than a convenience. Transcript tables are short. */
}

.rt-table tbody tr:nth-child(even) td {
  background: var(--bs-body-bg);
}

.rt-left { text-align: left; }
.rt-right { text-align: right; }
.rt-center { text-align: center; }

.rt-list {
  margin: .25rem 0;
  padding-left: 1.25rem;
}

.rt-list li { margin: .15rem 0; }

/* Links the model wrote. Open in a new tab (target=_blank, rel=noopener) so the session page
   stays put. word-break keeps a long address from pushing the transcript sideways. */
.rt-link {
  text-decoration: underline;
  word-break: break-all;
}

.rt-code {
  background: var(--bs-body-bg);
  border-radius: .2rem;
  padding: .05rem .3rem;
  font-size: .85em;
}

/* Fenced blocks. What the model puts in one is a PowerShell command, a registry path or a
   log excerpt -- something a technician is about to read character by character or copy -- so
   nothing here wraps and nothing here is trimmed. A long line scrolls the block sideways
   rather than folding in half, because a folded command looks like two commands. */
.rt-pre {
  background: var(--bs-body-bg);
  border: 1px solid var(--yf-border);
  border-radius: .25rem;
  margin: .5rem 0;
  padding: .45rem .6rem;
  /* Both axes: the width guard above, and a ceiling so one long script cannot push the rest of
     the transcript off the screen. Matches .entry pre, which does the same for tool output. */
  overflow: auto;
  max-height: 24rem;
  font-size: .8rem;
  line-height: 1.45;
  white-space: pre;
}

/* The <code> inside is structural. Left to itself it would pick up .rt-code's chip styling --
   a grey box inside a grey box -- so it is flattened back to the <pre> it sits in. */
.rt-pre code {
  background: none;
  border-radius: 0;
  padding: 0;
  font-size: inherit;
  white-space: inherit;
}

/* The fence's language, when it named one. A corner label rather than a line of the block, so
   it says what is being looked at without displacing the first command. */
.rt-pre[data-lang]::before {
  content: attr(data-lang);
  display: block;
  margin-bottom: .3rem;
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: .65rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--yf-muted);
}

/* The model's own section breaks inside one message. Sized down hard: these sit inside a chat
   entry, and a browser-default h1 next to a 0.9rem paragraph reads as a rendering fault rather
   than as a heading. Only the first two levels differ much -- the model rarely goes deeper, and
   six visibly distinct sizes inside one message would be noise. */
.rt-h {
    font-weight: 600;
    line-height: 1.3;
    margin: .6rem 0 .2rem;
}

.rt-h:first-child { margin-top: 0; }

.rt-h1 { font-size: 1.05rem; }
.rt-h2 { font-size: 1rem; }
.rt-h3 { font-size: .95rem; }
.rt-h4, .rt-h5, .rt-h6 { font-size: .9rem; color: var(--yf-muted); }

/* The streaming turn's own box. RichText's paragraphs are styled by its scoped stylesheet;
   these are the same paragraphs built in script, so they are styled here. */
.live-prose p {
    margin: .25rem 0;
    white-space: pre-wrap;
}

.live-prose > *:first-child { margin-top: 0; }
.live-prose > *:last-child { margin-bottom: 0; }

/* ---- The session state pill (StatePill.razor), which .dots above belongs to ----------------

   Moved out of Diagnose.razor.css when the pill became its own component: a scoped rule cannot
   reach markup that now carries another component's scope attribute.

   Explicit colours rather than inherited: in the dock (white background) inheriting the page's
   default near-black text worked by coincidence, but the same pill also renders in the shell's
   top bar, which sets white text for everything in it. Inherited white on top of these pale
   pill colours made both the label and the dots next to invisible there. */
.state { font-size: .75rem; padding: .15rem .5rem; border-radius: 999px; background: var(--bs-body-bg); color: var(--bs-body-color); }
.state-proposalpending { background: var(--bs-body-bg); }
.state-dispatching, .state-opening { background: var(--bs-body-bg); }

/* The model actually thinking, dots and all, gets its own colour rather than sharing the blue
   used for "something on the wire is in flight" -- it is the state worth a technician's eye the
   most, so it is the one that stands out. */
.state-awaitingmodel { background: var(--yf-success-soft); }
.state-stopping { background: var(--bs-body-bg); color: var(--yf-muted); }

/* The only state pill that is a warning colour: it is the only one the technician cannot act
   their way out of. */
.state-workerlost { background: var(--yf-danger-soft); color: var(--yf-danger-fg); }

/* The menu's bottom section (NavMenu.razor: Tutorial, then Feedback).

   The column is already a flex column -- Bootstrap's .nav.flex-column -- so pinning is one
   auto margin, but only once that column is as tall as the panel it sits in. Hence the two
   rules above it: give the scroll box the full height and let the list fill it. min-height
   rather than height, so a menu longer than the screen still scrolls instead of being
   squashed, and the bottom section simply follows the end of the list. */
.shell-nav .nav-scrollable {
    height: 100%;
    overflow-y: auto;
}

.shell-nav .nav-scrollable > .nav {
    min-height: 100%;
}

.nav-bottom {
    margin-top: auto;
    padding-top: .375rem;
    padding-bottom: .5rem;
    border-top: 1px solid var(--yf-menu-line);
}

/* The screenshot lightbox (wwwroot/js/lightbox.js).

   Unscoped and in this file because the overlay is created by script and appended to <body>:
   a scoped stylesheet's rules carry an attribute that a JS-built element never has, so scoped
   CSS could not reach it. */

img[data-lightbox] {
    cursor: zoom-in;
}

.lightbox {
    position: fixed;
    inset: 0;
    z-index: 2000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2rem;
    /* A little dim, not a blackout: the point is to lift the picture off the page, not to
       pretend the page is gone. */
    background: var(--yf-shadow);
    cursor: zoom-out;
}

.lightbox img {
    max-width: 95vw;
    max-height: 90vh;
    /* Its own size if it fits, scaled down if it does not. A 537px screenshot blown up to fill
       a 4K display would be a wall of soft pixels. */
    width: auto;
    height: auto;
    border-radius: 8px;
    background: var(--bs-body-bg);
    box-shadow: 0 24px 60px var(--yf-shadow);
}

/* --------------------------------------------------------------------------
   The public pages' shell (Layout/MarketingLayout): the bar, the button, the footer.
   The marketing page itself (hero, value props, packages) moved to the
   youfixit-www.youfixit.ai repo on 2026-10-11, with its styles.

   Unscoped, and in this file rather than a .razor.css, because the page and its
   layout share the palette and the button -- scoped CSS would have to say all of
   it twice. Everything is prefixed .mk- so it cannot reach the app shell, which
   a visitor here never sees anyway.
   -------------------------------------------------------------------------- */

.mk {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
}

.mk-bar {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0 1.5rem;
    height: 60px;
    background: var(--brand-blue);
    color: var(--yf-on-color);
}

.mk-brand {
    font-size: 1.35rem;
    font-weight: 600;
    letter-spacing: .02em;
    color: var(--yf-on-color);
    text-decoration: none;
}

.mk-spacer { flex: 1 1 auto; }

.mk-links {
    display: flex;
    align-items: center;
    gap: 1.25rem;
}

.mk-links a {
    color: var(--yf-on-color);
    text-decoration: none;
    opacity: .9;
}

.mk-links a:hover { opacity: 1; text-decoration: underline; }

/* Sign out is a form (it posts), drawn as one more link in the bar. */
.mk-links .mk-signout { display: inline; margin: 0; }
.mk-links .mk-signout button {
    padding: 0;
    border: 0;
    background: none;
    color: var(--yf-on-color);
    font: inherit;
    opacity: .9;
    cursor: pointer;
}
.mk-links .mk-signout button:hover { opacity: 1; text-decoration: underline; }

.mk-links .mk-signin {
    padding: .4rem .9rem;
    border-radius: 6px;
    background: var(--brand-orange);
    opacity: 1;
    color: var(--yf-on-orange);
}

.mk-links .mk-signin:hover {
    background: var(--brand-orange);
    filter: brightness(1.1);
    text-decoration: none;
    color: var(--yf-on-orange);
}

/* ---- Button ----------------------------------------------------------- */

.mk-btn {
    display: inline-block;
    padding: .7rem 1.4rem;
    border-radius: 7px;
    background: var(--brand-orange);
    color: var(--yf-on-orange);
    font-weight: 600;
    text-decoration: none;
    /* A <button> gets the browser's border otherwise; a link does not (owner, 2026-10-08). */
    border: 0;
    cursor: pointer;
}

/* A brightness step on hover (owner, 2026-10-07): it used to be identical to the resting button. */
.mk-btn:hover { background: var(--brand-orange); color: var(--yf-on-orange); filter: brightness(1.1); }
.qr-btn-blue:hover { filter: none; }

/* ---- Footer ------------------------------------------------------------ */

.mk-foot {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    max-width: 1140px;
    width: 100%;
    margin: auto auto 0;
    padding: 1.5rem;
    color: var(--yf-muted);
    font-size: .9rem;
}

.mk-foot a { color: var(--brand-blue-fg); }

/* The "click your download" hint (wwwroot/js/install-hint.js), shown after Install on /pcscan
   and /get. Global rather than scoped: the markup is built by script, which scoped CSS never
   sees. */
.bi-overlay {
    position: fixed;
    inset: 0;
    z-index: 2000;
    display: flex;
    align-items: flex-start;
    justify-content: flex-end;
    padding: 6.5rem 2rem 2rem;
    background: var(--yf-shadow);
}

.bi-edge {
    align-items: center;
    justify-content: center;
    padding: 2rem;
}

.bi-arrow {
    position: fixed;
    top: .25rem;
    right: 6rem;
    width: 96px;
    height: 96px;
    color: var(--yf-on-color);
    animation: bi-nudge 1.1s ease-in-out infinite;
}

@keyframes bi-nudge {
    0%, 100% { transform: translate(0, 0); }
    50%      { transform: translate(8px, -10px); }
}

@media (prefers-reduced-motion: reduce) {
    .bi-arrow { animation: none; }
}

.bi-card {
    position: relative;
    max-width: 24rem;
    padding: 1.5rem 1.75rem 1.25rem;
    border-radius: 12px;
    background: var(--bs-body-bg);
    box-shadow: 0 18px 40px var(--yf-shadow);
    text-align: left;
}

.bi-card h2 {
    margin: 0 0 .75rem;
    font-size: 1.4rem;
    font-weight: 700;
    color: var(--brand-blue-fg);
}

.bi-card ol {
    margin: 0 0 .75rem;
    padding-left: 1.3rem;
    font-size: 1.05rem;
    line-height: 1.8;
}

.bi-help {
    margin: 0;
    font-size: .85rem;
    color: var(--yf-muted);
}

.bi-close {
    position: absolute;
    top: .4rem;
    right: .6rem;
    border: 0;
    background: none;
    font-size: 1.6rem;
    line-height: 1;
    color: var(--yf-muted);
    cursor: pointer;
}

.bi-close:hover { color: var(--bs-body-color); }

.bi-stage {
    margin: 0 0 1rem;
    font-size: 1.05rem;
}

.bi-bar {
    position: relative;
    height: 6px;
    overflow: hidden;
    border-radius: 3px;
    background: var(--bs-body-bg);
}

.bi-bar span {
    position: absolute;
    inset: 0 auto 0 0;
    width: 35%;
    border-radius: 3px;
    background: var(--brand-blue);
    animation: bi-slide 1.2s ease-in-out infinite;
}

@keyframes bi-slide {
    0%   { left: -35%; }
    100% { left: 100%; }
}

@media (prefers-reduced-motion: reduce) {
    .bi-bar span { animation: none; width: 100%; }
}

.bi-detail {
    white-space: pre-wrap;
    margin-bottom: 2rem;
}

/* ---- PCScan results (/results pages, Results v1 section 5). Read on phones as often as on
   the computer that was checked, so one column and nothing wider than the screen. ---- */

.qr {
    max-width: 44rem;
    margin: 0 auto;
    padding: 2.5rem 1rem 3rem;
    line-height: 1.6;
}

.qr h1 {
    margin: 0 0 .75rem;
    font-size: clamp(1.7rem, 4vw, 2.3rem);
    font-weight: 700;
    color: var(--brand-blue-fg);
}

.qr h2 {
    margin: 1.75rem 0 .5rem;
    font-size: 1.25rem;
    color: var(--brand-blue-fg);
}

.qr-card h2 { margin-top: 0; }

.qr-lede { font-size: 1.1rem; }

.qr-small { font-size: .9rem; color: var(--yf-muted); }

.qr-card {
    margin: 1rem 0;
    padding: 1rem 1.25rem;
    border: 1px solid var(--yf-border);
    border-radius: 10px;
    background: var(--bs-body-bg);
}

.qr-form { display: flex; flex-wrap: wrap; gap: .5rem; margin: 1rem 0; }

/* Labelled fields in a column (My account, Delete my data): the labels in one column, the boxes
   lined up in the next; one above the other on a phone. */
.qr-stack { display: flex; flex-direction: column; gap: 1rem; margin: 1rem 0; }
.qr-stack p { margin: 0; }
.qr-stack > button { align-self: flex-start; }
.qr-stack label {
    display: grid;
    grid-template-columns: 10rem minmax(0, 20rem);
    align-items: center;
    gap: .75rem;
    margin: 0;
    font-weight: 600;
}
.qr-stack input[type=password],
.qr-stack input[type=email] {
    width: 100%;
    box-sizing: border-box;
    padding: .6rem .8rem;
    font-size: 1rem;
    font-weight: 400;
    border: 1px solid var(--yf-border);
    border-radius: 8px;
}
@media (max-width: 36rem) {
    .qr-stack label { grid-template-columns: minmax(0, 1fr); gap: .35rem; }
}

/* A little more room between things (My account, Delete my data). */
.qr-roomy .qr-card { margin: 1.5rem 0; padding: 1.5rem 1.5rem 1.25rem; }
.qr-roomy .qr-card h2 { margin-bottom: 1rem; }
.qr-roomy > p, .qr-roomy .qr-card > p { margin: 0 0 1rem; }
.qr-roomy .qr-actions { margin-top: 2.5rem; }
.qr-roomy .qr-stack { margin: 1.5rem 0; }

.qr-form input[type=email] {
    flex: 1 1 16rem;
    min-width: 0;
    padding: .7rem .9rem;
    font-size: 1rem;
    border: 1px solid var(--yf-border);
    border-radius: 8px;
}

.qr-badge {
    display: inline-block;
    padding: .15rem .7rem;
    border-radius: 999px;
    font-size: .9rem;
    font-weight: 600;
}

.qr-good { background: var(--bs-body-bg); color: var(--yf-success-fg); }
.qr-attention { background: var(--bs-body-bg); color: var(--yf-danger-fg); }
.qr-urgent { background: var(--bs-body-bg); color: var(--yf-danger-fg); }

.qr-finding {
    margin: 1rem 0;
    padding: .75rem 1rem;
    border-left: 4px solid var(--yf-border);
    background: var(--bs-body-bg);
    border-radius: 0 8px 8px 0;
}

.qr-finding h3 { margin: 0 0 .25rem; font-size: 1.05rem; }
.qr-finding p { margin: .35rem 0; overflow-wrap: anywhere; }
.qr-sev-High { border-left-color: var(--yf-danger); }
.qr-sev-Medium { border-left-color: var(--brand-orange); }
.qr-sev-Low { border-left-color: var(--brand-blue); }

.qr-list { padding-left: 1.25rem; }

.qr-inline { display: inline; }

.qr-actions { margin-top: 2rem; display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }

/* Your computers (/results): as wide as an ordinary page, still centred, one section per computer. */
.qr-wide { width: 100%; box-sizing: border-box; max-width: 64rem; padding-left: 1.5rem; padding-right: 1.5rem; }

.qr-computer {
    margin: 1.5rem 0;
    padding: 1.25rem 1.5rem 1.5rem;
    border: 1px solid var(--yf-border);
    border-radius: 12px;
    background: var(--bs-body-bg);
}

.qr-computer-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .4rem .9rem;
    margin-bottom: .75rem;
}

.qr-computer-head h2 { margin: 0; font-size: 1.7rem; font-weight: 700; }

/* Being scanned now (/results): a pulsing dot, dots that count up and a bar that sweeps across. */
.qr-scanning {
    margin: .75rem 0 1rem;
    padding: .9rem 1.1rem .6rem;
    border: 1px solid var(--yf-border);
    border-radius: 10px;
    background: var(--bs-body-bg);
}
.qr-scan-head {
    display: flex;
    align-items: center;
    gap: .65rem;
    font-weight: 700;
    font-size: 1.05rem;
    color: var(--brand-blue-fg);
}
.qr-scan-dot {
    position: relative;
    flex: none;
    width: .75rem;
    height: .75rem;
    border-radius: 50%;
    background: var(--brand-blue);
}
.qr-scan-dot::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: var(--brand-blue);
    animation: qr-ping 1.6s ease-out infinite;
}
.qr-scan-dots {
    display: inline-block;
    width: 0;
    overflow: hidden;
    vertical-align: bottom;
    animation: qr-dots 1.6s steps(4, end) infinite;
}
.qr-scan-bar {
    position: relative;
    height: 6px;
    margin: .7rem 0 .55rem;
    overflow: hidden;
    border-radius: 3px;
    background: var(--bs-body-bg);
}
.qr-scan-bar span {
    position: absolute;
    top: 0;
    bottom: 0;
    left: -35%;
    width: 35%;
    border-radius: 3px;
    background: linear-gradient(90deg, transparent, var(--brand-blue), transparent);
    animation: qr-sweep 1.8s ease-in-out infinite;
}
.qr-scanning .qr-small { margin: 0; }
@keyframes qr-ping { from { transform: scale(1); opacity: .6; } to { transform: scale(2.8); opacity: 0; } }
@keyframes qr-dots { to { width: 1.1em; } }
@keyframes qr-sweep { from { left: -35%; } to { left: 100%; } }
@media (prefers-reduced-motion: reduce) {
    .qr-scan-dot::after, .qr-scan-bar span, .qr-scan-dots { animation: none; }
    .qr-scan-dots { width: auto; }
    .qr-scan-bar span { left: 0; width: 100%; opacity: .35; }
}

/* The top finding's title with the date scanned at the box's top right. */
.qr-finding-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: .25rem 1rem;
}
.qr-finding-head > :first-child { margin-top: 0; }
.qr-scanned { margin-left: auto; white-space: nowrap; }

/* The report page: the computer's name (h1) with the pill beside it, as on /results. */
.qr-report-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .4rem .9rem;
    margin-bottom: .75rem;
}
.qr .qr-report-head h1 { margin: 0; }

/* The report page: the computer's name is the h1, the headline under it. */
.qr-report-headline { margin: 0 0 .75rem; font-weight: 700; font-size: 1.2rem; color: var(--bs-body-color); }

/* A smaller button: Scan now (blue) under the result's pill, View full report under its box. */
.qr-btn-small { padding: .35rem .9rem; font-size: .9rem; border-radius: 6px; }
.qr-btn-blue { background: var(--brand-blue); color: var(--yf-on-color); border: 0; cursor: pointer; }
.qr-btn-blue:hover { background: var(--brand-blue); color: var(--yf-on-color); }

.qr-headline { font-weight: 700; font-size: 1.1rem; color: var(--bs-body-color); }

.qr-next { font-weight: 600; color: var(--brand-blue-fg); }

.qr-none { background: var(--bs-body-bg); color: var(--yf-muted); }

.qr-top { position: relative; margin: 1rem 0; }
.qr-top .qr-finding { margin: 0; }

/* See more / See less, without script: a hidden checkbox before the box opens its second half
   (faded out while closed), and the label under the box swaps its words. */
.qr-more-toggle { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
.qr-more-body {
    /* Closed: the start of the rest shows and fades away, so it's plain there is more. */
    max-height: 3.2rem;
    overflow: hidden;
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 0%, transparent 100%);
}
.qr-more-toggle:checked ~ .qr-finding .qr-more-body {
    max-height: none;
    overflow: visible;
    -webkit-mask-image: none;
    mask-image: none;
}
/* Under the box: See more in the middle, tucked up against it; View full report at the right. */
.qr-more-row {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: start;
    column-gap: 1rem;
    margin-top: .15rem;
}
.qr-more-label { grid-column: 2; line-height: 1.3; cursor: pointer; color: var(--brand-blue-fg); font-weight: 600; }
.qr-more-row .mk-btn { grid-column: 3; justify-self: end; margin-top: .35rem; }
.qr-more-label:hover { text-decoration: underline; }
.qr-more-toggle:focus-visible ~ .qr-more-row .qr-more-label { outline: 2px solid var(--brand-blue); outline-offset: 2px; }
.qr-more-close { display: none; }
.qr-more-toggle:checked ~ .qr-more-row .qr-more-open { display: none; }
.qr-more-toggle:checked ~ .qr-more-row .qr-more-close { display: inline; }

.qr-earlier-head { margin: 1.25rem 0 .25rem; font-size: 1rem; color: var(--yf-muted); }
.qr-earlier { list-style: none; padding: 0; margin: 0; }
.qr-earlier li { padding: .35rem 0; border-top: 1px solid var(--yf-border); }
.qr-earlier li:first-child { border-top: 0; }
.qr-earlier a { margin-right: .5rem; }

/* Scan now (/results/scan/{computer}): one card per scan on offer. */
.qr-tier h2 { margin-top: 0; }
.qr-plan-on { border-color: var(--brand-blue); background: var(--bs-body-bg); }
/* Scan now while subscribed: Standard struck through and off; the usual Priority price struck through. */
.qr .qr-struck { text-decoration: line-through; color: var(--yf-muted); }
.qr-struck strong { color: inherit; }
.qr-tier-off .qr-btn-blue:disabled { background: var(--yf-success-soft); color: var(--yf-success-fg); cursor: not-allowed; }
.qr-was { color: var(--yf-muted); font-weight: 400; margin-right: .2rem; }

/* Scan now page: Automatic scans, and No Subscription, Just Scan Me, each in its own grey box. */
.qr-section {
    margin: 1.5rem 0;
    padding: 1.25rem 1.5rem;
    background: var(--bs-body-bg);
    border: 1px solid var(--yf-border);
    border-radius: 12px;
}
.qr-section > h2:first-child { margin-top: 0; }
.qr-section > :last-child { margin-bottom: 0; }

/* The same search on the Customers page (Customers.razor): light surface, and the results
   drop down over the table instead of pushing it. */
.page-search {
    position: relative;
    flex: 0 1 22rem;
}

.page-search .cust-search {
    width: 100%;
}

.page-search .nav-search-results {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 30;
    margin-top: .2rem;
    padding: .25rem;
    background: var(--bs-body-bg);
    border: 1px solid var(--yf-border);
    border-radius: .375rem;
    box-shadow: 0 6px 16px var(--yf-shadow);
}

.page-search .nav-search-results li a {
    color: var(--bs-body-color);
}

.page-search .nav-search-results li a:hover,
.page-search .nav-search-results li a:focus {
    background: var(--bs-body-bg);
}

.page-search .nav-search-results .nav-search-empty {
    color: var(--yf-muted);
}

/* ---- Settings tables: SettingsTable (operator settings) and SettingRow (shop forms) ----------
   Global rather than scoped so both components, on any page, get one look. */
/* Settings tables. Fixed layout + fixed column widths so several tables on one page (e.g. the
   PCScan dashboard's four groups) share the same column edges. Details are in a tooltip. */
.settings-grid {
    width: 100%;
    max-width: 48rem;
    table-layout: fixed;
    border-collapse: collapse;
    margin-bottom: 1rem;
}

.col-name  { width: 45%; }
.col-value { width: 55%; }

.settings-grid th,
.settings-grid td {
    padding: .3rem .5rem;
    border-bottom: 1px solid var(--yf-border);
    vertical-align: middle;
}

.settings-grid th {
    text-align: left;
    font-size: .8rem;
    font-weight: 600;
    color: var(--yf-muted);
    border-bottom-color: var(--yf-border);
}

.settings-grid tr.changed td {
    background: var(--bs-body-bg);
}

.setting-key {
    font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
    font-size: .85rem;
    overflow-wrap: anywhere;
}

.setting-input {
    width: 100%;
    box-sizing: border-box;
}

/* Three widths, the same on every settings page: numbers, choices / short values, and text.
   max-width keeps them inside the column on a narrow screen. */
.setting-input.w-num    { width: 7rem;  max-width: 100%; }
.setting-input.w-choice { width: 16rem; max-width: 100%; }
.setting-input.w-text   { width: 100%; }

/* The (i) marker and its tooltip. */
.setting-info {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1rem;
    height: 1rem;
    margin-left: .35rem;
    border-radius: 50%;
    background: var(--bs-body-bg);
    color: var(--yf-muted);
    font: italic 700 .7rem/1 Georgia, serif;
    cursor: help;
    vertical-align: middle;
}

.setting-info:hover,
.setting-info:focus {
    background: var(--brand-blue);
    color: var(--yf-on-color);
    outline: none;
}

.setting-tip {
    display: none;
    position: absolute;
    top: 1.4rem;
    left: -.5rem;
    z-index: 20;
    width: max-content;
    max-width: 22rem;
    padding: .5rem .65rem;
    border-radius: .375rem;
    background: var(--brand-blue);
    color: var(--yf-on-color);
    font: normal 400 .8rem/1.35 var(--yf-font);
    text-align: left;
    white-space: normal;
    box-shadow: 0 4px 12px var(--yf-shadow);
}

.setting-info:hover .setting-tip,
.setting-info:focus .setting-tip {
    display: block;
}

.tip-line {
    display: block;
}

.tip-line + .tip-line {
    margin-top: .3rem;
}

.tip-muted {
    color: var(--yf-on-color);
}

/* Phones: one block per setting, key on top and value beneath. */
@media (max-width: 600px) {
    .settings-grid colgroup,
    .settings-grid thead {
        display: none;
    }

    .settings-grid,
    .settings-grid tbody {
        display: block;
    }

    .settings-grid tr {
        display: grid;
        grid-template-columns: 1fr;
        gap: .3rem .5rem;
        padding: .5rem 0;
        border-bottom: 1px solid var(--yf-border);
    }

    .settings-grid td {
        border: none;
        padding: 0;
    }

    .setting-tip {
        max-width: 80vw;
    }
}

/* "Saved" bubble beside a settings field that just saved: shows, then fades out by 3s.
   The element is recreated per save (@key), which restarts the animation. */
.setting-field {
    position: relative;
}

.saved-bubble {
    position: absolute;
    top: 50%;
    right: -5rem;
    transform: translateY(-50%);
    padding: .3rem .8rem;
    border-radius: 1rem;
    background: var(--yf-success);
    color: var(--yf-on-color);
    font-size: .9rem;
    font-weight: 600;
    white-space: nowrap;
    pointer-events: none;
    box-shadow: 0 1px 4px var(--yf-shadow);
    animation: saved-fade 3s ease-in forwards;
}

.saved-bubble::before {
    content: "";
    position: absolute;
    left: -6px;
    top: 50%;
    transform: translateY(-50%);
    border: 6px solid transparent;
    border-left: 0;
    border-right-color: var(--yf-success);
}

/* Beside a narrow field (number, drop-down, checkbox) the bubble sits right after it, not at
   the far end of the column. Still absolute -- it floats over the row and never makes it
   taller. The offsets are the cell's left padding (.5rem) + the field's width + a gap. */
.setting-input.w-num ~ .saved-bubble    { right: auto; left: calc(.5rem + 7rem + .75rem); }
.setting-input.w-choice ~ .saved-bubble { right: auto; left: calc(.5rem + 16rem + .75rem); }
input[type=checkbox] ~ .saved-bubble    { right: auto; left: calc(.5rem + 1rem + .75rem); }
/* A field with something beside it (SettingsTable RowExtra): the bubble goes back past the
   column's end, so it does not cover the links. */
.setting-field .setting-extra ~ .saved-bubble { left: auto; right: -5rem; }
/* A field's unit (SettingRow Unit; the Price List pages): beside the box, quieter than the label. */
.setting-unit { margin-left: .6rem; color: var(--yf-muted); font-size: .9rem; white-space: nowrap; }

@keyframes saved-fade {
    0%, 70% { opacity: 1; }
    100%    { opacity: 0; visibility: hidden; }
}

@media (max-width: 600px) {
    .saved-bubble,
    .setting-input.w-num ~ .saved-bubble,
    .setting-input.w-choice ~ .saved-bubble,
    input[type=checkbox] ~ .saved-bubble {
        left: auto;
        right: .35rem;
    }

    .saved-bubble::before {
        display: none;
    }
}

/* A refused save (SettingRow OnTrySave, owner 2026-10-03): the Saved pill's place and shape,
   light red. Held (no fade) while a text box keeps the cursor; a checkbox's fades like Saved.
   The default spot is further out than Saved's because the word is longer. */
.not-saved-bubble {
    position: absolute;
    top: 50%;
    right: -6.6rem;
    transform: translateY(-50%);
    padding: .3rem .8rem;
    border-radius: 1rem;
    background: var(--yf-danger-soft);
    color: var(--yf-danger-fg);
    font-size: .9rem;
    font-weight: 600;
    white-space: nowrap;
    box-shadow: 0 1px 4px var(--yf-shadow);
    animation: saved-fade 3s ease-in forwards;
}

.not-saved-bubble.held {
    animation: none;
}

.not-saved-bubble::before {
    content: "";
    position: absolute;
    left: -6px;
    top: 50%;
    transform: translateY(-50%);
    border: 6px solid transparent;
    border-left: 0;
    border-right-color: var(--yf-border);
}

.setting-input.w-num ~ .not-saved-bubble    { right: auto; left: calc(.5rem + 7rem + .75rem); }
.setting-input.w-choice ~ .not-saved-bubble { right: auto; left: calc(.5rem + 16rem + .75rem); }
input[type=checkbox] ~ .not-saved-bubble    { right: auto; left: calc(.5rem + 1rem + .75rem); }

@media (max-width: 600px) {
    .not-saved-bubble,
    .setting-input.w-num ~ .not-saved-bubble,
    .setting-input.w-choice ~ .not-saved-bubble,
    input[type=checkbox] ~ .not-saved-bubble {
        left: auto;
        right: .35rem;
    }

    .not-saved-bubble::before {
        display: none;
    }
}

/* A pill right after a button in running text (the customer page's "Email the set-password
   link again"), not out past a settings column. */
.pill-host { display: inline-block; }
.saved-bubble.beside,
.not-saved-bubble.beside {
    position: relative;
    top: auto;
    right: auto;
    left: auto;
    transform: none;
    display: inline-block;
    margin-left: .75rem;
}

.login-other > div + div { margin-top: .35rem; }

/* Shop Settings, System Prompt (owner, 2026-10-03): the system prompt read-only, then the
   shop's own, both full width up to a readable measure. The Saved pill floats past the box's
   right edge, as beside any other field. */
.shop-prompt {
    max-width: 64rem;
}

.shop-prompt label {
    display: block;
    margin-top: 1rem;
    font-weight: 600;
}

.shop-prompt label .muted {
    font-weight: normal;
    font-size: .85rem;
    margin-left: .5rem;
}

.shop-prompt textarea {
    display: block;
    width: 100%;
    margin-top: .3rem;
    padding: .45rem;
    border: 1px solid var(--yf-border);
    border-radius: .25rem;
    font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
    font-size: .85rem;
    line-height: 1.4;
    resize: vertical;
}

.shop-prompt textarea.prompt-view {
    background: var(--bs-body-bg);
    color: var(--yf-muted);
}

.shop-prompt .prompt-field {
    display: block;
}

/* "Use System Prompt": the checkbox to the left of the "System prompt" label, on one line, with
   the Saved pill after it. */
.shop-prompt .use-system-prompt {
    position: relative;
    display: flex;
    align-items: center;
    gap: .5rem;
    margin-top: 1rem;
}

.shop-prompt .use-system-prompt input[type="checkbox"] {
    margin: 0;
    cursor: pointer;
}

.shop-prompt .use-system-prompt label {
    margin-top: 0;
}

.shop-prompt .use-system-prompt .saved-bubble,
.shop-prompt .use-system-prompt .not-saved-bubble {
    /* Relative, not static: the pill's arrow (::before) is absolute and has to be placed
       against the pill itself, or it lands at the left edge of the row. */
    position: relative;
    top: auto;
    left: auto;
    right: auto;
    transform: none;
    margin-left: .4rem;
}

.shop-prompt .prompt-field .saved-bubble,
.shop-prompt .prompt-field .not-saved-bubble {
    top: 1.2rem;
}

/* A field that failed a form's check (New customer, owner 2026-10-03): light red, fading back
   to the field's own background. Two names so a second failure replays the fade. */
.field-flash-a { animation: field-flash-a 2.5s ease-out; }
.field-flash-b { animation: field-flash-b 2.5s ease-out; }

@keyframes field-flash-a { from { background-color: var(--yf-danger-soft); } }
@keyframes field-flash-b { from { background-color: var(--yf-danger-soft); } }

/* PCScan dashboard's Now table: as wide as its content, not the screen. */
.techs.qd-now {
    width: auto;
    max-width: 100%;
}

.techs.qd-now th {
    white-space: nowrap;
    padding-right: 1.5rem;
}

/* A page broken into sections the way /results is (.qr-computer): each section a white card with
   its heading inside it. Used on the technician, machine and shop-editor pages. A wrapper whose
   component rendered nothing (a panel that does not apply here) is hidden rather than left as an
   empty box. */
.page-section {
    margin: 1.25rem 0;
    padding: 1.25rem 1.5rem 1.5rem;
    border: 1px solid var(--yf-border);
    border-radius: 12px;
    background: var(--bs-body-bg);
}
.page-section > h2:first-child,
.page-section > section > h2:first-child { margin-top: 0; }
.page-section > :last-child { margin-bottom: 0; }
.page-section > .access-panel { margin-top: 0; }
.page-section:not(:has(*)) { display: none; }
.machine-layout > .page-section { margin: 0; }

/* A product's own data under the machine page's Products section (DailyDelta, PCScan):
   indented beneath the products table, its heading an h3 and its data indented under that. */
.product-detail {
    margin: 1.25rem 0 0 1.25rem;
}
.product-detail > h3 { margin: 0 0 .5rem; }
.product-detail > :not(h3) { margin-left: 1.25rem; }
.product-detail h4 { font-size: 1rem; margin-top: 1rem; }

/* Products & Permissions v1, section 6: the three layers of switches on the machine page, the
   technician page and the customer's computers page. One column per tier. */
.access-panel {
    margin-top: 1.5rem;
}

/* As wide as its columns, not the panel: at full width the row-label column took all the
   slack and pushed the checkboxes far to the right. */
table.access-table {
    width: auto;
}

.access-table th,
.access-table td {
    text-align: center;
}

/* The tier columns: each header word on its own line ("Change / Settings"), and white space
   either side of every column so Read, Change Settings and Write read as separate. */
/* The line breaks are <br>s in the markup. Not word-spacing: that trick makes the header's
   natural width enormous, and the table stretched to the whole panel to fit it. */
.access-table th.access-tier {
    line-height: 1.2;
    vertical-align: bottom;
}

.access-table th.access-tier,
.access-table td {
    padding-left: 1.25rem;
    padding-right: 1.25rem;
}

.access-table th[scope="row"] {
    text-align: left;
    font-weight: normal;
}

.access-table .access-final th[scope="row"] {
    font-weight: 600;
}

/* The Saved pill after the Shop row's checkboxes (MachineAccessPanel): in the row's last cell,
   in the flow rather than floated, so it sits just right of the last checkbox. */
/* The Saved pill floats: its cell takes no width, and the pill is positioned over the space
   to the right of the table, so nothing moves when it appears or fades. */
.access-table td.access-saved {
    position: relative;
    width: 0;
    padding: 0;
    border: 0;
}

.access-table .access-saved .saved-bubble {
    left: .75rem;
    right: auto;
}

/* The bottom row is the sum of the rows above it, so it is ruled off like a total. */
.access-table .access-final th,
.access-table .access-final td {
    border-top: 2px solid var(--yf-muted);
}

/* Read-only switches: a checkbox that cannot be changed, still legible rather than greyed out. */
.access-table input[type="checkbox"]:disabled {
    opacity: 1;
    cursor: default;
}

.tier-switches {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1.25rem;
    margin: 0.5rem 0;
}

.tier-switches label {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}

/* New Customer, under the customers table (Customers.razor). */
.cust-new { margin-top: .75rem; }
.cust-new .edit-link { margin-left: 0; }

/* An action link with no href (Activity's Abort): looks and behaves like a link, runs a handler,
   and is never taken over by enhanced navigation. */
.act-link { color: var(--brand-blue-fg); text-decoration: underline; cursor: pointer; }
.act-link:hover { color: var(--brand-blue-fg); }

/* A PCScan report inside the technicians' app (MachinePCScanReport): the customer report's look
   and width, but starting at the content's left edge like the app's other pages. */
.qr.qr-in-app { margin: 0; padding: .25rem 0 2rem; }

/* The note under a Give a free scan button (machine and customer pages): on its own line. */
.free-scan-note { display: block; margin-top: .4rem; }

/* --------------------------------------------------------------------------
   Confirmation popup (Controls/ConfirmButton): the dimmed backdrop fades in and
   the box pops up from slightly smaller, then both fade away on Cancel. Global
   for the same reason as .dots above: keyframes stay out of scoped CSS.
   -------------------------------------------------------------------------- */
@keyframes confirm-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes confirm-fade-out {
    from { opacity: 1; }
    to { opacity: 0; }
}

@keyframes confirm-pop-in {
    from { opacity: 0; transform: translateY(12px) scale(.92); }
    to { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes confirm-pop-out {
    from { opacity: 1; transform: scale(1); }
    to { opacity: 0; transform: scale(.95); }
}

@keyframes confirm-icon-pulse {
    0% { transform: scale(.6); opacity: 0; }
    60% { transform: scale(1.12); opacity: 1; }
    100% { transform: scale(1); }
}

/* "Start without a machine", first thing in the left pane (OwnedSessions.razor; owner,
   2026-10-07, moved to the top 2026-10-11). Global rather than scoped so the button keeps one
   definition. */
.anon-start {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: .35rem;
    margin: 0 0 .25rem;
}

/* Brand orange and Text on orange (Server Settings > Appearance), so it follows the scheme.
   The full width of the menu and one row tall (owner, 2026-10-11; was two thirds of its old
   size, 2026-10-07): it is the one action in the panel, and the panel's first line. */
.anon-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .375rem;
    width: 100%;
    height: 2rem;
    padding: 0 .75rem;
    font-size: .8rem;
    border: 1px solid var(--brand-orange);
    border-radius: 6px;
    background: var(--brand-orange);
    color: var(--yf-on-orange);
    font-weight: 600;
    cursor: pointer;
}

/* The plus on it: a mask painted with the button's own text color. */
.anon-btn::before {
    content: "";
    width: 1rem;
    height: 1rem;
    flex: none;
    background-color: currentColor;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.25' stroke-linecap='round'%3E%3Cpath d='M12 6v12M6 12h12'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.25' stroke-linecap='round'%3E%3Cpath d='M12 6v12M6 12h12'/%3E%3C/svg%3E") center / contain no-repeat;
}

.anon-btn:hover:not(:disabled) { filter: brightness(1.1); }
.anon-btn:disabled { opacity: .6; cursor: default; }

.anon-error { font-size: .8rem; color: var(--yf-danger-fg); }

/* ---- DailyDelta pages (owner, 2026-10-11; migration 0100) -----------------------------------
   /daily-delta: every check across the shop, newest first. /daily-delta/{machine}/{report}: one check and
   every change in it. Shared, so here rather than scoped. */
.delta-filter {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    margin: .25rem 0 1rem;
}
.delta-filter input { width: auto; margin: 0; }

.delta-when,
.delta-result,
.delta-verdict { white-space: nowrap; }
.techs .delta-num { text-align: right; white-space: nowrap; }
.techs.delta-changes td { vertical-align: top; }
.delta-summary,
.delta-change { overflow-wrap: anywhere; }
.delta-path { font-size: .85rem; }

.delta-badge {
    display: inline-block;
    padding: .05rem .5rem;
    border-radius: 999px;
    font-size: .85rem;
    font-weight: 600;
    background: var(--yf-danger-soft);
    color: var(--yf-danger-fg);
}

.delta-changes tr.delta-suspicious > td:first-child { box-shadow: inset 3px 0 0 var(--yf-danger); }
.delta-changes tr.delta-ask > td:first-child { box-shadow: inset 3px 0 0 var(--yf-muted); }
.delta-changes tr.delta-suspicious .delta-verdict { color: var(--yf-danger-fg); font-weight: 600; }

.delta-raw { margin-top: .35rem; font-size: .85rem; }
.delta-raw summary { cursor: pointer; color: var(--yf-muted); }
.delta-raw dl {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: .2rem .75rem;
    margin: .35rem 0 0;
}
.delta-raw dt { color: var(--yf-muted); }
.delta-raw dd { margin: 0; overflow-wrap: anywhere; }
.delta-raw pre {
    margin: 0;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    font-size: .8rem;
}

.delta-notes { margin: 0; padding-left: 1.25rem; }
