﻿html, body {
    font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

:root {
    --startup-bg: #FAFAFA;
    --startup-surface: #FFFFFF;
    --startup-text: #212121;
    --startup-muted: #5F6368;
    --startup-ring: #E0E0E0;
    /* A copy of PaletteLight.Primary in UI/Theme/SkyfleTheme.cs: this paints the WASM loading ring
       before Blazor exists, so it cannot read a --mud-palette-* variable. The dark value below is
       PaletteDark.Primary. SkyfleThemeTests.StartupAccent_InAppCss_IsEachPalettesPrimary fails when
       either copy drifts (ADO 360). */
    --startup-accent: #00798A;
    /* The text ON --startup-accent: the boot page's retry button in index.html. A copy of PaletteLight's
       PrimaryContrastText, and the dark value below of PaletteDark's. SkyfleThemeTests.
       StartupOnAccent_InAppCss_IsEachPalettesPrimaryContrastText fails when either copy drifts (ADO 719). */
    --startup-on-accent: #FFFFFF;
}

@media (prefers-color-scheme: dark) {
    :root {
        --startup-bg: #121212;
        --startup-surface: #1E1E1E;
        --startup-text: #E0E0E0;
        --startup-muted: #B0B0B0;
        --startup-ring: #2C2C2C;
        --startup-accent: #26C6DA;
        --startup-on-accent: #102027;
    }
}

html {
    color-scheme: light dark;
}

body {
    background-color: var(--mud-palette-background, var(--startup-bg));
    color: var(--mud-palette-text-primary, var(--startup-text));
}

#app {
    min-height: 100vh;
    background-color: var(--mud-palette-background, var(--startup-bg));
    color: var(--mud-palette-text-primary, var(--startup-text));
}

h1:focus,
h2:focus,
h3:focus,
h4:focus {
    outline: none;
}

.content {
    padding-top: 1.1rem;
}

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

.invalid {
    outline: 1px solid red;
}

.validation-message {
    color: red;
}

/* The default full-width error banner stays hidden: the offline bar (ADO 898) says when the
   connection is gone, and the error boundary in MainLayout says when a page broke. */
#blazor-error-ui {
    display: none !important;
}

/* ── Offline bar (ADO 898) ───────────────────────────────────────────────────────────────
   One slim bar directly under the fixed top app bar, sticky inside the main content: always
   visible while offline, never over the bottom navigation or the + button, and never a modal.
   It replaced a corner icon (which sat on "Messages") and a full-screen overlay. */
.offline-bar {
    position: sticky;
    top: var(--mud-appbar-height);
    z-index: 5;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.375rem 1rem;
    font-size: 0.8125rem;
    line-height: 1.35;
    background: var(--mud-palette-warning);
    color: var(--mud-palette-warning-text);
}

.offline-bar--back {
    background: var(--mud-palette-success);
    color: var(--mud-palette-success-text);
    transition: opacity 0.6s ease; /* FadeFor in OfflineBar.razor */
}

.offline-bar--fading {
    opacity: 0;
}

.offline-bar__text {
    flex: 1;
    min-width: 0;
}

.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: white;
}

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

.loading-progress {
    position: absolute;
    display: block;
    width: 8rem;
    height: 8rem;
    inset: 20vh 0 auto 0;
    margin: 0 auto 0 auto;
}

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

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

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

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

code {
    color: #c02d76;
}

/* ── Mobile bottom navigation ────────────────────────────────────────────── */
/* Items are real anchors so they are keyboard-focusable, support middle-click /
   open-in-new-tab, and expose aria-current to assistive tech. */
.bottom-nav {
    display: flex;
    flex-direction: row;
    justify-content: space-evenly;
    align-items: center;
}

.bottom-nav-link {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    padding: 4px 0;
    text-decoration: none;
    color: inherit;
    border-radius: 8px;
    min-height: 44px; /* comfortable touch target */
}

.bottom-nav-link:hover {
    text-decoration: none;
}

.bottom-nav-link:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
}

/* Respect the iOS home-indicator inset so the fixed bar doesn't overlap it. */
.bottom-nav-safe-area {
    padding-bottom: env(safe-area-inset-bottom, 0px);
    height: calc(56px + env(safe-area-inset-bottom, 0px));
    /* L41 (ADO 1076): flat, with a hairline where it meets the page. It cast an elevation 2 shadow. */
    border-top: 1px solid var(--mud-palette-lines-default);
}

/* L41 (ADO 1076): the desktop drawer sits in the page, so it is flat, with a hairline where it meets the content. It cast
   an elevation 1 shadow, and MudBlazor draws it no edge of its own. */
.skyfle-drawer {
    border-inline-end: 1px solid var(--mud-palette-lines-default);
}

/* ── Shared floating action button placement ─────────────────────────────── */
/* Desktop has no bottom bar, so sit near the corner; on mobile lift above the
   fixed bottom navigation (and the home-indicator inset). */
.skyfle-fab-fixed {
    position: fixed;
    right: 16px;
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
    z-index: 1300;
}

@media (max-width: 959.98px) {
    .skyfle-fab-fixed {
        bottom: calc(80px + env(safe-area-inset-bottom, 0px));
    }

    /* Collapse the extended "Offer something" FAB to a round icon FAB on mobile so its wide
       label stops floating over card action buttons while scrolling (same overlap gripe as the
       scroll-to-top FAB, #6).

       letter-spacing is NOT optional here. MudBlazor declares it on .mud-fab in em, so it is
       absolutised once against the button's own 14px font (0.4px) and inherited by the label span
       as a FIXED length that font-size:0 cannot re-resolve. MudFab renders Label as a bare text
       node, so there is no element to select and nowhere else to fix it: the invisible text stayed
       an anonymous flex item n_chars x 0.4px wide in a justify-content:center row and pushed the
       24px icon left by half of it — 3.0px for "Offer something", 3.2px for "Legg til kontakt",
       1.0px for "Tilby". That is why only SOME FABs looked off, and why it differed per language.
       Zeroing both is what centres the icon; the .mud-icon-root rule below does not. */
    .skyfle-fab-fixed.mud-fab-extended {
        width: 56px;
        min-width: 56px;
        height: 56px;
        padding: 0;
        border-radius: 50%;
    }

    .skyfle-fab-fixed.mud-fab-extended .mud-fab-label {
        gap: 0;
        font-size: 0;
        letter-spacing: 0;
    }

    /* Redundant today — MudIcon already carries .mud-icon-size-medium { font-size: 1.5rem }, an
       absolute rem that font-size:0 cannot touch — but kept deliberately: it is the only thing
       between font-size:0 and a zero-sized icon if MudBlazor ever expresses that size relatively. */
    .skyfle-fab-fixed.mud-fab-extended .mud-icon-root {
        font-size: 1.5rem;
    }
}

/* ADO 1040 (L11, ruled 2026-09-30): on a phone the button gets out of the way while the page scrolls down, and comes back
   on the way up and at the end. fabScroll.js sets data-fab-hidden on <html>; only this reads it, and only below 960px,
   so a desktop is unchanged.
   - Focus brings it back (:focus-within), so a keyboard user can always reach it.
   - Inventory's selection mode keeps it ([data-selecting]): there it is "Give away (n)", the count being built.
   - The transition list keeps MudBlazor's own three for .mud-fab (hover and press) and adds transform and opacity. */
@media (max-width: 959.98px) {
    .skyfle-fab-fixed {
        transition: transform 200ms ease, opacity 200ms ease,
            background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
            box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
            border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
    }

    html[data-fab-hidden] .skyfle-fab-fixed:not(:focus-within):not([data-selecting]) {
        transform: translateY(16px) scale(0.6);
        opacity: 0;
        pointer-events: none;
    }
}

/* The first motion in the app that does not answer a tap, so the first to honour reduced motion: it appears and
   disappears without animating. On phones only, like the rule it calms: unscoped, it replaced .mud-fab's own hover and
   press transitions on a desktop too (PR 827's review), where nothing hides. */
@media (max-width: 959.98px) and (prefers-reduced-motion: reduce) {
    .skyfle-fab-fixed {
        transition: none;
    }
}
/* --- Brand wordmark: self-hosted Lobster ---------------------------------
   wwwroot/fonts/lobster-skyfle.woff2, subset and shipped. font-display: swap still falls back to
   a cursive face while it loads. (This used to cite a fonts/README.md that has never existed, and
   to describe the font as not yet present — both stale since the woff2 landed.) */
@font-face {
    font-family: 'Lobster';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/fonts/lobster-skyfle.woff2') format('woff2');
}

.wordmark {
    font-family: 'Lobster', 'Brush Script MT', cursive;
    letter-spacing: 0.01em;
}

/* A nav link the ROUTE does not mark as current, but the app does — manage-offer, where the rail
   names Beholdning because that is where the page hangs (NavMenu.HighlightsInventory).

   ⚠ Why a rule at all, rather than just passing the class: MudNavLink puts a Class it is given on
   its WRAPPER (.mud-nav-item), while MudBlazor's own active styling keys off the ANCHOR
   (.mud-nav-link.active). So the class lands, does nothing, and the link looks ordinary. This is
   the bridge between the two.

   ⚠ These MIRROR `.mud-navmenu-default .mud-nav-link.active` — the DEFAULT menu's rule, which is
   the variant this app renders. Getting that qualifier wrong is not theoretical: the first draft
   copied the `.mud-navmenu-primary` declarations instead and shipped a link tinted
   primary-hover with a 2px edge that no genuinely active link in this app has. A copied
   declaration without its selector context is a guess.

   ⚠ And a copy can drift when MudBlazor restyles, so `pilot-nav-follows-the-item.mjs` compares the
   COMPUTED colour and background of a forced-active link against a genuinely active one rather
   than trusting this rule to stay in step. */
.mud-nav-item.active > .mud-nav-link:not(.mud-nav-link-disabled) {
    color: var(--mud-palette-primary);
    background-color: var(--mud-palette-action-default-hover);
}

/* The share page's wordmark, white in its arrival band (ADO 1070, L35): 56px on a phone and 72px on a PC, as drawn on
   ADO 1046's canvas. It was the app bar's 2.25rem, set to match the app; the band is the brand's moment, and the link card
   the guest just tapped (ADO 1035) has the name large. The class is older than the band: tests and the swarm's pilots
   find the guest's wordmark by it. */
.guest-header__wordmark {
    font-size: 3.5rem;
    line-height: 1;
    color: #FFFFFF;
}

@media (min-width: 960px) {
    .guest-header__wordmark {
        font-size: 4.5rem;
    }
}

/* Landing hero wordmark size. Global (not scoped) because the class is on a MudText
   component that Blazor CSS isolation doesn't reach, so a plain rule styles it — no ::deep.
   Clamped to the largest that fills the two-column hero without overflowing; bump the 8rem
   cap to go bigger. */
/* ⚠ ADO 884 (ruled 2026-09-29). The installed app opens on "/", the manifest's start_url: a signed-in person goes
   straight on to the dashboard, and a signed-out one meets the landing page instead of a bounce to a bare sign-in form.
   Somebody who has installed the app needs no pitch, so there the page is its hero alone: Skyfle, one line, email and
   Continue. A browser tab still gets the whole page. */
@media (display-mode: standalone) {
    .landing-root .landing-section:not(.landing-hero) {
        display: none;
    }
}

.landing-hero-title {
    font-size: clamp(3.5rem, 10vw, 8rem);
    line-height: 1.1;
}

/* Hero subtitle (tagline) + the sign-in hint below the email field. Global for the same
   reason as the title: both classes sit on MudText components that CSS isolation can't
   reach, so a plain rule styles them — no ::deep. */
.landing-hero-subtitle {
    color: var(--mud-palette-text-secondary);
    font-weight: 400;
}

.landing-hero-hint {
    color: var(--mud-palette-text-secondary);
}

/* Full-width Continue button on phones: a small left-aligned button under a full-width
   field reads unbalanced and gives a needlessly small touch target. Global (not scoped)
   because the class is on a MudButton. */
@media (max-width: 599.98px) {
    .landing-cta {
        width: 100%;
    }
}

/* --- Body font: self-hosted Roboto (was Google Fonts CDN) ----------------
   Weights 300/400/500/700 used by the app + MudBlazor, all shipped in wwwroot/fonts/.
   font-display: swap still falls back to the system sans-serif while they load.
   These four are the only weights that exist. A 600 is drawn at 700, so the app asks for none:
   SkyfleThemeTests holds every font-weight in the app's styles to the weights declared here, and the
   theme's font stack to the html/body one above (ADO 1024). */
@font-face {
    font-family: 'Roboto';
    font-style: normal;
    font-weight: 300;
    font-display: swap;
    src: url('/fonts/roboto-300.woff2') format('woff2');
}
@font-face {
    font-family: 'Roboto';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/fonts/roboto-400.woff2') format('woff2');
}
@font-face {
    font-family: 'Roboto';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('/fonts/roboto-500.woff2') format('woff2');
}
@font-face {
    font-family: 'Roboto';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('/fonts/roboto-700.woff2') format('woff2');
}

/* Desktop card-list idiom: a wrapping grid of fixed-width cards. Mobile lists stack
   full-width instead - the adaptive list components switch between the two. */
.skyfle-card-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    align-items: stretch;
    justify-content: flex-start;
}

.skyfle-card {
    width: 440px;
    max-width: 100%;
}

/* ── The inventory list's dense rows ───────────────────────────────────────────────────
   A LIST row is not a card you read; it is a line you scan and tap. The shared .skyfle-card
   spacing is built for cards that carry a paragraph and two buttons (the bulk give-away results,
   and the dashboard's action queue before ADO 935), and on a 390px phone it cost more height
   than anything it wrapped: 48px of padding around a 48px thumbnail, so the padding was half
   the row.

   24px instead of 48. On a phone that takes an in-use row from 96px to 72px and puts six on
   screen where three fitted before the row rework.

   ⚠ THE TAP TARGET IS THE WHOLE CARD, not the padding, and it does not get smaller here: the
   row is floored at 72px by the thumbnail and the overflow menu together, half again the 48px
   a touch target is meant to clear. Padding was never what made these rows tappable.

   ⚠ Scoped to the scan-and-tap lists deliberately, by the modifier on their grid: the inventory,
   the shared loading rows, and since ADO 604 the previously-owned list, which gained search and
   became the same kind of list. .skyfle-card is also shared with the bulk give-away results, and
   those are not scan-and-tap lines; they carry no dense grid, so this
   cannot reach them (StyleHookBindingTests pins which files carry it).

   The 12px wins over MudBlazor's .mud-card-content.mud-card-content-padding (also two classes)
   on load order: index.html links app.css after MudBlazor.min.css. */
.skyfle-card-grid--dense .mud-card-content {
    padding: 12px;
}

/* ADO 1073 (L39, ruled 2026-10-04 on ADO 1042's canvas): every list is ONE SHEET. The rows sit in one sheet of the
   surface colour, with a hairline round it and between the rows, and no shadow: People's shape (ADO 932). They were
   a raised card each, with 8px of page between them, which drew the list as a stack of separate things.

   The rows stay the MudCards they were, with every gesture, role and selected state; only their edges change. The
   modifier marks the scan-and-tap lists alone (the inventory, the archive and their loading rows), so the bulk
   give-away results, which share .skyfle-card, keep their cards. Two classes, to outrank .mud-paper's corner and
   .skyfle-card's fixed width. */
.skyfle-card-grid--dense {
    flex-direction: column;
    flex-wrap: nowrap;
    gap: 0;
    background-color: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: var(--mud-default-borderradius);
    overflow: hidden;
}

.skyfle-card-grid--dense > .skyfle-card {
    width: 100%;
    max-width: none;
    border-radius: 0;
    box-shadow: none;
}

.skyfle-card-grid--dense > .skyfle-card + .skyfle-card {
    border-top: 1px solid var(--mud-palette-lines-default);
}

/* The selected row's ring, drawn inside the row: the sheet clips what spills over its edge. */
.skyfle-card-grid--dense > .skyfle-card[aria-pressed="true"] {
    outline-offset: -2px;
}

/* ⚠ And the keyboard's focus ring, for the same reason (found in review of PR 843). The rows are a button each
   (tabindex 0), and the browser's own ring is drawn OUTSIDE the row: the sheet clipped it at the edges, and the next
   row painted over it at every join, so tabbing through the list showed nothing. The People rows and the inbox rows
   draw theirs the same way. */
.skyfle-card-grid--dense > .skyfle-card:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
}

/* The first and last rows round to the sheet's inner corner (its radius less its 1px border), so a ring drawn inside
   them follows the corner rather than being notched by the sheet's clip. */
.skyfle-card-grid--dense > .skyfle-card:first-child {
    border-top-left-radius: calc(var(--mud-default-borderradius) - 1px);
    border-top-right-radius: calc(var(--mud-default-borderradius) - 1px);
}

.skyfle-card-grid--dense > .skyfle-card:last-child {
    border-bottom-left-radius: calc(var(--mud-default-borderradius) - 1px);
    border-bottom-right-radius: calc(var(--mud-default-borderradius) - 1px);
}

/* The same sheet for a list of plain rows (L39; ADO 1074, Messages first): the rows are its children and draw no box
   of their own, so the sheet draws the edge round them and a hairline between them. The card grid above is this sheet
   over MudCards, which need their own edges taken off. */
.skyfle-sheet {
    background-color: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: var(--mud-default-borderradius);
    overflow: hidden;
}

/* ⚠ :not(:first-child) rather than "* + *", for its weight: a row that resets its own borders (a <button> row's
   "border: 0", ADO 1072's .waiting-row) sits later in this file at the same weight, and would take the hairline away. */
.skyfle-sheet > :not(:first-child) {
    border-top: 1px solid var(--mud-palette-lines-default);
}

/* The end rows round to the sheet's inner corner, as the grid's do, so a focus ring or a mark drawn inside them follows
   the corner rather than being notched by the sheet's clip. */
.skyfle-sheet > :first-child {
    border-top-left-radius: calc(var(--mud-default-borderradius) - 1px);
    border-top-right-radius: calc(var(--mud-default-borderradius) - 1px);
}

.skyfle-sheet > :last-child {
    border-bottom-left-radius: calc(var(--mud-default-borderradius) - 1px);
    border-bottom-right-radius: calc(var(--mud-default-borderradius) - 1px);
}

/* ── Dashboard backdrop ──────────────────────────────────────────────────────
   The hand-over illustration as a backdrop the dashboard is read over: as wide as the content
   column, and pinned to the viewport so it stays put while the page scrolls across it.

   It replaced a flex-sized <img> at the foot of the column (a 340px picture that took whatever
   vertical space the content left, and none that it did not). Everything that mechanism was
   careful about — never pushing the page into scrolling, never eating a tap — this still is;
   it just answers them with a zero-height layer instead of a zero-basis flex item.

   ⚠ NOT `background-attachment: fixed`, which is the obvious spelling of "fixed" and is the one
   thing that CANNOT do this. A fixed attachment moves the background POSITIONING AREA to the
   viewport, so `100%`, `cover` and `contain` all size against the window rather than against the
   element — and matching the container's width is the other half of the ask. (It is also the
   property iOS Safari has never rendered properly, where it lands somewhere between `scroll` and
   a jitter.) A STICKY layer keeps both halves: it is an ordinary flex item, so it is exactly the
   column's width without a second copy of the drawer's 250px and the layout's padding to keep in
   step, and it is pinned to the viewport for as long as the canvas is on screen — the whole page.

   ⚠ THE LAYER CONTRIBUTES NO HEIGHT. `height: 0`, with the picture painted by an absolutely
   positioned ::before that escapes the box. A layer with real height is a flex item like any
   other, and would push the content down by however tall the picture is.

   ⚠ `isolation: isolate` ON THE CANVAS IS LOAD-BEARING, not tidiness. The layer sits at
   `z-index: -1` so the cards paint over it; without a stacking context here that -1 escapes to
   the root, where negative-z children paint BEFORE the in-flow backgrounds — including
   `#app`'s opaque background-color — and the picture vanishes completely. */
.dashboard-canvas {
    --dashboard-chrome: 200px;
    /* Where the backdrop is pinned: clear of the fixed app bar (MudMainContent's pt-20 = 80px). */
    --dashboard-backdrop-top: 80px;
    /* And where it stops. Desktop leaves a little air above the fold; mobile has to clear the
       bottom nav and the page FAB, the 152px MudMainContent already reserves for them. */
    --dashboard-backdrop-bottom: 24px;
    /* ⚠ ONE NUMBER, deliberately, and the only one to touch. The picture is a saturated cartoon
       and every word on this page is read over it, so it is a tint rather than a picture.

       MEASURED, because a screenshot is not evidence here — the illustration composited over each
       background at a given alpha, sampled over the 57% of the square that is not transparent.
       Mean and worst-case channel shift away from the page background, light / dark:
           0.06 →  9 / 5 mean, 15 / 14 worst   — a hint; reads as a slightly warm page
           0.10 → 15 / 9 mean, 25 / 24 worst   — plainly a picture, still plainly a backdrop
           0.18 → 26 / 15 mean, 45 / 42 worst  — decoration competing with the content
       Dark mode lands at roughly 60% of light mode's shift at the same alpha, which is why there
       is one number rather than two: the picture is bright and the dark page is not, so the same
       alpha is already quieter there.

       Contrast is not the constraint. The cards are opaque surfaces, so the only type sitting on
       the bare canvas is #212121 on #FAFAFA: 15.4:1 undisturbed, 12.3:1 over this rule's worst
       pixel, and still 10.1:1 at 0.18 — against the 4.5:1 that WCAG AA asks for. The picture
       would have to reach roughly 0.30 before body text landed under 7:1. */
    --dashboard-backdrop-opacity: 0.10;

    isolation: isolate;

    display: flex;
    flex-direction: column;
    /* min-height, not height: it keeps the canvas at least a screen tall, which is what gives the
       sticky layer room to travel, and it grows with content taller than the viewport. */
    min-height: calc(100dvh - var(--dashboard-chrome));
}

@supports not (height: 100dvh) {
    .dashboard-canvas { min-height: calc(100vh - var(--dashboard-chrome)); }
}

/* Mobile carries the bottom nav and the page FAB; MudMainContent already reserves 152px for them. */
@media (max-width: 960px) {
    .dashboard-canvas {
        --dashboard-chrome: 300px;
        --dashboard-backdrop-bottom: 152px;
    }
}

.dashboard-backdrop {
    position: sticky;
    top: var(--dashboard-backdrop-top);
    /* Behind the content, above the canvas's own (transparent) background — see the isolate note. */
    z-index: -1;
    /* The column's full width (stretch on the cross axis), and none of its height. */
    align-self: stretch;
    flex: 0 0 auto;
    height: 0;
    /* Decoration must never eat a tap meant for something else. */
    pointer-events: none;
    user-select: none;
}

.dashboard-backdrop::before {
    content: "";
    position: absolute;
    /* left/right rather than a width, so the box is the column's width by construction. */
    top: 0;
    left: 0;
    right: 0;
    height: calc(100dvh - var(--dashboard-backdrop-top) - var(--dashboard-backdrop-bottom));

    background-image: url("../img/inspirational.webp");
    background-repeat: no-repeat;
    background-position: center;
    /* ⚠ `100% auto` — exactly the container's width, height following the aspect ratio. The
       picture is SQUARE (760×760), so at desktop widths it is taller than the window and the box
       above crops it top and bottom. `contain` is the other answer — the whole picture, narrower
       than the column — and is a one-word change right here. */
    background-size: 100% auto;
    opacity: var(--dashboard-backdrop-opacity);
}

@supports not (height: 100dvh) {
    .dashboard-backdrop::before {
        height: calc(100vh - var(--dashboard-backdrop-top) - var(--dashboard-backdrop-bottom));
    }
}

/* ⚠ NEW OBLIGATION, and it arrived with the move to a background. As a picture at the foot of the
   column this was beside the content and high contrast mode could leave it alone; behind the
   content it is a decorative image under text on a surface the user has asked to be forced to
   their own two colours. Forced colors repaints the text and the page but not this, so the one
   correct answer is to drop it. */
@media (forced-colors: active) {
    .dashboard-backdrop { display: none; }
}

/* Dashboard impact story — a uniform grid of stat cards. The celebratory hero above it (tinted
   panel, 44px number, brand illustration) was removed 2026-09-05: the illustration repeated the
   app-bar logo, and with no reach yet the panel decorated an empty headline. Reach is a card now. */
.impact-stat-grid {
    display: grid;
    /* auto-FILL, not auto-fit. auto-fit collapses the empty tracks and stretches whatever is left
       across the whole row, so a user with a single card — no reach yet, nothing handed down this
       month — got one 960px-wide bar on desktop. That is the exact "stretched/empty" failure the
       card grid was introduced to replace, and it only surfaced once the hero and the platform card
       stopped padding the row out. auto-fill keeps the tracks, so one card is one card. */
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 12px;
    margin-top: 16px;
}

.impact-stat-label {
    display: flex;
    align-items: center;
    font-size: 13px;
    color: var(--mud-palette-text-secondary);
}

/* ⚠ .impact-stat-number is deliberately UNSTYLED and must not be deleted as dead CSS. It is the
   anchor five bUnit assertions use to read a card's number, because a bare digit in the markup is
   also matched by SVG path data from the icons. It is declared here so a sweep for unused classes
   finds a reason rather than a silence. */

/* "at least" / "minst" in front of the reach number, when the count is a floor. Deliberately quiet:
   it must qualify the number without competing with it, and it appears and disappears as items
   travel onward — so it cannot be something the eye has to re-anchor on each visit. */
.impact-stat-atleast {
    font-size: 0.6em;
    font-weight: 500;
    opacity: 0.75;
    margin-right: 0.25em;
}

/* Household settings section headers (title + action buttons on one row). On a phone the two
   invite buttons shared the row with the heading and wrapped their labels onto two lines each.
   Wrapping the row instead lets the buttons drop to their own line only when they don't fit, so
   the roomier headers (Members) keep their action inline. A flex-direction override was tried and
   removed: MudStack's own .flex-row utility wins the cascade, so it was dead code — measured
   flex-direction stays `row` and the wrap is what does the work.
   Lives here rather than in a scoped .razor.css because MudStack renders its own element and
   never receives the component's scope attribute. */
.hh-section-head {
    flex-wrap: wrap;
    gap: 8px;
}

/* Buttons read as sentences, not shouting. MudBlazor uppercases every button label by default,
   which fought the sentence-case pass on headings and mangled labels that are real phrases —
   "Copy this link" rendered as "COPY THIS LINK". Norwegian suffers more, its words being longer.
   Global rather than scoped: .mud-button-root is MudBlazor's own element, which never receives a
   component's scope attribute. */
.mud-button-root {
    text-transform: none;
}

/* The dashboard's pickup strip ("Awaiting pickup"). Full-bleed, each row's chevron sat ~700px from
   its own text on a wide screen; capped, it sat in the same 440px column as the action queue's
   cards. The queue went in ADO 935 and the cap stayed, for the chevron. Global for the usual reason:
   these are MudStack/MudPaper elements. */
.dashboard-strip {
    max-width: 440px;
}

/* ADO 347. The dashboard's pickup rows are links: the anchor takes the whole row, with none of a link's text styling, and
   shows the keyboard where focus is. ADO 1072: they are the rows of a sheet now, so the ring is drawn inside the row,
   where the sheet's clip cannot cut it, and the row takes its corners from the sheet rather than rounding all four. */
.dashboard-strip-link {
    display: block;
    color: inherit;
    text-decoration: none;
}

.dashboard-strip-link:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
}

/* ── The browse-first dashboard (ADO 935, ADR-007 Amendment 2) ──────────────────────────────
   The page is a column: the "Venter på deg" strip on top, the pickup strip, then the browse feed. Its
   impact line moved to the account menu (ADO 1104). Global for the usual reason: these are MudBlazor elements and child
   components' elements, which never receive a page's scope attribute. */
.dashboard-page {
    display: flex;
    flex-direction: column;
    gap: 16px;
    padding-bottom: 16px;
}

/* On a wide screen the strip keeps the width the approved desktop canvas gave it, where the impact line stood beside it
   until that line moved to the account menu (L57, ADO 1104): rows that span the whole page put "Svar" far from what it
   answers. */
@media (min-width: 960px) {
    .dashboard-page > .waiting-strip {
        max-width: 640px;
    }
}

/* What is waiting on you: a heading over one sheet of rows (L39, ADO 1072), each row ONE control (a link, or a button
   for the two answers that open a dialog) with "Svar" as its visible affordance. The heading sat inside one bordered box
   with the rows; the sheet is .skyfle-sheet's now. */
.waiting-strip {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* A section heading that is a plain h2/h3 rather than a MudText (the dashboard's, whose ids the
   sections' aria-labelledby point at). It reads the theme's H6, so a section heading has one size
   whichever way it is written: 18px medium, sentence case (ADO 1025). */
.section-heading {
    margin: 0;
    font-family: var(--mud-typography-h6-family);
    font-size: var(--mud-typography-h6-size);
    font-weight: var(--mud-typography-h6-weight);
    line-height: var(--mud-typography-h6-lineheight);
    letter-spacing: var(--mud-typography-h6-letterspacing);
    color: var(--mud-palette-text-primary);
}

/* ⚠ The reset is the point: a <button> row otherwise renders as the browser's grey button with centred
   text, and an <a> row takes a link's colour and underline across the whole line. The hairline between rows is the
   sheet's, which outweighs this reset (ADO 1072). */
.waiting-row {
    display: block;
    width: 100%;
    margin: 0;
    padding: 0 14px;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    text-decoration: none;
}

.waiting-row:is(a, button) {
    cursor: pointer;
}

.waiting-row:is(a, button):hover {
    background: var(--mud-palette-action-default-hover);
}

.waiting-row:disabled {
    cursor: default;
    opacity: 0.6;
}

/* The reset takes the browser's focus ring with the border, so this is the one a keyboard user sees. */
.waiting-row:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
}

.waiting-row-content {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 56px;
    padding: 6px 0;
    box-sizing: border-box;
}

/* L58 (ADO 1103): what the row is about, at 40px with L41's 8px corners. It fits inside the row's 56px less its 6px of
   padding, so the row is no taller than it was. The edge is ADO 1073's, below. */
.waiting-row-photo {
    flex: none;
    width: 40px;
    height: 40px;
    overflow: hidden;
    border-radius: var(--mud-default-borderradius);
}

.waiting-row-photo > img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.waiting-row-text {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-width: 0;
}

/* L58 (ADO 1103): one line each, cut with "…", so the picture beside them never makes a row taller (Stig: "not expanding
   the current row height"). The picture takes 52px of the text's width, which made "Kari Nilsen vil knytte kontakt med
   deg" wrap to a 69px row on a 390px phone. The whole text is on the page "Svar" opens, and a screen reader reads it all. */
.waiting-row-title {
    font-size: 14px;
    color: var(--mud-palette-text-primary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.waiting-row-caption {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
}

.waiting-row-caption-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The channel's chip keeps its whole word; the text before it gives way. */
.waiting-row-channel {
    flex: none;
    padding: 0 8px;
    border: 1px solid var(--mud-palette-lines-inputs);
    border-radius: 999px;
    line-height: 18px;
}

.waiting-row-answer {
    flex: none;
    font-size: 14px;
    font-weight: 700;
    color: var(--mud-palette-primary);
}

.waiting-strip-see-all {
    align-self: flex-start;
}

/* The browse feed: finn-style, two columns on a phone and more as the column widens (ruled 2026-09-24). */
.browse-feed,
.browse-rest {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.browse-rest {
    padding-top: 8px;
    border-top: 1px solid var(--mud-palette-lines-default);
}

.browse-head {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.browse-heading {
    margin: 0;
}

.browse-rest-heading {
    margin: 0;
}

.browse-subheading {
    font-size: 13px;
    color: var(--mud-palette-text-secondary);
}

/* L56 (ADO 1100): the feed with nothing on offer is an empty SECTION under whatever waits above, not an empty screen. Its
   line and its two ways on sit left-aligned under the section's heading, where an empty screen centres them under the
   crates. The section's own 12px gap spaces the three. */
.browse-empty-line {
    max-width: 34rem;
    text-wrap: pretty;
}

.browse-empty-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.browse-add-sizes {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 44px;
    margin: 4px 0;
    padding: 0 12px;
    border-radius: 8px;
    background: color-mix(in srgb, var(--mud-palette-primary) 12%, transparent);
    color: var(--mud-palette-text-primary);
    font-size: 14px;
    font-weight: 500;
    text-decoration: none;
}

.browse-add-sizes > span {
    flex: 1 1 auto;
}

.browse-add-sizes:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 2px;
}

.browse-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px;
}

@media (min-width: 600px) {
    .browse-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (min-width: 960px) {
    .browse-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
}

@media (min-width: 1280px) {
    .browse-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}

.browse-tile {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
    color: var(--mud-palette-text-primary);
    text-decoration: none;
    border-radius: 8px;
}

.browse-tile:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 2px;
}

.browse-tile-photo {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    border-radius: 8px;
    background: var(--mud-palette-action-default-hover);
}

.browse-tile-photo > img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* A loading tile: the photo's square alone, so the grid holds its shape while the first page arrives. */
.browse-tile-placeholder {
    opacity: 0.6;
}

/* ADO 1109: the heading's line while the first page arrives, in the loading tiles' grey. Text drawn there first showed
   one heading and then another, since the empty feed's heading is its news (L55). The h2's line height keeps the line
   the height the text will have, so nothing below moves when it arrives. */
.browse-heading-placeholder {
    display: inline-block;
    width: 9em;
    height: 0.8em;
    vertical-align: middle;
    border-radius: var(--mud-default-borderradius);
    background: var(--mud-palette-action-default-hover);
    opacity: 0.6;
}

.browse-tile-fit {
    position: absolute;
    top: 8px;
    left: 8px;
    max-width: calc(100% - 16px);
    padding: 3px 8px;
    border-radius: 999px;
    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
    font-size: 12px;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    box-sizing: border-box;
}

/* Two lines before the name is cut (ADO 1072, from ADO 1063): "Puky balansesykkel, grønn – lite brukt, passer 2–4 år"
   was "Puky balansesykkel, grø…" on one. */
.browse-tile-title {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    font-size: 14px;
    font-weight: 700;
}

.browse-tile-meta,
.browse-tile-asked {
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.browse-tile-asked {
    font-weight: 700;
}

.browse-more {
    align-self: center;
}

/* The one empty state (ADO 1031; L20, ruled 2026-10-02 on the canvas): the crates, a section heading, one line, the next
   step. Every page-level and list-level empty state renders through EmptyState.razor, the dashboard's two cold starts
   included, which had a look of their own. */
.skyfle-empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    padding: 32px 16px;
    text-align: center;
}

.skyfle-empty-state__picture {
    width: 112px;
    height: auto;
}

/* ADO 1061: no line ends on one word. The review found "…så la oss starte / der." at 1280px in Norwegian. */
.skyfle-empty-state__line {
    max-width: 34rem;
    text-wrap: pretty;
}

.skyfle-empty-state__action {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    margin-top: 4px;
}

/* ADO 1108, 1122: Stig's pictures over the next step (EmptyPicture.razor). Each is the column's width on a phone and no
   wider than 360px on a PC; the img's width and height attributes keep its shape before it loads. */
.skyfle-empty-picture {
    display: block;
    width: 100%;
    max-width: 360px;
    height: auto;
}

/* L59, ADO 1108: the empty Offers page under its title, the picture and then the way on, left-aligned as the dashboard's
   empty feed is. L61, ADO 1122: the empty Beholdning page the same way, under its title and search. */
.offers-empty,
.inventory-empty {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
}

/* Desktop clearance so the fixed "Offer something" FAB (bottom: 16px, ~56px tall) can't sit on top
   of the last row when the page is scrolled to the bottom. Mobile needs none: MudMainContent
   already reserves 152px there for the bottom nav and the FAB together. */
.skyfle-fab-clearance {
    height: 88px;
}

@media (max-width: 959.98px) {
    .skyfle-fab-clearance {
        height: 0;
    }
}

/* Buttons inside a filled alert take Color.Inherit so their label isn't the alert's own colour on
   itself (a Color.Error button on a filled Error alert is 1.00:1 — invisible). But MudBlazor's
   hover and focus backgrounds are colour-SPECIFIC (.mud-button-text-error:hover and friends), so
   Inherit silently drops both — and .mud-button-root sets outline:0, leaving the error-boundary
   Retry with no focus indicator at all. That is the one control a keyboard user needs when a page
   has just blown up.

   Scoped with .mud-alert on purpose: MudBlazor builds a SNACKBAR's root class as
   `mud-snackbar mud-alert-filled-{severity}` with no `mud-alert`, so an unscoped
   `.mud-alert-filled-error` would restyle every toast in the app too.

   The hover tint is 6% and not more. It lightens the fill, and three of the four light severities
   carry a WHITE label (only Warning overrides ContrastText there) — so tinting toward white eats
   the very contrast this file spent a PR fixing. Measured worst-case across all eight
   severity×theme pairs: 18% → 3.65:1, 12% → 4.05, 8% → 4.42, 6% → 4.55. Six is the ceiling.
   Focus deliberately gets the ring ONLY, no tint: the ring is the indicator, so the tint would be
   contrast cost for nothing. */
.mud-alert.mud-alert-filled-warning .mud-button-root:hover,
.mud-alert.mud-alert-filled-error .mud-button-root:hover,
.mud-alert.mud-alert-filled-success .mud-button-root:hover,
.mud-alert.mud-alert-filled-info .mud-button-root:hover {
    background-color: rgba(255, 255, 255, 0.06);
}

/* LINKS TOO, not just buttons (ADO 461 review). The selector below used to name only
   .mud-button-root, and a MudLink renders `<a class="mud-typography mud-link ...">` — which is not
   a .mud-button-root, so it matched nothing here and fell through to MudBlazor's own reset,
   `a:focus-visible { outline: none; }`. Measured in a real browser against the shipped stylesheet:
   the link was the ONLY focusable child of a filled alert with zero visual change on focus — no
   outline, no box-shadow, no background — while the buttons either side of it got a 2px ring.
   WCAG 2.4.7, and worse than a generic miss: in the Terms banner that link is how someone reads
   what they are being asked to accept, so the control a keyboard user most needs was the invisible
   one. The app already knew anchors need this spelled out — see .bottom-nav-link:focus-visible.
   The tonal notices (.skyfle-notice, below) are not filled alerts, so every rule from here to them names them too: the
   Terms notice's link is the same link. */
.mud-alert.mud-alert-filled-warning .mud-button-root:focus-visible,
.mud-alert.mud-alert-filled-error .mud-button-root:focus-visible,
.mud-alert.mud-alert-filled-success .mud-button-root:focus-visible,
.mud-alert.mud-alert-filled-info .mud-button-root:focus-visible,
.mud-alert.skyfle-notice .mud-button-root:focus-visible,
.mud-alert.mud-alert-filled-warning .mud-link:focus-visible,
.mud-alert.mud-alert-filled-error .mud-link:focus-visible,
.mud-alert.mud-alert-filled-success .mud-link:focus-visible,
.mud-alert.mud-alert-filled-info .mud-link:focus-visible,
.mud-alert.skyfle-notice .mud-link:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 2px;
}

/* A bare inline link in a Dense alert is a ~20px-tall tap target sitting a few pixels from the
   Accept button — small enough to mis-tap on a phone, and mis-tapping here means accepting instead
   of reading. Pad it to the 24px WCAG 2.2 target minimum without disturbing the text baseline. */
.mud-alert[class*="mud-alert-filled-"] .mud-link,
.mud-alert.skyfle-notice .mud-link {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    padding-inline: 2px;
}

/* MudBlazor forces disabled labels to --mud-palette-action-disabled with !important, which on
   filled amber is 1.82:1 light / 1.11:1 dark. These buttons use Disabled as a BUSY state, so the
   label has to stay readable while the request is in flight.
   No opacity here, deliberately: dimming the label cannot reach AA at ANY value — 0.9 is still
   only 4.28:1 worst-case, 0.7 is 3.12 — so the disabled cue is MudBlazor's own cursor:default and
   pointer-events:none, plus the outline below, rather than a colour the user cannot read. */
.mud-alert.mud-alert-filled-warning .mud-button-root:disabled,
.mud-alert.mud-alert-filled-error .mud-button-root:disabled,
.mud-alert.mud-alert-filled-success .mud-button-root:disabled,
.mud-alert.mud-alert-filled-info .mud-button-root:disabled,
.mud-alert.skyfle-notice .mud-button-root:disabled {
    color: inherit !important;
}

/* ...and the outlined border, which MudBlazor also fades to action-disabled-background — ~1.19:1
   on amber, so a busy "Cancel deletion" read as a floating word rather than a button. */
.mud-alert.mud-alert-filled-warning .mud-button-outlined:disabled,
.mud-alert.mud-alert-filled-error .mud-button-outlined:disabled,
.mud-alert.mud-alert-filled-success .mud-button-outlined:disabled,
.mud-alert.mud-alert-filled-info .mud-button-outlined:disabled,
.mud-alert.skyfle-notice .mud-button-outlined:disabled {
    border-color: currentColor;
}

/* The notices at the top of every screen (install, terms, notifications): tonal in both themes, ruled 2026-10-02 (ADO
   1029, L16). Filled, the dark theme's Info was a pale slab and the brightest thing on the screen. MudBlazor's text variant
   tints at its hover opacity, too faint to read as a notice, so this sets the tint: 14% of Info into the surface, under the
   page's own text colour. Their buttons and links are Color.Inherit and follow it. SkyfleThemeTests reads the 14% from
   here and holds the text to 4.5:1 on it in both themes. The focus ring, the link's target size and the readable busy
   state are the filled alerts' rules above, which name .skyfle-notice beside them; FilledAlertFocusCssTests holds both. */
.mud-alert.skyfle-notice {
    background-color: color-mix(in srgb, var(--mud-palette-info) 14%, var(--mud-palette-surface));
    color: var(--mud-palette-text-primary);
}

/* The filled alerts' white 6% hover would vanish on the light theme's near-white tint, so a notice's buttons take the
   theme's own hover, which darkens in light and lightens in dark. MudBlazor gives the outlined ones this already; the
   text ones (Dismiss, Maybe later) had none. */
.mud-alert.skyfle-notice .mud-button-root:hover {
    background-color: var(--mud-palette-action-default-hover);
}

/* Stand-in tile for an item with no photo. It used to be a filled Color.Primary block, which in
   the dark palette is #26C6DA — 8.08:1 against its own card, i.e. the BRIGHTEST thing on the page,
   shouting about the one thing that is missing. Hierarchy exactly inverted, and only visible once
   dark mode was actually looked at.
   Neutral now, from tokens the theme already defines: the tile sits ~1.2-1.3:1 against the card so
   it reads as a quiet recess, while the initials stay legible at 4.58:1 light / 6.44:1 dark.

   Also used by the icon stand-ins on the notification panel and the message
   list. Those were painted with a per-type accent — 6.44:1 to 11.81:1 against the dark card, i.e.
   saturated colour competing with the body text beside it rather than receding behind it — and the
   accent is dropped entirely when a photo IS present, so it never encoded type; it only ever marked
   the absence of an image. The icon still carries the type. The shouting does not. */
.item-placeholder {
    background-color: var(--mud-palette-divider-light);
    color: var(--mud-palette-text-secondary);
}

/* ── A thing without a photo (ADO 1068; ADO 1044's canvas, ruled 2026-10-03: L30–L34) ─────────────────
   One tile on every surface: the logo's open crate on the .item-placeholder recess above. The crate's flaps, its front
   and the thing in it are filled with the recess colour, so the back edge of the crate disappears behind them. */
.no-photo-tile {
    --no-photo-fill: var(--mud-palette-divider-light);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    flex-shrink: 0;
    box-sizing: border-box;
    overflow: hidden;
    border-radius: var(--mud-default-borderradius);
}

/* The browse tile's square: the tile fills it, and the crate takes most of it. */
.no-photo-tile--fluid {
    width: 100%;
    height: 100%;
}

.no-photo-tile--fluid .no-photo-tile__glyph {
    width: 62%;
    height: auto;
}

.no-photo-tile__fill {
    fill: var(--no-photo-fill);
}

.no-photo-tile__caption {
    font-size: 0.8125rem;
    line-height: 1rem;
    color: var(--mud-palette-text-secondary);
}

/* L32: the share page draws the crate in the logo's colours, on a tint of the crate's own colour mixed into the
   surface, so the tint follows the theme. Two classes, to outrank .item-placeholder's background. */
.no-photo-tile.no-photo-tile--colour {
    background-color: color-mix(in srgb, var(--crate-colour) 14%, var(--mud-palette-surface));
}

/* Finish the sentence-case pass. MudBlazor uppercases tab labels from .mud-tab, which the
   .mud-button-root rule above never reached — so after the buttons were de-shouted the tabs were
   the only thing left shouting ("INNKOMMENDE" beside "Kopier denne lenken"). */
.mud-tab {
    text-transform: none;
}

/* Disabled audience options that stay readable.
   MudBlazor paints .mud-radio-content and .mud-radio-icons from --mud-palette-text-disabled, so
   redefining the variable on the container reaches the label, the control and every hover/focus
   variant in one rule instead of fighting specificity on five selectors.
   Stock disabled ink measures 2.65:1 light and 1.91:1 dark — the dark case is simply gone, which
   is only acceptable when disabled-ness is redundant with something else on screen. Here it isn't.
   color-mix against the palette's own tokens flips with the theme, which a media query could not:
   a user on an OS set to dark may have explicitly chosen light. Lands at 3.86:1 light / 5.43:1
   dark — legible, and still far below the enabled label (16.10 / 12.63) so it reads unavailable.

   @supports is load-bearing, not decorative. An earlier version claimed an unsupported color-mix
   would "drop the declaration and inherit today's behaviour". That is not how custom properties
   fail: the declaration parses and is STORED, and substitution into `color` then fails at
   computed-value time, which for an inherited property means it INHERITS. Verified in a browser.
   So without the guard, a pre-2023 engine renders these disabled options at full body-text colour
   — identical to the enabled one, with `cursor: default` as the only remaining cue and nothing at
   all on touch. That is worse than the faint-but-unambiguous state this replaces. */
@supports (color: color-mix(in srgb, red, blue)) {
    .audience-picker {
        --mud-palette-text-disabled: color-mix(in srgb, var(--mud-palette-text-secondary) 80%, var(--mud-palette-surface));
    }
}

/* ADO 936 (canvas approved 2026-09-25). Each choice on the destination dial says whom it reaches, on its own line beneath
   the choice and quieter than it, so five choices read as five answers to "who gets it?" rather than one paragraph. The
   manage page's destinations under "All contacts" read the same way. */
.destination-dial__label {
    display: block;
}

.destination-dial__reach {
    display: block;
    font-size: 0.8125rem;
    line-height: 1.3;
    color: var(--mud-palette-text-secondary);
}

/* The canvas draws each choice on the quick offer's dial as a card, and the chosen one in the brand's teal. MudBlazor marks
   the chosen radio with .mud-checked on its icon button, so the card is found by what it holds. A browser without :has()
   keeps plain cards, which still read as choices. */
.destination-dial__option {
    width: 100%;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 12px;
    padding: 2px 12px 2px 0;
    margin-block-end: 8px;
    background: var(--mud-palette-surface);
}

.destination-dial__option:has(.mud-checked) {
    border-color: var(--mud-palette-primary);
    background: color-mix(in srgb, var(--mud-palette-primary) 10%, var(--mud-palette-surface));
}

/* ADO 936 (canvas approved 2026-09-25, "En bestemt kontakt"). The one-contact list: each family's rows in one card, a person's
   avatar round and a family's square as on the People page, and the chosen row in the dial's teal, found by the .mud-checked
   MudBlazor puts on its radio. */
.contact-list__heading {
    margin-block: 12px 4px;
}

/* MudBlazor 9 lays a radio group out as a wrapping ROW, which shrank each family's card to its content. ⚠ Its own rule is
   .mud-input-control.mud-input-control-boolean-input .mud-radio-group, three classes, so this needs four to win. */
.contact-list .contact-list__groups.mud-input-control .mud-radio-group {
    flex-direction: column;
    flex-wrap: nowrap;
    gap: 8px;
}

.contact-list__group {
    width: 100%;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 12px;
    background: var(--mud-palette-surface);
    overflow: hidden;
}

.contact-list__row {
    width: 100%;
    margin: 0;
    padding: 4px 12px 4px 0;
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.contact-list__row:last-child {
    border-bottom: 0;
}

/* The whole row picks it, not only the radio and the words: a phone's thumb lands anywhere on it. */
.contact-list__row .mud-radio {
    width: 100%;
}

.contact-list__content {
    display: flex;
    align-items: center;
    gap: 12px;
    min-width: 0;
}

.contact-list__row:has(.mud-checked) {
    background: color-mix(in srgb, var(--mud-palette-primary) 10%, var(--mud-palette-surface));
}

.contact-list__avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 36px;
    width: 36px;
    height: 36px;
    border: 1px solid var(--mud-palette-primary);
    border-radius: 50%;
    color: var(--mud-palette-primary);
    font-size: 0.875rem;
}

.contact-list__row--family .contact-list__avatar {
    border-color: var(--mud-palette-lines-default);
    border-radius: 8px;
    background: var(--mud-palette-action-default-hover);
    color: var(--mud-palette-text-secondary);
}

.contact-list__text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.contact-list__name {
    font-size: 0.9375rem;
    line-height: 1.4;
}

.contact-list__caption {
    font-size: 0.8125rem;
    line-height: 1.3;
    color: var(--mud-palette-text-secondary);
}

/* The current language in the menu is marked ONLY by being disabled — there is no check icon and
   no selected state, and the trigger is a bare globe glyph with no label. MudBlazor paints it from
   --mud-palette-action-disabled, which composites to 1.88:1 light and 2.34:1 dark: the one item a
   user cannot read is the one telling them which language they are already in.
   Same treatment as .audience-picker, but a different variable and applied to the item itself —
   MudBlazor's rule is `color: ... !important`, which no plain override can outrank, and the menu
   renders in a teleported popover so a class on the container would never reach it. Redefining the
   variable where it is consumed sidesteps both.

   Opted in per item, NOT applied to .mud-menu-item.mud-disabled generally. An earlier version did
   the latter, on the strength of a grep that said these were the only disabled menu items in the
   app. The grep was single-line and the other two declarations wrap: "Request connection"
   (OfferInterestedPartiesSection) and "Dispose" (ItemDetail) are both disabled while an operation
   is IN FLIGHT — the one case where looking unavailable is the whole point. Brightening those to
   near-normal while MudBlazor keeps pointer-events:none would leave a user tapping a live-looking
   item that silently does nothing. */
@supports (color: color-mix(in srgb, red, blue)) {
    .mud-menu-item.mud-disabled.culture-option {
        --mud-palette-action-disabled: color-mix(in srgb, var(--mud-palette-text-secondary) 80%, var(--mud-palette-surface));
    }
}

/* ── D1: the item workspace's two-pane layout (Epic 500, Story 530) ────────────────────
   Deliberately NOT a media query. The three width bands are decided in C#
   (BreakpointRules.IsTwoPane), the way every other adaptive surface in this app works, so
   these rules only shape columns MasterDetailView already chose to render. A @media rule
   here would fight the C# gate and could paint two columns in a band that rendered one
   thing.

   The grid is keyed off [data-two-pane] rather than a conditional class for two reasons:
   the wrapper element must exist in every band so the pane keeps its component identity
   across a resize (see the comment in MasterDetailView), and a Razor conditional class
   nests quotes inside the class attribute, where StyleHookBindingTests cannot see it.

   400px is D1's list column; the pane takes the remainder. minmax(0, 1fr) rather than 1fr
   because a grid track defaults to min-content, and one long unbroken word in the pane
   would otherwise push the whole grid wider than its container. */
.skyfle-master-detail[data-two-pane] {
    display: grid;
    grid-template-columns: 400px minmax(0, 1fr);
    gap: 16px;
    align-items: start;
}

.skyfle-master-detail__list,
.skyfle-master-detail__pane {
    min-width: 0;
}

/* D3 — while the pane is dirty the list dims and stops taking input. Keyed off [inert] rather
   than a modifier class on purpose: inert is what actually removes the rows from the tab order,
   so styling that same attribute means the look and the behaviour cannot disagree — a list that
   looks locked and still takes clicks, or takes none while looking live, are both worse than
   either honest state. Browsers that predate inert still match the selector, so they keep the
   dim and the pointer-events block and lose only the tab-order half.
   The lock reason is rendered OUTSIDE this wrapper: a reason nobody can read is not a reason.
   No discard-changes dialog anywhere — the app has no precedent for one, which is exactly
   why D3 chose to lock the list instead. */
.skyfle-master-detail__list-rows[inert] {
    opacity: 0.45;
    pointer-events: none;
    transition: opacity 120ms ease;
}

/* Story 552 — the row whose offer is open in the pane. A left rule rather than a fill: the cards
   already carry status chips in the palette's accent colours, and a filled row competed with them
   for the same meaning. Keyed off [aria-current] rather than a modifier class so the marker screen
   readers get and the marker sighted users get cannot drift apart. */
.offers-row[aria-current="true"] {
    border-left: 3px solid var(--mud-palette-primary);
}

/* ── The /inventory list header and control row (Epic 500, Story 531; row rework) ──────
   THE ROW THIS REPLACES DID NOT FIT, and the comment that used to stand here certified
   that it did. The error is recorded rather than quietly corrected, because it is easy to
   make again:

     · it measured the title at 67px, which is a body-text figure. The markup asks for
       Typo.h4 — 2.125rem, i.e. 34px — where "Inventory" is ~144px and "Beholdning" ~169px;
     · it left out the count MudText that sits beside the title INSIDE the titles stack,
       and counted a bare number where the page renders ItemCountTotal ("12 items" ~56px,
       "12 gjenstander" ~95px);
     · it concluded nb-NO had the more slack because "Registrer" is shorter than "Register
       item". True of the button, false of the row: Norwegian's title is the wider of the
       two, and nb-NO was the worse of the two cultures, not the better.

   Totalled honestly, the old row wanted, in a 400px column:
     en-US  144 + 8 + 56 + 8 + 142 + 12 + 150 = ~520
     nb-NO  169 + 8 + 95 + 8 + 105 + 12 + 127 = ~524
   (gaps: titles Spacing="2" 8px, SpaceBetween 8px, actions Spacing="3" 12px.) Nothing ever
   overflowed, because the title is the only element permitted to shrink — so the whole
   ~120px deficit was paid, silently and in both cultures, out of the page's own name.

   The row carries two things now, the title and R12's archive link:
     en-US  144 + 8 + 150 = ~302 of 400
     nb-NO  169 + 8 + 127 = ~304 of 400
   The register button went to the control row below; the count went to a caption above the
   rows.

   ⚠ Two things that stay true whatever these numbers drift to. 400px is the two-pane list
   track (.skyfle-master-detail above) and the only width that has to fit — below 1280 the
   list is full width. And the widest TITLE this component is handed is not /inventory's but
   /previously-owned's ("Previously owned", ~264px at h4, with a count beside it), which is
   why the truncation below stays even though /inventory now has ~100px to spare.

   RE-MEASURE rather than trusting the figures above; every one of them is an approximation
   of Roboto at the typography the markup asks for. ItemsListHeaderMarkupDumpTests writes the
   real MudBlazor DOM for both cultures to SKYFLE_DUMP_HEADER_MARKUP, to be opened beside
   MudBlazor's own stylesheet. bUnit renders markup and never computes layout, so no unit
   test can answer this question — which is how the wrong arithmetic survived. */
.skyfle-list-header__titles {
    min-width: 0;
    flex: 1 1 auto;
}

.skyfle-list-header__title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The actions keep their intrinsic width; the title above gives way instead. Losing a
   character of a title costs nothing; R12's archive link pushed out of the column is the
   only route to Feature 504's whole list gone. (This used to say "a Register button" — that
   button is in the control row now, and the link is what is left to protect.) */
.skyfle-list-header__actions {
    flex: 0 0 auto;
}

/* The control row: search, the filter menu, and D4's register button after the row rework
   moved it out of the header.

   The search field is the only element allowed to give way, and that is not a preference.
   The width of the button and of the filter trigger IS a word or a glyph — shrinking those
   clips meaning. Shrinking the field only shortens a placeholder, which is decoration.

   min-width is the load-bearing half. A flex item's default min-width is min-content, so
   without this the field refuses to shrink past its own placeholder and the overflow is
   pushed onto the primary action instead. With it, the row can never overflow the 400px
   column: the worst case is an elided placeholder.

   flex: 0 0 auto on the other two is what makes that true. Flexbox distributes shrink in
   proportion to basis, so at 0 1 auto the ~142px button would absorb roughly a quarter of
   any deficit and clip its own label while the field still had room to give. */
.inventory-filter-bar__search {
    flex: 1 1 auto;
    min-width: 0;
}

.inventory-filter-bar__register,
.inventory-filter-bar__filter {
    flex: 0 0 auto;
}

/* The filter menu's panel (Epic 500, Story 506).

   Measured in the running app at 390x844 with the menu open: the panel was 582px wide, left
   -208px, right 374px, and 14 of its 25 chips sat entirely at a negative x — Size's first
   five, all three Genders, the first three of Status and of Category. Not visible, not
   tappable, and with no scrollbar anywhere that could reach them. There was no rule for this
   class at all, and that IS the bug: a MudMenu popover is position:absolute with width:auto,
   so it shrink-to-fits its widest chip row, and anchored BottomRight/TopRight it grows
   LEFTWARDS. Re-anchoring does not fix it — 582 does not fit in 390 from any corner, and
   MudBlazor's own left-edge correction is skipped for every top-/bottom- transform origin
   (mudpopoverHelper.placePopover, the !B0 guard).

   The bound goes on THIS div and not on .mud-popover, and that is not taste. That same JS
   writes style["max-width"]="none" INLINE on the popover every time it positions it, so a
   stylesheet rule there loses the cascade unless it shouts !important. Nothing touches this
   div, and a block child's max-content contribution is clamped by its own max-width — which
   is exactly what a shrink-to-fit popover sizes itself to.

   vw and not %: the popover is measured while parked at left:-9999px, so a bound that
   resolves against its containing block is not the bound we think we wrote.

   The 32px is the page's 16px gutter, twice. The trigger's right edge is at 374 on a 390px
   phone and the panel is right-aligned to it, so 390-32=358 lands the left edge on exactly
   16. The chip rows already carry Wrap.Wrap, so at 358-24=334px of content they finally
   wrap: Status 2 lines, Category 1, Size 2, Gender 1 — six lines where there were four,
   taking the panel from 284px to 356px tall. It covers LESS of the screen than it does today
   (358x356 = 127k px2 against 582x284 = 165k) and, unlike today, all of it is on the screen.

   max-height is for the households this fixture does not have: add the member group and a
   full size ladder and the panel runs past 500px. 420px is half an 844px phone. It does not
   bind at 356, so this fixture never scrolls; when it does bind, this div is the scroller and
   the affordance is a chip row cut by its edge — rows are 32px, so the cut lands inside a row
   rather than on a group boundary — plus the platform scrollbar under the thumb.

   No overscroll-behavior: contain, deliberately. On a short viewport (a landscape phone)
   MudBlazor's JS caps the popover to the space below the trigger and scrolls .mud-list around
   this box; the scroll chain out of here into that one is how the last group stays
   reachable. */
.inventory-filter-panel {
    max-width: calc(100vw - 32px);
    max-height: min(60vh, 420px);
    overflow-y: auto;
}

/* ADO 1073 (L42): the count beside a list's title, Inventory's and Previously owned's alike ("7", or "3 av 7" while a
   filter narrows). It moved up from a caption under the search, whose line had to be reserved so the rows did not
   jump when the number landed; on the title's line it reserves itself. Kept to one line: "3 av 7" must not break. */
.skyfle-list-header__count {
    white-space: nowrap;
}

/* ── The in-possession status chip (Epic 500, Story 508) ───────────────────────────────
   R5 as amended by G1: six values. Colour and variant come from the theme through
   MudChip, so only the two treatments MudBlazor has no parameter for live here.

   ⚠ The Requested chip deliberately does NOT set its own ink. Both sketch boards paint it
   white on the brand orange, which is 2.27:1 — the pairing SkyfleTheme's own comment
   exists to forbid — and MudBlazor already resolves SecondaryContrastText correctly. The
   fix is to not override it, so there is no rule for it here. */
.inventory-row-status[data-status="draft"] {
    border-style: dashed;
}

/* The chip-shaped placeholder that stands in while the page does not yet know. It must
   occupy the chip's slot rather than collapse it, or the row reflows when the real chip
   arrives — and "In use" is a positive claim that must never fill this gap. */
.inventory-row-status--pending {
    border-radius: 999px;
}

/* ── Multi-select on the item list (Epic 500, Story 507) ───────────────────────────────
   The check badge sits over the thumbnail because the mobile row has no checkbox
   column, and a tint on its own is a colour-only signal. */
.item-thumb {
    position: relative;
}

/* ADO 1073 (from ADO 1063, drawn on ADO 1042's canvas): a photo thumbnail has an edge, an inset hairline, so a white
   product shot does not dissolve into the white sheet behind it. Mixed from the text colour, so the line shows in both
   themes: dark on light, light on dark, where a black product shot would dissolve the same way. Drawn over the photo
   by a pseudo-element because an inset shadow on the img itself paints under the picture. Messages' thumbnails carry
   it too (ADO 1074): they are MudAvatars, which MudBlazor already positions and clips. So do the dashboard's two lists
   (ADO 1103): the "Venter på deg" rows' photo, and the pickup rows', which is a MudAvatar as Messages' are. And the
   bell's rows (ADO 1102), a MudAvatar too. */
.item-thumb__photo,
.inbox-thumb,
.waiting-row-photo,
.dashboard-strip-thumb,
.bell-thumb {
    position: relative;
}

.item-thumb__photo::after,
.inbox-thumb::after,
.waiting-row-photo::after,
.dashboard-strip-thumb::after,
.bell-thumb::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--mud-palette-text-primary) 14%, transparent);
    pointer-events: none;
}

/* ⚠ z-index, or an initials tile paints over the badge (ADO 1055). The tile is a MudAvatar, which MudBlazor makes
   position: relative, and it comes AFTER the badge in the markup, so at equal stacking it wins. A photo tile is not
   positioned, which is why only initials rows lost their badge. */
.item-thumb__check {
    position: absolute;
    top: -6px;
    left: -6px;
    z-index: 1;
    background: var(--mud-palette-surface);
    border-radius: 50%;
    line-height: 0;
}

/* A selected row. Tinted AND badged: two signals, because either alone excludes someone. */
.skyfle-card[aria-pressed="true"] {
    background-color: var(--mud-palette-action-default-hover);
    outline: 2px solid var(--mud-palette-primary);
}

/* R6: a row the server would refuse to offer again — MARKED, not dimmed (ADO 730).

   It used to carry opacity: 0.6, which folds the text toward the card ground. Measured from the
   theme tokens, the muted size suffix fell to 2.59:1 in light and 3.67:1 in dark, against the
   4.5:1 WCAG 1.4.3 asks of text — and the inactive-control exemption does not apply, because the
   row still opens its item on click. Nothing caught it: the contrast lens and SkyfleThemeTests
   measure token PAIRS, and the pair is fine; it was the opacity on the whole row that broke it.
   InventoryItemsCardListTests now fails if any rule on an inventory row sets opacity below 1.

   The dim was always decoration. What carries the state is unchanged: the STATUS CHIP names the
   offer doing the blocking, and the row's own reason reaches a screen reader through
   .inventory-row-blocked-reason (visually hidden via MudBlazor's .mud-sr-only). New on desktop: a
   block mark in the checkbox gutter, whose tooltip repeats that same per-status reason.

   .inventory-row--blocked itself stays on the row as the "suppressed" marker the selection tests
   address. It no longer styles anything, so it has no rule here — and no row in
   StyleHookBindingTests' rule table, which pairs each hook with a rule and would otherwise be
   satisfied by an empty one. */

/* The block mark in a suppressed row's checkbox gutter (ADO 730). Muted, because it annotates the
   row rather than competing with its name; at text-secondary it still clears the 3:1 WCAG 1.4.11
   asks of a meaningful graphic by a wide margin — 6.05:1 light, 7.69:1 dark, the same token as
   the size suffix below. */
.inventory-row-blocked-mark {
    color: var(--mud-palette-text-secondary);
}

/* The size trailing the item name on line 1, after a middle dot. Muted because it qualifies the
   name rather than standing beside it as a second fact — "Rain jacket · 62" has to read as one
   line, not as a name with something bolted on. nowrap so the dot and the number can never be
   split across a wrap and leave a lone "·" at the end of a line.

   Its own class rather than MudBlazor's .mud-text-secondary: the span is aria-hidden and paired
   with a visually-hidden "Size 62" beside it, and that pairing is this row's, not a utility's. */
.inventory-row-size {
    color: var(--mud-palette-text-secondary);
    white-space: nowrap;
}

/* ADO 604. The date on the archive row's second line, after the recipient's name ("Silje · 12. sep"),
   or alone after the chip on a retired row. Muted for the size's reason above: it qualifies the line
   rather than standing beside it as a second fact. nowrap so the dot and the date never split across
   a wrap. */
.previously-owned-date {
    color: var(--mud-palette-text-secondary);
    white-space: nowrap;
}

/* The desktop checkbox column, held open on a row that gets no checkbox. R6 renders no control at
   all on an unselectable row, so without this the thumbnail beside it slides left by the width of
   the control and the column stops being a column — more visible at 40px, not less, because the
   thumbnails are the only vertical line left in the list.

   ⚠ This width must equal the rendered MudCheckBox's footprint, and MudBlazor arrives at that from
   three separate declarations (a 24px icon, 4px of dense padding on each side, and a -4px margin on
   .mud-checkbox itself). MEASURE IT in a browser after any MudBlazor upgrade — it is one number in
   one place precisely so that is a one-line fix. */
.inventory-row-gutter {
    flex: 0 0 32px;
    width: 32px;
    /* Centres the block mark a suppressed row carries here (ADO 730). An end-state row's gutter
       stays empty, and flex on an empty box changes nothing. */
    display: flex;
    align-items: center;
    justify-content: center;
}

/* ── Selection bar and the bulk give-away (Epic 500, Stories 507 / 520) ─────────────────── */
.skyfle-selection-bar {
    min-height: 48px;
}

.skyfle-selection-bar__count {
    font-weight: 500;
}

/* The confirmation is page-hosted — no dialog, no scrim — so it needs its own breathing room
   where a MudDialog would have had padding. */
.bulk-give-away {
    padding-top: 8px;
}

.bulk-give-away__actions--stacked {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* AC 7: the ONE escape hatch from the fixed-FAB collapse above. Scoped to the data-selecting
   ATTRIBUTE, so the other three fixed FABs — and this same FAB at zero selected — keep
   collapsing. An attribute rather than a modifier class because a Razor component attribute
   cannot mix literal text with an expression, and a computed Class would hide the literal
   .skyfle-fab-fixed from StyleHookBindingTests (see Inventory.razor).

   The attribute selector adds a compound, so this outranks the collapse block above it on
   specificity (0,3,0 vs 0,2,0) and does not depend on source order. */
@media (max-width: 959.98px) {
    .skyfle-fab-fixed.mud-fab-extended[data-selecting] {
        width: auto;
        min-width: 90px;
        border-radius: 999px;
        padding: 0 16px;
    }

    .skyfle-fab-fixed.mud-fab-extended[data-selecting] .mud-fab-label {
        gap: 8px;
        font-size: 0.875rem;
        letter-spacing: 0.02857em;
    }
}

/* ── Item details: the offer summary row and the action bar (Epic 500, Story 509) ─────── */
/* AC 6's tinted treatment. The tint is the SECOND signal — the sub-line already says how many
   households are waiting, in words, because a tint alone says nothing to a screen reader. */
.item-offer-summary--wanted {
    border-left: 4px solid var(--mud-palette-secondary);
}

/* AC 3: on a phone the bar sits at the foot of the page, so it has to clear the fixed bottom
   navigation — otherwise the last control on the screen is permanently under it. */
.item-action-bar--stacked {
    padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
}

/* ── The item's lead photo (L46, ruled 2026-10-04; Task 1063) ─────────────────────────────
   Photo 1 whole and large under the title: the full width on a phone, 360px from 960px up, in a frame of one height so
   the page does not move when the photo arrives. Whole means fitted, never cropped, on the palette's quiet grey: the
   share page's way (L24). The rest of the photos are 56px tiles under it.

   ⚠ 960px, not 600px: the app's phone ends there (BreakpointRules.IsMobile), and the details list one card down
   switches with it (L47). At 600px, as first built, a 700px window got the PC's photo over the phone's list.

   ⚠ The tiles' row pulls back by its own 4px of left padding (room for a tile's focus ring), so the first tile lines up
   with the photo's edge rather than sitting 4px in from it. */
.item-lead-photos {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.item-lead-photos .item-photo-strip {
    margin-left: -4px;
}

.item-lead-photo {
    display: block;
    width: 100%;
    height: 320px;
    padding: 0;
    border: none;
    background: var(--mud-palette-background-gray);
    border-radius: var(--mud-default-borderradius);
    overflow: hidden;
    cursor: zoom-in;
    outline: 2px solid transparent;
    outline-offset: 2px;
    transition: outline-color .12s ease;
}

@media (min-width: 960px) {
    .item-lead-photo {
        width: 360px;
    }
}

.item-lead-photo img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
}

.item-lead-photo:hover {
    outline-color: var(--mud-palette-primary);
}

/* The ring AND a halo, as on a tile: the ring alone can vanish against a photo of the same colour. */
.item-lead-photo:focus-visible {
    outline-color: var(--mud-palette-primary);
    box-shadow: 0 0 0 6px rgba(var(--mud-palette-primary-rgb), .22);
}

/* ── The item's details on a phone (L47, ruled 2026-10-04; Task 1063) ─────────────────────
   A small grey label with its value under it, at the card's full width. A value breaks anywhere rather than run out of
   the card: a model number or a pasted link has no spaces to wrap at. */
.item-detail__facts {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin: 0;
}

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

/* ── Item edit form (Epic 500, Story 511) ─────────────────────────────────────────────── */
/* One field per row on a phone; paired on a wide screen. The shipped Wrap band with per-field
   min-widths did neither — it produced whatever the viewport happened to allow. */
.item-edit-row > * {
    flex: 1 1 0;
    min-width: 0;
}

/* The category-change caution and the "depends on the category" group have gone: the server no
   longer drops the size or the gender a category does not carry, so both were describing a rule
   that no longer exists. Their rules (.item-edit-category-note--warning, .item-edit-dependent__note)
   went with the markup — a rule whose hook nothing carries is dead weight that reads as live. */

/* ── Item photo editor (Epic 500) ─────────────────────────────────────────────────────── */
.item-photo-editor__tile {
    position: relative;
    width: 120px;
    height: 120px;
}

.item-photo-editor__tile img {
    width: 120px;
    height: 120px;
    border-radius: var(--mud-default-borderradius);
}

/* The remove control sits on the picture, so it needs its own ground to stay legible over
   whatever the photo happens to be. */
.item-photo-editor__remove {
    position: absolute;
    top: 2px;
    right: 2px;
    background: var(--mud-palette-surface);
}

/* Story 583 — the bottom nav carries FIVE items now that Messages has a door of its own.

   Two rules, because the width at which this actually breaks was measured rather than guessed.
   With MudBlazor's caption defaults the widest label ("Connections" en, "Beholdning" nb) is 70px,
   and each item gets a fifth of the viewport: it spills at 320px (58px slots) and 360px (66px),
   and fits from 390px (72px). So the font only shrinks below that crossover — a 390px phone, which
   is most of them, keeps the normal caption. */

@media (max-width: 430px) {
    .bottom-nav-link .mud-typography-caption {
        /* MudBlazor sets caption tracking at 0.033em, ~4px across a word as long as "Connections".
           Free width with nothing to look at, so it goes first, everywhere the bar is tight. */
        letter-spacing: normal;
        white-space: nowrap;
    }
}

@media (max-width: 380px) {
    .bottom-nav-link .mud-typography-caption {
        /* Below the crossover the tracking alone is not enough. Shrink the label rather than
           ellipsing it: a nav item whose name is cut off is a worse answer than one set a point
           smaller, and truncation is what the story ruled out. */
        font-size: 0.625rem;
        line-height: 1.25;
    }

    .bottom-nav-link {
        padding-left: 1px;
        padding-right: 1px;
        min-width: 0;
    }
}

/* ── Messages inbox, grouped by offer (Feature 560) ─────────────────────────────────────────
   The rows are plain elements with role="button" rather than MudBlazor buttons (ADO 592's idiom),
   so their affordances live here. Global rather than scoped on purpose: scoped .razor.css rules
   silently no-op on MudBlazor-rendered elements. */
.inbox-group-head,
.inbox-row {
    cursor: pointer;
}

.inbox-group-head:focus-visible,
.inbox-row:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
}

/* A conversation row inside an expanded desktop group card: tinted and set in, like the sketch. */
.inbox-group-rows .inbox-row {
    background-color: var(--mud-palette-background-gray);
    border-top: 1px solid var(--mud-palette-lines-default);
}

.inbox-row-indented {
    padding-left: 32px !important;
}

/* An offer group is one row of the sheet that holds more (ADO 1074). Its head takes the group's top corners, and
   whichever row ends it takes the bottom ones, so a focus ring on either follows the sheet's corner. A group in the
   middle of the sheet has no corners to hand down. */
.inbox-group > .inbox-group-head {
    border-top-left-radius: inherit;
    border-top-right-radius: inherit;
}

.inbox-group > :last-child,
.inbox-group-rows > .inbox-row:last-child {
    border-bottom-left-radius: inherit;
    border-bottom-right-radius: inherit;
}

/* The thumbnail box carries the stacked avatar cluster of who is asking, bottom-right. */
.inbox-avatar {
    position: relative;
    flex-shrink: 0;
}

.inbox-cluster {
    position: absolute;
    right: -8px;
    bottom: -6px;
}

.inbox-cluster .mud-avatar {
    width: 22px;
    height: 22px;
    font-size: 9px;
    border: 2px solid var(--mud-palette-surface);
}

/* Sender prefix ("You:", "Familien Berg:") sits in the preview line but reads as body text. */
.inbox-prefix {
    color: var(--mud-palette-text-primary);
}

/* The offer's status beside an item name in Messages: the shared StatusChip (ADO 1027), so its colour and fill are every
   other screen's, at the inbox's caption size rather than MudBlazor's 24px small chip. */
.mud-chip.inbox-status-chip {
    flex-shrink: 0;
    height: 20px;
    margin: 0;
    padding: 0 6px;
    font-size: 11px;
    font-weight: 500;
}

.mud-chip.inbox-status-chip .mud-icon-root {
    font-size: 14px;
    margin-inline-start: -2px;
    margin-inline-end: 3px;
}

/* The "Recipient" marker on the giver's thread with the person getting the item. */
.inbox-chip {
    flex-shrink: 0;
    font-size: 11px;
    font-weight: 500;
    line-height: 16px;
    border-radius: 999px;
    padding: 1px 6px;
    white-space: nowrap;
    color: var(--mud-palette-text-secondary);
    background-color: var(--mud-palette-background-gray);
}

.inbox-chip-primary {
    color: var(--mud-palette-primary);
    background-color: var(--mud-palette-primary-hover);
}

/* An ended offer's row reads as history: the title recedes. */
.inbox-title-ended {
    color: var(--mud-palette-text-secondary) !important;
}

.inbox-muted {
    color: var(--mud-palette-text-secondary);
}

/* "3 conversations" over the per-offer list: a quiet label, in sentence case like every label (ADO 1025). */
.inbox-section-label {
    font-size: 12px;
    font-weight: 500;
    color: var(--mud-palette-text-secondary);
    padding: 4px 4px 0 4px;
}

/* The safety line under a thread (ADO 498): a quiet row, not a banner — it is always there, so it
   must not compete with the conversation it sits under. */
.chat-safety-line {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 0 4px;
}

/* ADO 598: a guest's row says how they are reached, where an account holder's row has its chat button. */
.guest-contact {
    display: inline-flex;
    align-items: center;
    gap: 2px;
}

.chat-safety-line .chat-safety-line__action {
    padding: 0 4px;
    min-width: 0;
    text-transform: none;   /* a question and its answer, not a shouted command */
}

/* Messages at two-pane width (ADO 588): the row whose thread is open in the pane carries a left
   rule, keyed off aria-current for the same reason the offers rows are — the marker screen
   readers get and the marker sighted users get cannot drift apart. */
.inbox-group-head[aria-current="true"],
.inbox-row[aria-current="true"] {
    border-left: 3px solid var(--mud-palette-primary);
}

/* ADO 611 — the addressing chip sits on the title row, not under it: the row is already three
   caption lines deep and a fourth would push the pickup summary off a phone card. Wrapping is
   deliberate — a long item name and a long chip must not squeeze the name to an ellipsis. */
.offer-title-row {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    min-width: 0;
}

/* Small enough to read as an annotation rather than an action — this is a label, not a control,
   which is also why it is a plain span and not a MudChip. */
.offer-addressing-chip {
    display: inline-flex;
    align-items: center;
    padding: 0 8px;
    height: 20px;
    border-radius: 999px;
    font-size: 0.7rem;
    line-height: 1;
    white-space: nowrap;
    border: 1px solid transparent;
}

/* Weight follows how specifically the offer is aimed at the reader. ⚠ ContrastText is never
   recomputed by MudBlazor, so the solid arm names its foreground explicitly rather than trusting a
   theme variable to have followed the background. */
.offer-addressing-chip--you {
    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
}

.offer-addressing-chip--household {
    background: var(--mud-palette-action-default-hover);
    color: var(--mud-palette-text-primary);
}

.offer-addressing-chip--connections {
    background: transparent;
    color: var(--mud-palette-text-secondary);
    border-color: var(--mud-palette-lines-default);
}

/* ── The manage-offer page without boxes (L40, ruled 2026-10-04, ADO 1075) ─────────────────
   Each section is a heading and space, the interested people are one sheet (.skyfle-sheet), and History is a closed row.
   On a PC the page keeps a 720px column, as drawn: its boxes stretched across the whole content area. */
.manage-offer {
    max-width: 720px;
}

/* "14 visninger så langt", the line under the title. It was a section of its own, "Sett". The -8px is taken from the head
   block's 12px gap (ManageOffer.razor's Spacing="3"), so it sits 4px under the title: change one and look at the other. */
.manage-offer__views {
    margin-top: -8px;
    color: var(--mud-palette-text-secondary);
}

/* The share link on one line, cut with an ellipsis, so Share and Copy stay beside it (from ADO 1063). It broke mid-token
   over three lines at 390px. */
.offer-share-link__url {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* An interested person's row (L45, ruled 2026-10-04, ADO 1077): the mark, the name and the actions on one line, the
   actions on the right as on a PC, and the chips and the caption below at full width, set in under the name. A cluster
   too wide to sit beside the name drops to a line of its own, still on the right. On a phone the actions had a line of
   their own, under a column that left the right side empty. */
.offer-interested__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: 8px;
}

.offer-interested__mark {
    flex: none;
}

/* The name takes what the actions leave. Its 6rem basis is the room it keeps before the actions drop below it: "Velg"
   and its two icons fit beside a name at 390px, the chosen person's "Sett opp henting" does not. */
.offer-interested__name {
    flex: 1 1 6rem;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.offer-interested__actions {
    flex: none;
    margin-inline-start: auto;
}

/* Under the name: the mark's 24px and the head's 8px gap. */
.offer-interested__details {
    padding-inline-start: 32px;
}

/* History is a closed row with a hairline above and below. MudBlazor draws a panel on the surface with a line below only,
   and drops even that from the last panel, which this one always is. Same weight as those rules, and later. */
.offer-history .mud-expand-panel {
    background-color: transparent;
    border-top: 1px solid var(--mud-palette-lines-default);
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.offer-history .mud-expand-panel-header {
    padding-left: 4px;
    padding-right: 4px;
}

/* ── A one-way message as a letter (ADO 1013, canvas approved 2026-10-03) ──────────────────
   The welcome letter is a document, not a chat bubble: a sheet with the sender and the date at the top, its first
   paragraph as the title, then 16px paragraphs at a readable measure. Its edge and corners are the inbox's own sheet's
   (.skyfle-sheet, ADO 1074): the surface, a hairline and the theme's radius, so it follows the theme into dark mode. */
.one-way-letter {
    display: flex;
    flex-direction: column;
    gap: 16px;
    max-width: 600px;
    box-sizing: border-box;
    padding: 20px;
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: var(--mud-default-borderradius);
    color: var(--mud-palette-text-primary);
}

@media (min-width: 960px) {
    .one-way-letter {
        padding: 24px;
    }
}

.one-way-letter__from {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 4px;
}

/* ADO 1069: the file is 290×238, so 49×40 keeps its shape. ADO 1013 drew it in a 40×40 square, a fifth too narrow. */
.one-way-letter__logo {
    width: 49px;
    height: 40px;
}

.one-way-letter__sender {
    font-size: 0.875rem;
    line-height: 1.25rem;
    font-weight: 500;
}

.one-way-letter__date {
    font-size: 0.75rem;
    line-height: 1rem;
    color: var(--mud-palette-text-secondary);
}

.one-way-letter__title {
    margin: 0;
    font-size: 1.125rem;
    line-height: 1.5rem;
    font-weight: 500;
}

/* pre-line: a single line break inside a paragraph is the letter's own, as in its signature. */
.one-way-letter__paragraph {
    margin: 0;
    font-size: 1rem;
    line-height: 1.5rem;
    white-space: pre-line;
}

.one-way-letter__action {
    align-self: flex-start;
    margin-top: 4px;
}

/* ── Item photo strip (Story 626) ─────────────────────────────────────────────────────────
   A row of an item's photos. Each tile is a <button>, so the reset here is doing real work:
   without it the browser paints its own border, background and padding on every photo.

   ⚠ Colours come from palette variables, not literals. A hard-coded grey on the overflow tile
   stays near-white on the dark canvas and drops its "+3" to about 1.4:1 — the same class of
   defect as the filled-chip pass in July. */
.item-photo-strip {
    display: flex;
    gap: 8px;
    /* Room for the focus ring, which is drawn outside the tile. Without it the ring is clipped
       by the row on the top edge and by the card on the left. */
    padding: 4px 0 0 4px;
}

.item-photo-strip--scrolls {
    overflow-x: auto;
    /* The tile cut by the screen edge IS the affordance that says the row scrolls, so nothing
       here may shrink a tile to make them all fit. */
    scrollbar-width: none;
}

.item-photo-strip--scrolls::-webkit-scrollbar {
    display: none;
}

.item-photo-strip__tile {
    flex: 0 0 auto;
    padding: 0;
    border: none;
    background: none;
    border-radius: var(--mud-default-borderradius);
    overflow: hidden;
    cursor: zoom-in;
    outline: 2px solid transparent;
    outline-offset: 2px;
    transition: outline-color .12s ease;
}

.item-photo-strip__tile img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.item-photo-strip__tile:hover {
    outline-color: var(--mud-palette-primary);
}

/* Focus gets the ring AND a halo. The ring alone can vanish against a photo that happens to be
   the same colour; the halo survives anything behind it. */
.item-photo-strip__tile:focus-visible {
    outline-color: var(--mud-palette-primary);
    box-shadow: 0 0 0 6px rgba(var(--mud-palette-primary-rgb), .22);
}

.item-photo-strip__more {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--mud-palette-divider-light);
    color: var(--mud-palette-text-secondary);
    font-size: 1rem;
    font-weight: 500;
    cursor: pointer;
}

/* ── Confirmations (L50, ruled 2026-10-05; ADO 1090) ──────────────────────────────────────
   One dialog asks before every act: ConfirmDialog, through UI.Infrastructure.Confirmation. MudBlazor sizes a dialog to its
   content with no margin of its own, so on a phone a long question ran to both edges of the screen, its rounded corners
   against the bezel. It keeps the page's 16px gutter instead. Two classes, to outrank MudBlazor's .mud-dialog-width-sm. */
.mud-dialog.skyfle-confirm {
    max-width: min(600px, calc(100vw - 32px));
}

/* ── The viewer ───────────────────────────────────────────────────────────────────────── */

/* ⚠ The whole viewer flexes, and it has to (bug 638). MudBlazor already makes .mud-dialog a flex
   column capped at `100dvh - appbar`, with .mud-dialog-content as its `flex: 1 1 auto` child — but
   the carousel used to be a hard `height: 75vh`, a share of the VIEWPORT rather than of the room
   actually left after a title and an 80px filmstrip. At a 900px window that put the filmstrip 15px
   below the bottom of the dialog: the row you navigate with, reachable only by scrolling to find
   it. The taller the monitor the better it looked, which is why it survived being looked at.

   Sizing against the space left instead of against the window also means the title or the filmstrip
   can change height without anyone having to remember this rule exists. The four rules below are
   one chain; none of them works alone. */

/* ⚠ A DEFINITE height, not just the max-height MudBlazor already sets. A flex child can only grow
   into free space, and a dialog sized by its own content has none — with max-height alone the
   carousel collapsed to its min-height and the viewer became a 200px letterbox on a 1200px screen,
   while every "does the filmstrip fit" assertion went green. Full height is the right answer for a
   lightbox anyway: the photo is the whole point of the screen, and with one photo (no counter, no
   filmstrip) it reads as a lightbox rather than as a dialog hugging a small image.
   The declaration is repeated in vh and dvh on purpose — the dvh line is ignored where it is not
   supported, which is the same fallback MudBlazor writes for its own max-height. */
.image-gallery-dialog {
    height: calc(100vh - var(--mud-appbar-height));
    height: calc(100dvh - var(--mud-appbar-height));
}

.image-gallery-dialog .mud-dialog-content {
    display: flex;
    flex-direction: column;
}

.image-gallery-dialog__surface {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    /* Without this the carousel refuses to shrink below its content and the flex is decorative:
       min-height on a flex item defaults to auto, not 0. */
    min-height: 0;
}

.image-gallery-dialog__carousel {
    flex: 1 1 auto;
    /* A floor, so a very short window scrolls the dialog rather than rendering a sliver of photo
       above a full-size filmstrip. */
    min-height: 200px;
}

/* Holds focus so the arrow keys have somewhere to land. It is a surface, not a control, so it
   must not draw a focus ring of its own — the photo is what the eye is on. */
.image-gallery-dialog__surface:focus,
.image-gallery-dialog__surface:focus-visible {
    outline: none;
}

/* Black in both themes on purpose: a lightbox's job is to get out of the way of the photo, and
   a light ground behind a contained image reads as part of the picture. */
.image-gallery-dialog__frame {
    height: 100%;
    width: 100%;
    background: #000;
}

.image-gallery-dialog__counter {
    color: var(--mud-palette-text-secondary);
}

.image-gallery-dialog__filmstrip {
    /* Never squeezed: the photo above it gives up the space instead. */
    flex: 0 0 auto;
    display: flex;
    justify-content: center;
    gap: 8px;
    padding: 16px 4px 8px;
    overflow-x: auto;
    scrollbar-width: none;
}

.image-gallery-dialog__filmstrip::-webkit-scrollbar {
    display: none;
}

.image-gallery-dialog__frame-btn {
    flex: 0 0 auto;
    width: 56px;
    height: 56px;
    padding: 0;
    border: none;
    background: none;
    border-radius: var(--mud-default-borderradius);
    overflow: hidden;
    cursor: pointer;
    opacity: .55;
    outline: 2px solid transparent;
    outline-offset: 2px;
    transition: opacity .12s ease, outline-color .12s ease;
}

.image-gallery-dialog__frame-btn img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.image-gallery-dialog__frame-btn:hover,
.image-gallery-dialog__frame-btn:focus-visible {
    opacity: 1;
    outline-color: var(--mud-palette-primary);
}

/* ⚠ Not opacity alone. "Which one am I on" has to survive a photo that is nearly white and a
   viewer who cannot tell .55 from 1 — the ring is the part that carries the answer.

   ⚠ And NOT white. The image sits on a black ground, but the filmstrip below it sits on the
   DIALOG's surface, which is white in the light theme — so a white ring was invisible exactly
   where it was supposed to be doing the work. bUnit cannot see this: it renders no stylesheet, so
   every test still passed. It took looking at the running app. */
.image-gallery-dialog__frame-btn.is-current {
    opacity: 1;
    outline-color: var(--mud-palette-primary);
}

/* ── Guest share page: the hero as a control (Story 629) ──────────────────────────────────
   The image was a bare <img>. It is a button now so the largest thing on a stranger's screen
   opens the viewer like everything else does — the reset keeps it looking exactly as it did.
   ADO 1070: square, edge to edge in the card, which clips its corners; so the focus ring is drawn
   inside the photo, where the card cannot clip it. */
.shared-offer-hero {
    display: block;
    position: relative;
    width: 100%;
    padding: 0;
    border: none;
    background: none;
    cursor: zoom-in;
    border-radius: 0;
    outline: 2px solid transparent;
    outline-offset: -3px;
    transition: outline-color .12s ease;
}

.shared-offer-hero:hover,
.shared-offer-hero:focus-visible {
    outline-color: var(--mud-palette-primary);
}

/* Sits ON the photo, so it needs its own ground to stay legible over whatever is behind it —
   same reasoning as .item-photo-editor__remove. Not a palette colour: this is ink on a
   photograph, and a photograph is neither light nor dark theme. */
.shared-offer-hero__count {
    position: absolute;
    right: 10px;
    bottom: 10px;
    background: rgba(0, 0, 0, .55);
    color: #FFFFFF;
    font-size: .78rem;
    border-radius: 999px;
    padding: 4px 11px;
}

/* ── The share page as Skyfle's showcase (ADO 1070; the canvas of ADO 1046, ruled 2026-10-03: L35–L38) ─────────────

   L35, the arrival band. A guest comes from the teal link card (ADO 1035), and the page carries it on: the bar
   (MainLayout gives a guest on /offer/* the share-band-bar class) and the band under it are one teal block, with
   "Logg inn" in the bar, and the large crates and the white wordmark in the band. The first sheet overlaps its foot.

   ⚠ The teal is the BRAND here, not an action (L3): nothing but "Logg inn" sits on it. In light it IS the palette's
   primary, #00798A, 5.12:1 under white. In dark it is a deep teal, #00515C, because the dark palette's primary is a bright
   cyan that white cannot sit on. SkyfleThemeTests holds both values, and ColourRolesTests the roles.

   ⚠ The dark value cannot hang off a class: MudBlazor 9 switches theme by rewriting the :root variables (NotFound.razor
   says more), so nothing marks an ancestor. It does write --mud-native-html-color-scheme, light or dark, with them
   (MudBlazorColorSchemeVariableTests); and light-dark() picks by the element's OWN color-scheme, so the band follows the
   app's theme, never the operating system's. A browser without light-dark() keeps the first declaration, the light teal,
   still 5.12:1 under white. */
.share-band,
.mud-appbar.share-band-bar {
    color-scheme: var(--mud-native-html-color-scheme, light);
    background-color: #00798A;
    background-color: light-dark(#00798A, #00515C);
    color: #FFFFFF;
}

/* 84px under the brand: 28px of band, then the 56px the card overlaps. */
.share-band {
    padding: 4px 20px 84px;
}

/* On a PC, in line with the card: MaxWidth.Medium is 960px, less the container's 16px a side. */
.share-band__brand {
    display: flex;
    align-items: center;
    gap: 12px;
    max-width: 928px;
    margin: 0 auto;
}

.share-band__logo {
    display: block;
}

.share-page--band {
    margin-top: -56px;
}

@media (min-width: 960px) {
    .share-band {
        padding: 4px 16px 88px;
    }

    .share-band__brand {
        gap: 16px;
    }

    .share-band__logo {
        width: 98px;
        height: 80px;
    }

    .share-page--band {
        margin-top: -64px;
    }
}

/* The item card: 12px corners with the photo clipped to them, and a shadow that lifts it off the band.
   ⚠ clip, not hidden, where the browser has it: hidden makes the card a scroll container, and the photo column's sticky
   inner would then stick to the card, which never scrolls. Two classes, to outrank .mud-elevation-0. */
.mud-card.share-card {
    border-radius: 12px;
    overflow: hidden;
    overflow: clip;
    box-shadow: 0 2px 10px rgba(0, 0, 0, .14);
}

/* The title: 28px on a phone and 32px on a PC, the one page title larger than 24px (L5, L12). Two classes, to outrank
   .mud-typography-h5. TypeScaleTests lists it among the sizes above a page title. */
.page-title.share-offer-title {
    font-size: 1.75rem;
    line-height: 2.125rem;
}

@media (min-width: 960px) {
    .page-title.share-offer-title {
        font-size: 2rem;
        line-height: 2.5rem;
    }
}

/* L36: "Familien Berg gir bort", the name in the page's ink and the words after it a step quieter. */
.share-giver {
    color: var(--mud-palette-text-secondary);
}

.share-giver strong {
    color: var(--mud-palette-text-primary);
}

/* L37: the one thing a visitor came here to do, 48px tall (MudBlazor's large button is about 42px). */
.mud-button-root.share-cta {
    min-height: 48px;
    font-size: 1rem;
}

/* The crate in the photo frame (ADO 1068): square, as the card clips it, and 176px at every width, as drawn, where the
   browse tile's fluid crate takes 62% of its square. */
.shared-offer-photo-frame .no-photo-tile {
    border-radius: 0;
}

.shared-offer-photo-frame .no-photo-tile--fluid .no-photo-tile__glyph {
    width: 176px;
}

/* L38, the good news first: a green panel (L3: "it's yours") with the crates, above the item card. The tint is the
   palette's success mixed into the surface, so it follows the theme. The line is the success green itself, at the page
   title's 24px medium: large text, 4.4:1 on the tint in light and 5.5:1 in dark. */
.share-good-news {
    margin-bottom: 16px;
    padding: 24px 20px;
    border-radius: 12px;
    background-color: color-mix(in srgb, var(--mud-palette-success) 12%, var(--mud-palette-surface));
}

.share-good-news__crates {
    display: block;
    margin: 0 auto 12px;
}

.share-good-news__title {
    text-align: center;
    color: var(--mud-palette-success);
    font-size: var(--mud-typography-h5-size);
    font-weight: 500;
    line-height: 1.25;
}

/* On the band (a guest who opened their personalised link) the panel is the sheet that overlaps it, so it lifts too. */
.share-page--band .share-good-news {
    box-shadow: 0 2px 10px rgba(0, 0, 0, .14);
}

/* On a PC the panel is the card's width, and its buttons ran 900px wide: what is in it keeps a phone's column. A button
   is inline-flex, which auto margins cannot centre, so here it is a block. */
@media (min-width: 960px) {
    .share-good-news > * {
        max-width: 480px;
        margin-left: auto;
        margin-right: auto;
    }

    .share-good-news > .mud-button-root {
        display: flex;
    }
}

/* ── The offer preview (Story 641) ─────────────────────────────────────────────────────── */

/* The hosted page is a full page: it assumes it owns the viewport. Inside a dialog it needs a
   viewport of its own, hence the scroll — the alternative is a dialog as tall as the offer.

   ⚠ `inert` is what makes the preview safe, and it is an ATTRIBUTE on the container, not this rule.
   The rule below only stops the cursor implying otherwise; deleting it would be cosmetic, deleting
   the attribute would let a giver act on their own offer. */
.offer-preview-dialog__surface {
    max-height: 65vh;
    overflow-y: auto;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: var(--mud-default-borderradius);
    padding: 8px;
    cursor: default;
}

/* ADO 1070: the share page's arrival band runs edge to edge, so the frame that holds it gives up its inset, or the giver's
   preview of what a stranger sees would show the band inside a white margin no stranger sees. The border stays: it is
   what says this is a picture of the page. (Full screen, below, drops the padding anyway.) */
.offer-preview-dialog__surface:has(.share-band) {
    padding: 0;
}

.offer-preview-dialog__audience {
    color: var(--mud-palette-text-secondary);
}

/* Full screen (phones): the frame and the inner scroll box both stop helping. The border would
   outline the whole screen, and a 65vh scroller inside a full-height dialog is a small window
   into a page that already has the room to be itself — two scrollbars for one document.

   ⚠ Keyed on the dialog MudBlazor actually renders for FullScreen, not on a media query. The
   preview is full screen because the CALLER said the viewport is a phone (BreakpointRules), and
   a media query here would answer a slightly different question and disagree at the edges. */
.mud-dialog-fullscreen .offer-preview-dialog__surface {
    max-height: none;
    overflow-y: visible;
    border: none;
    border-radius: 0;
    padding: 0;
}

/* The notice keeps its inset so it does not run to the screen edge on a phone. */
.mud-dialog-fullscreen .offer-preview-dialog__notice {
    margin-inline: 4px;
}
/* ⚠ And the dialog's own side padding goes with them. A visitor's phone gives the page the whole
   390px and the page applies its own padding; leaving the dialog's 24px each side rendered the
   preview 48px narrower than anything a visitor sees, which is a smaller version of the nesting
   this full-screen mode exists to fix. Scoped to this dialog rather than to full screen in
   general: this is the app's first full-screen dialog, and dropping a dialog's padding only makes
   sense for one that hosts a whole page. The next one will almost certainly want its padding. */
.offer-preview-dialog.mud-dialog-fullscreen .mud-dialog-content {
    padding-inline: 0;
}

/* ADO 932: the People page as one list. Each row is one real <button>, so Tab reaches it and Enter opens its sheet,
   drawn as a list row rather than a MudButton, whose uppercase label and centring fight a two-line row. */
.skyfle-people-list {
    display: flex;
    flex-direction: column;
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: var(--mud-default-borderradius);
    overflow: hidden;
}

.skyfle-people-row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    min-height: 64px;
    padding: 10px 12px;
    border: 0;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.skyfle-people-row:last-child {
    border-bottom: 0;
}

.skyfle-people-row:hover {
    background: var(--mud-palette-action-default-hover);
}

.skyfle-people-row:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
}

/* ADO 932 review: everything inside the row button is a <span>, since a <button> may hold no block and nothing
   interactive, so these lay out what MudAvatar, MudStack, MudText and MudChip used to. */
.skyfle-people-row__avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 40px;
    width: 40px;
    height: 40px;
    border: 1px solid var(--mud-palette-primary);
    border-radius: 50%;
    color: var(--mud-palette-primary);
    font-size: 1rem;
}

.skyfle-people-row__avatar--household {
    border-color: var(--mud-palette-lines-default);
    border-radius: var(--mud-default-borderradius);
    background: var(--mud-palette-action-default-hover);
    color: var(--mud-palette-text-secondary);
}

.skyfle-people-row__text {
    display: flex;
    flex: 1;
    flex-direction: column;
    min-width: 0;
}

.skyfle-people-row__name {
    font-size: 1rem;
    line-height: 1.5;
}

.skyfle-people-row__line {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    font-size: 0.75rem;
    line-height: 1.4;
}

.skyfle-people-row__chip {
    padding: 0 8px;
    border: 1px solid var(--mud-palette-primary);
    border-radius: 999px;
    color: var(--mud-palette-primary);
    line-height: 20px;
}

/* Filled, never amber ink. A user can flip theme at runtime, and amber on the light surface reads at 1.66:1, so the app
   paints a warning as the palette's warning with its own contrast text: the offline bar above does the same. */
.skyfle-people-row__warning {
    padding: 0 6px;
    border-radius: 999px;
    background: var(--mud-palette-warning);
    color: var(--mud-palette-warning-text);
}
