﻿/* ---------------------------------------------------------------------------
   The boot screen, and the error strip.

   Everything above this comment used to be the stock Blazor WASM template: Bootstrap button
   and form rules for a UI that uses MudBlazor, and a two-circle spinner in the template's
   own blue. None of the Bootstrap rules matched a single element in the app.

   The spinner mattered more than it looked. ADR 0005 puts cold starts at 10-30 seconds and
   design-principles.md calls loading the most important decision the cost architecture forces
   - but LoadingState.razor cannot render until the runtime is downloaded and started. Until
   then this file is the entire user interface, so this is the screen that carries the worst
   wait in the app.

   Two different waits, said two different ways: here the app itself is downloading, and the
   percentage is real, so the note reports it. LoadingState's bar is indeterminate because it is
   waiting on a server that has not told it anything yet.

   The bar that used to sit here has been replaced by the mark drawing itself - the same opening
   gesture as the landing page, so arriving at the app looks like arriving somewhere already
   visited. The number it carried was the honest part and it moved to the note; the drawing runs
   on its own clock and is never allowed to stand in for progress it knows nothing about.

   Literal hex, because --mud-palette-* does not exist until MudThemeProvider renders. The
   values are the light and dark tokens from design-principles.md, and the media query is the
   only place in the app that has to duplicate them.
   --------------------------------------------------------------------------- */

html, body {
    font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
}

h1:focus { outline: none; }

.boot {
    position: fixed;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 18px;
    padding: 32px 22px;
    text-align: center;

    background: #FAFAF8;
    color: #2E2C31;
}

/*
    A splash, not a toolbar. prototype-inventory.md describes the AppLoader as logo, name and
    progress messaging, and design-principles.md calls the lockup a header and splash mark - it
    is 3.9:1 and wants horizontal room, so this is the one screen that can give it any.

    Sized against the viewport with a ceiling: large on a phone, not absurd on a 4K monitor. It
    is a background rather than an <img> so the light and dark artwork swap with the media query
    below - the mark is a PNG and #7851A9 measures 2.7:1 on the dark ground.
*/
.boot__mark {
    /*
        Larger than it was. The bar underneath it is gone, so the mark is the screen now rather
        than one element on it - and this is the only place in the app that can give a 3.9:1
        lockup the horizontal room it wants.
    */
    width: min(84vw, 420px);

    /* The lockup's own proportions (630x162), so it never distorts as the width changes. */
    aspect-ratio: 630 / 162;

    /* The trace is absolutely positioned against this. */
    position: relative;

    opacity: 0.92;
    background: url("../brand/main_logo.png") center / contain no-repeat;
}

/*
    The drawing, layered exactly over the static mark.

    The static background is the BASE state and this is added on top, so every way the drawing
    can fail - no script, no SVG, no Web Animations, reduced motion, a thrown exception - lands
    on a finished logo rather than an empty box. index.html removes the class the moment the
    last stroke settles, and the artwork underneath is the same file, so the hand-off cannot be
    seen.
*/
.boot__trace {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    overflow: visible;
    pointer-events: none;
}

/*
    While drawing, the trace IS the mark. Hiding the background is what makes it a draw rather
    than a fade-in over an already-visible logo.

    Guarded by no-preference rather than switched off again under reduce, so the static mark is
    never turned off for somebody who will not see the trace. Restoring it instead would mean
    naming the artwork a third and fourth time - once per theme - and the light lockup measures
    2.7:1 on the dark ground, so getting that pair wrong is invisible here and unreadable there.
*/
@media (prefers-reduced-motion: no-preference) {
    .boot--drawing {
        background-image: none;
    }

    /*
        A LONG WAIT, said without lying about it.

        ADR 0005 puts cold starts at 10-30 seconds and the drawing is over in under two, so most
        of a cold start is the screen after it - and a completely still one reads as crashed.
        index.html adds this once the wait has gone on long enough that a warm start would
        already be over.

        A slow breath rather than a redraw on repeat. The distinction is the claim each makes:
        this one says the page is alive, where a looping 1.8s drawing would be a spinner saying
        "still nothing" over and over, and a stroke animated to the download percentage would be
        a progress bar the drawing has no way to honour. The real number is in the note.

        Opacity only, so it composites and costs nothing on a phone that is already busy
        starting a WebAssembly runtime.
    */
    .boot--waiting {
        animation: boot-breathe 2.6s ease-in-out infinite;
    }
}

@keyframes boot-breathe {
    0%, 100% { opacity: 0.92; }
    50% { opacity: 0.55; }
}

.boot__word {
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 26px;
    font-weight: 700;
    letter-spacing: -0.03em;
}

.boot__meal { color: #7851A9; }
.boot__az { color: #4E9E6C; }

.boot__note {
    margin: 0;
    max-width: 30ch;
    font-size: 13px;
    line-height: 1.55;
    color: #736F7A;
}

.boot__note::after { content: var(--blazor-load-percentage-text, "Starting up"); }

/*
    ASKED FOR NO ANIMATION, SO THERE IS NONE - and the logo is still there.

    index.html returns before building anything when this matches, so normally no trace exists
    to hide. This rule is the second line: a preference that changes after the draw has started,
    or a browser that reports it only to CSS, still ends on the finished static mark instead of
    a blank space where the background was turned off.
*/
@media (prefers-reduced-motion: reduce) {
    .boot__trace { display: none; }

    /* Decorative. Somebody who asked for no motion gets a mark that simply sits there, which is
       what they asked for - the note still reports the real download. */
    .boot--waiting { animation: none; }
}

/*
    THE SPLASH FOLLOWS THE CHOICE, not just the system.

    index.html stamps data-theme on <html> from the stored choice before this file loads, so the
    three states are: an explicit "dark" wins, an explicit "light" blocks the media query, and no
    attribute at all means System - which is what prefers-color-scheme is for.

    The dark values live in one place and are pointed at twice, because writing them out twice is
    how the splash and the toggle drift apart.
*/
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .boot {
        background: #262429;
        color: #F5F3F7;
    }

    /* #7851A9 measures 2.7:1 on this ground, so the mark and wordmark both lighten. */
    :root:not([data-theme="light"]) .boot__mark { background-image: url("../brand/main_logo_dark.png"); }
    :root:not([data-theme="light"]) .boot__meal { color: #C9B6EC; }
    :root:not([data-theme="light"]) .boot__az { color: #78D79B; }

    :root:not([data-theme="light"]) .boot__note { color: #A5A1AC; }
}

/* And the explicit choice, which wins on a machine set the other way. */
:root[data-theme="dark"] .boot {
    background: #262429;
    color: #F5F3F7;
}

:root[data-theme="dark"] .boot__mark { background-image: url("../brand/main_logo_dark.png"); }
:root[data-theme="dark"] .boot__meal { color: #C9B6EC; }
:root[data-theme="dark"] .boot__az { color: #78D79B; }
:root[data-theme="dark"] .boot__note { color: #A5A1AC; }

/*
    Blazor's unhandled-error strip. Restyled rather than removed - it is the last thing standing
    when the app has fallen over, so it must not depend on anything the app renders.
*/
#blazor-error-ui {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 1000;
    display: none;
    box-sizing: border-box;
    padding: 14px 18px;

    background: #33303A;
    color: #F5F3F7;
    font-size: 14px;
    box-shadow: 0 -2px 12px rgb(0 0 0 / 25%);
}

#blazor-error-ui .reload {
    color: #C9B6EC;
    margin-left: 6px;
}

#blazor-error-ui .dismiss {
    position: absolute;
    top: 10px;
    right: 14px;
    cursor: pointer;
    padding: 4px 8px;
    color: #A5A1AC;
}

.blazor-error-boundary {
    padding: 14px 18px;
    background: #33303A;
    color: #F5F3F7;
}

.blazor-error-boundary::after { content: "Something went wrong on this screen."; }

/* ---------------------------------------------------------------------------
   Onboarding wizard. Mobile-first: this is the base layout, and larger screens
   only get a max-width and more breathing room (ui/design-principles.md).
   Colours come from MudBlazor theme tokens where possible; these are the few
   shapes the theme has no opinion about.
   --------------------------------------------------------------------------- */

.wizard {
    display: flex;
    flex-direction: column;

    /* Fixed height, not min-height: the page itself must never scroll, on any device.
       dvh rather than vh so a mobile browser's collapsing address bar does not leave the
       primary action under the chrome. */
    height: 100dvh;
    overflow: hidden;

    padding: 0 22px calc(24px + env(safe-area-inset-bottom));
    max-width: 520px;
    margin: 0 auto;
}

/* Each step fills what is left and scrolls INTERNALLY if it ever has to - a long error, a
   large font size, a very short window. min-height:0 is what lets a flex child shrink
   below its content instead of pushing the container taller. */
.wizard-step {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
}

.wizard-step-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    /* Room for a focus ring on the last field, which would otherwise clip against the edge. */
    padding-bottom: 4px;
}

.wizard-loading {
    margin: auto;
    text-align: center;
}

.wizard-progress {
    display: flex;
    align-items: center;
    gap: 5px;
    padding: 24px 0 0;
    flex-shrink: 0;
}

.wizard-progress span {
    flex-grow: 1;
    height: 4px;
    border-radius: 2px;
}

.wizard-progress span.done { background: var(--mud-palette-primary); }
.wizard-progress span.todo { background: var(--mud-palette-lines-default); }

.wizard-progress-label {
    flex-grow: 0 !important;
    height: auto !important;
    background: none !important;
    font-size: 12px;
    font-weight: 600;
    color: var(--mud-palette-text-secondary);
    white-space: nowrap;
    padding-left: 6px;
}

.wizard-brand { padding-top: 22px; flex-shrink: 0; }

.wizard-wordmark {
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 20px;
    font-weight: 700;
    letter-spacing: -0.025em;
}

.wizard-wordmark .m { color: var(--mud-palette-primary); }
.wizard-wordmark .z { color: var(--mud-palette-success); }

.wizard-title {
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 29px;
    font-weight: 600;
    line-height: 1.15;
    letter-spacing: -0.03em;
    margin: 18px 0 0;
    text-wrap: balance;
}

.wizard-sub {
    font-size: 15px;
    line-height: 1.5;
    color: var(--mud-palette-text-secondary);
    margin: 8px 0 0;
}

/* Pinned to the bottom so the primary action sits under the thumb on a phone, and never
   scrolls out of reach on a short window. */
.wizard-actions {
    flex-shrink: 0;
    padding-top: 24px;
}

.invite-rows { display: flex; flex-direction: column; gap: 10px; }

.invite-row {
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 12px;
    padding: 6px 14px 10px;
    background: var(--mud-palette-surface);
}

.invite-add {
    border-style: dashed !important;
    border-radius: 12px !important;
    padding: 14px !important;
}

/* The "degrade honestly" panel. Warning-toned rather than error-toned: nothing has gone
   wrong, something is merely deferred. */
.plan-gate {
    margin-top: 20px;
    border-radius: 12px;
    padding: 15px 16px;
    display: flex;
    flex-direction: column;
    gap: 9px;
    background: var(--mud-palette-warning-hover);
    border: 1px solid var(--mud-palette-warning-lighten);
    color: var(--mud-palette-warning-darken);
}

.plan-gate-head {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    font-weight: 700;
}

.plan-gate p {
    margin: 0;
    font-size: 13px;
    line-height: 1.5;
}

.plan-gate-cta { align-self: flex-start; border-radius: 18px !important; }

/* Back chevron and the bar it sits beside. The spacer keeps the bars from shifting
   between a step that can go back and one that cannot. */
.wizard-progress-track {
    display: flex;
    gap: 5px;
    flex-grow: 1;
}

/* 44px minimum, per ui/design-principles.md. A component's default size is not a
   guarantee, and this one shipped at ~30px until it was measured. The negative margin
   keeps the icon optically aligned with the content below without shrinking the target. */
.wizard-back {
    min-width: 44px;
    min-height: 44px;
    margin-left: -10px;
}

.wizard-back-spacer {
    width: 34px;
    flex-shrink: 0;
}

/* ---------------------------------------------------------------------------
   Direction C — the app shell, and the shapes the MudBlazor theme has no
   opinion about. Every colour here is a theme token: a hex written into a
   component is a colour that cannot follow the theme into dark mode.
   --------------------------------------------------------------------------- */

.app-shell {
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
    background: var(--mud-palette-background);
}

.app-main {
    flex: 1;
    min-height: 0;

    /* Room for the tab bar, plus the phone's own home indicator. Without this the last row
       of every list sits underneath the bar and cannot be tapped. */
    padding-bottom: calc(66px + env(safe-area-inset-bottom));
}

/*
    Signed out: the wordmark and the page, nothing else.

    The reason is continuity as much as it is access. Before the app boots, index.html shows a
    full-bleed brand screen; the moment Blazor starts, a framed column with a tab bar appeared
    around the same spinner and the transition read as two different applications. Bare, the
    sign-in screens sit where the boot screen just was.
*/
/* No tab bar to reserve room for, so the wait can use the height it was designed for. */
.app-main--bare {
    padding-bottom: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

/* LoadingState caps itself at 60dvh for a page that has chrome above and below it. With
   neither, it should fill the screen the way the boot screen does. */
.app-main--bare .loading-state {
    min-height: min(78dvh, 640px);
}

.app-wordmark {
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 20px;
    font-weight: 700;
    letter-spacing: -0.028em;
    text-decoration: none;
}

.app-wordmark .m { color: var(--mud-palette-primary); }
.app-wordmark .z { color: var(--mud-palette-success); }

/* ---- top bar: md+ only ---- */

.app-topbar { display: none; }

.app-topbar__inner {
    display: flex;
    align-items: center;
    gap: 22px;
    height: 60px;
    padding: 0 24px;
    background: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.app-topbar__nav {
    display: flex;
    align-items: center;
    gap: 22px;
}

.app-topbar__link {
    font-size: 13px;
    font-weight: 500;
    color: var(--mud-palette-text-secondary);
    text-decoration: none;
    padding-bottom: 2px;
    border-bottom: 2px solid transparent;
}

.app-topbar__link:hover { color: var(--mud-palette-text-primary); }

/*
    A menu link carries a chevron, and an inline SVG sits on the text baseline - which leaves it
    riding high above the middle of the word. Centring it is what the flex row is for; the gap
    replaces the whitespace that used to separate them.
*/
.app-topbar__link--menu {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    cursor: pointer;
}

/* Weight and an underline as well as colour, so the active tab is not a colour-only
   signal (ui/design-principles.md). */
.app-topbar__link.is-active {
    color: var(--mud-palette-primary);
    font-weight: 600;
    border-bottom-color: var(--mud-palette-primary);
}

.app-topbar__actions {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 4px;

    /* The avatars sit beside Surprise Me, never wrapped underneath it. */
    flex-wrap: nowrap;
    flex-shrink: 0;
}

/*
    SURPRISE ME STANDS APART FROM THE AVATARS.

    The 4px gap above is right for the avatars, which are a cluster and read as one group. It was
    wrong between the button and that cluster: the pill is filled purple and the account avatar is
    filled purple, so four pixels apart they ran together into one shape and the button stopped
    looking like a button.

    The gap cannot just be widened - that would pull the avatars apart from each other too, and
    they are meant to overlap.
*/
.app-topbar__surprise {
    margin-right: 14px;
}

.app-topbar__nav { flex-shrink: 0; }

/* ---------------------------------------------------------------------------
   The catalog: ingredients and tags.

   One layout for both, because they are the same shape - a bounded screen with a header that
   stays put and a list that scrolls under it. Ingredients.dc.html and Tags.dc.html differ in
   what a row says, not in how the screen works.
   --------------------------------------------------------------------------- */

.catalog {
    display: flex;
    flex-direction: column;

    /* Bounded so the list scrolls under the header rather than taking it up the screen. The tab
       bar's height comes off the top, since it is still on screen while the list is. */
    height: calc(100dvh - 66px - env(safe-area-inset-bottom));
    overflow: hidden;
}

.catalog__header {
    flex-shrink: 0;
    padding: 8px 16px 12px;
    background: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.catalog__title { display: flex; align-items: baseline; gap: 9px; }

.catalog__name {
    margin: 0;
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 25px;
    font-weight: 600;
    letter-spacing: -0.025em;
}

.catalog__count { font-size: 15px; color: var(--mud-palette-text-disabled); }

.catalog__blurb {
    margin: 3px 0 0;
    font-size: 12px;
    line-height: 1.5;
    color: var(--mud-palette-text-secondary);
}

.catalog__tools {
    display: flex;
    align-items: center;
    gap: 9px;
    margin-top: 14px;
}

.catalog__search { flex: 1; }

.catalog__add.mud-button-root {
    /* Square against the search field, and on the 44px floor like everything tappable. */
    min-width: 44px;
    width: 44px;
    height: 44px;
    border-radius: 12px;
}

.catalog__new { flex: 1; min-height: 44px; }

.catalog__filters {
    display: flex;
    gap: 7px;
    margin-top: 12px;

    /* Sideways rather than wrapping into a second row that pushes the list down the screen. */
    overflow-x: auto;
    scrollbar-width: none;
}

.catalog__filters::-webkit-scrollbar { display: none; }

.catalog__results {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
}

.catalog__list { margin-top: 10px; padding: 0; list-style: none; }

/*
    The A-Z headers. Sticky, because scanning for a near-duplicate means knowing which letter you
    are in without scrolling back to find out.
*/
.catalog__letter {
    position: sticky;
    top: 0;
    z-index: 1;

    padding: 7px 16px 4px;
    background: var(--mud-palette-background);
    font-size: 12px;
    font-weight: 600;
    color: var(--mud-palette-text-secondary);
}

.catalog__note {
    margin: 14px 16px 24px;
    padding: 13px 15px;
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 12px;
    font-size: 12px;
    line-height: 1.5;
    color: var(--mud-palette-text-secondary);
}

.catalog-row {
    display: flex;
    align-items: center;
    gap: 12px;

    min-height: 56px;
    padding: 10px 12px 10px 16px;
    background: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

/* Quieter, not hidden: an unused row is the one most likely to be tidied away. */
.catalog-row--unused .catalog-row__name { color: var(--mud-palette-text-secondary); }

.catalog-row__tile {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 36px;
    height: 36px;
    border-radius: 10px;
    background: color-mix(in srgb, var(--mud-palette-primary) 13%, transparent);
    color: var(--mud-palette-primary);
}

.catalog-row--unused .catalog-row__tile {
    background: var(--mud-palette-background-gray);
    color: var(--mud-palette-text-disabled);
}

.catalog-row__body { flex: 1; min-width: 0; }

/* ================= board settings =================
   Built on the catalog shape - header, list of rows, RowActions - because that is what the
   screen is. Only the parts the catalog has no equivalent for live here. */

/* Sections rather than one long list: the name, the people and the delete are three different
   kinds of decision, and running them together is how somebody deletes a board while looking
   for the rename. */
.board-section {
    padding: 0 0 4px;
}

/*
    Tighter than the shared 26px top margin. Three sections on one screen at that rhythm leaves
    gaps wide enough to read as the end of the page, which is how somebody scrolls past the
    member list without realising there is anything below it.
*/
.board-section .section-label {
    padding: 0 16px;
    margin: 18px 0 4px;
}

.board-rename {
    display: flex;

    /* CENTRED on the field. It was flex-start with a 3px nudge on the button, tuned when the
       input was 40px tall; the input is 44 and the button 33, so the nudge left Save sitting
       visibly high against the box it belongs to. Centring needs no number and survives the
       field growing a line of validation text. */
    align-items: center;
    gap: 10px;
    padding: 4px 16px 0;
}

.board-rename .mud-input-control { flex: 1; }

/* MudBlazor's dense margin is sized for a stacked form. Here the field is one row in a section
   that has its own spacing, and keeping both puts a hole between the name and the member list. */
.board-rename .mud-input-control.mud-input-control-margin-dense { margin-bottom: 0; }

/* Nothing to nudge: align-items does it, at whatever height the field turns out to be. */
.board-rename .mud-button-root { flex: 0 0 auto; }

.board-readonly {
    margin: 0;
    padding: 2px 16px 0;
    font-size: 15px;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
}

.board-readonly__why {
    display: block;
    margin-top: 2px;
    font-size: 12px;
    font-weight: 400;
    color: var(--mud-palette-text-secondary);
}

/* Initials, not an icon: the tile is standing in for a person, and every other place in the app
   that stands in for a person does it this way (PersonDisplay.Initials). */
.catalog-row__tile.board-avatar {
    border-radius: 50%;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* Marks your own row so the actions on it read as yours. A label rather than a colour, because
   colour alone carries no meaning for about one man in twelve. */
.board-you {
    margin-left: 6px;
    padding: 1px 7px;
    border-radius: 9px;
    background: var(--mud-palette-background-gray);
    font-size: 11px;
    font-weight: 600;
    color: var(--mud-palette-text-secondary);
    vertical-align: middle;
}

/*
    The delete section. Set apart by a rule and by distance rather than by a red panel: a screen
    that is alarming the whole time it is open stops being alarming, and this is a control
    somebody has to hunt for deliberately, not one that should shout at them while they rename
    the board.
*/
.board-section--danger {
    margin-top: 22px;
    padding-top: 4px;
    border-top: 1px solid var(--mud-palette-lines-default);
}

.board-danger__body {
    margin: 2px 16px 12px;
    max-width: 60ch;
    font-size: 13px;
    line-height: 1.55;
    color: var(--mud-palette-text-secondary);
}

.board-section--danger .mud-button-root { margin-left: 16px; }


.catalog-row__name { display: block; font-size: 15px; font-weight: 500; }

.catalog-row__meta {
    display: block;
    margin-top: 2px;
    font-size: 12px;
    color: var(--mud-palette-text-disabled);
}

.catalog-row__areas { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }

/*
    What a tag applies to. Each carries an icon and a word as well as a colour, so the three
    states still separate under red-green colour blindness.
*/
.area {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 9px;
    border-radius: 11px;
    font-size: 11px;
    font-weight: 600;
}

.area .mud-icon-root { font-size: 13px; width: 13px; height: 13px; }

/*
    The label is mixed toward the text colour rather than being the accent itself.

    The accent on its own tint measured 2.76:1 for restaurants - below the 4.5:1 AA needs for
    text this size - and no amount of lightening the tint could fix it: #4E9E6C is only 3.27:1
    on pure WHITE, so the tint was never the problem. The ink had to darken.

    Mixed toward --mud-palette-text-primary rather than a fixed hex because that token flips with
    the theme: the same rule darkens the label on light and lightens it on dark, with no
    dark-mode selector to keep in step. MudBlazor swaps palette VALUES rather than exposing a
    theme class, so a hardcoded pair could not have been written here anyway.

    Measured at 60%: restaurants 4.84 light / 6.21 dark, recipes 6.94 light / 6.40 dark.
*/
.area--recipes {
    background: color-mix(in srgb, var(--mud-palette-primary) 14%, transparent);
    color: color-mix(in srgb, var(--mud-palette-primary) 60%, var(--mud-palette-text-primary));
}

.area--restaurants {
    background: color-mix(in srgb, var(--mud-palette-success) 16%, transparent);
    color: color-mix(in srgb, var(--mud-palette-success) 60%, var(--mud-palette-text-primary));
}

.area--off {
    background: var(--mud-palette-background-gray);
    color: var(--mud-palette-text-disabled);
    font-weight: 500;
}

/* ---- input density ---- */

/*
    Dense inputs, but never below the hit-target floor.

    Margin.Dense is the right density for this app - it keeps a form short enough to take in at
    once - but MudBlazor implements it by shrinking the control to 34px, and
    design-principles.md principle 2 sets 44px as the minimum hit target. Density should come
    out of the padding, not out of the target.

    It was also visible without measuring anything: the dense "Total time" field sat beside the
    44px Serves stepper in the same row, ten pixels shorter than the thing next to it.

    Multi-line fields already exceed this, so the floor does not touch them.
*/
.mud-input-control .mud-input {
    min-height: 44px;
}

/* ---- row actions: the row is the tap target, with visible buttons at md+ ---- */

.row-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 1;
    min-width: 0;
}

/*
    The row itself, as a real button. No kebab: RowActions.dc.html sanctions the sheet, the
    swipe and visible buttons at md+, and a per-row menu is none of those - it is a fourth
    mechanism that has to be learned separately.
*/
.row-actions__tap {
    display: flex;
    align-items: center;
    gap: 12px;

    flex: 1;
    min-width: 0;
    padding: 0;

    font: inherit;
    color: inherit;
    text-align: left;
    background: none;
    border: 0;
    cursor: pointer;
}

/* Hover changes emphasis, never existence - so these are always rendered at md+. */
.row-actions__inline { display: none; }

.row-actions__button--destructive .mud-icon-root { color: var(--mud-palette-error); }

@media (min-width: 960px) {
    .row-actions__inline { display: flex; gap: 2px; }

    /* Emphasis on hover, not existence. */
    .row-actions__button { opacity: 0.72; }
    .catalog-row:hover .row-actions__button,
    .row-actions__button:focus-visible { opacity: 1; }
}

/* ---- looking a restaurant up ---- */

.rest-form__lookup { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: -4px 0 4px; }

/*
    An allowance that has run out, or a search that found nothing. Said quietly beside the
    button: the form works without any of this, and always did.
*/
.rest-form__lookup-note { font-size: 13px; color: var(--mud-palette-text-secondary); }

.rest-form__found { list-style: none; margin: 0 0 8px; padding: 0; display: flex; flex-direction: column; gap: 4px; }

/*
    Offered rather than filled in. "Taqueria" matches four places in one town, and choosing for
    somebody is how a form ends up with the wrong address in it.
*/
.rest-form__place {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 9px 12px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 10px;
    background: var(--mud-palette-surface);
    text-align: left;
    cursor: pointer;
}

.rest-form__place:hover { background: var(--mud-palette-action-default-hover); }

.rest-form__place-name { font-size: 15px; font-weight: 500; }

.rest-form__place-where { font-size: 13px; color: var(--mud-palette-text-secondary); }

/* ---- plan and billing ---- */

/*
    The scrolling half needs a gutter here, unlike the catalogs: their rows run edge to edge by
    design, and these are cards that would otherwise touch the screen.
*/
.billing__body { padding: 0 16px 20px; }

/*
    ONE COLUMN, HEADER INCLUDED.

    Capping only the cards left the title hard against the left edge on a desktop while the
    ladder sat in the middle - two pages sharing a screen. The measure is the same rule that
    caps the cards: past roughly 70 characters the eye loses the line.
*/
.billing .catalog__header,
.billing .billing__body {
    max-width: 38rem;
    margin-inline: auto;
    width: 100%;
}

.billing__plan-price--none { font-size: 14px; color: var(--mud-palette-text-secondary); }

.billing__standing {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-bottom: 18px;
}

.billing__now { font-size: 16px; }

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

/*
    A failed payment. NOT an alarm: Stripe retries for a fortnight and the household keeps
    working throughout, so this is a thing to fix rather than a countdown. The icon and the
    sentence carry it; the colour is a tint, not a siren.
*/
.billing__warning {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 10px 12px;
    border-radius: 10px;
    background: color-mix(in srgb, var(--mud-palette-warning) 12%, transparent);
    font-size: 14px;
    line-height: 1.45;
}

.billing__warning .mud-icon-root { color: var(--mud-palette-warning); flex-shrink: 0; margin-top: 2px; }

.billing__cycle { display: flex; justify-content: center; margin-bottom: 18px; }

/*
    A LADDER, NOT A CARD RACK.

    Three cards of equal weight was the SaaS-pricing default, and it did the one thing this
    screen must not: it made comparison the reader's job. Every line carried the same purple
    tick - including Free's, where a tick beside "50 recipes" frames a ceiling as a benefit -
    so the marks encoded nothing and the numbers had to be held in the head between cards.

    One column, capped. The cap is the typographic rule rather than taste: past ~70 characters
    the eye loses the line, and a pricing card stretched across a desktop is a card nobody
    finishes reading.
*/
.billing__plans {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

.billing__plan {
    display: flex;
    flex-direction: column;
    gap: 14px;
    padding: 18px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 14px;
    background: var(--mud-palette-surface);
}

/* Name left, price right, sharing one baseline so they read as a single line. */
.billing__plan-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
}

.billing__plan-name {
    margin: 0;
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 21px;
    font-weight: 600;
    letter-spacing: -0.02em;
}

.billing__plan-badge {
    padding: 2px 8px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--mud-palette-primary) 14%, transparent);
    color: var(--mud-palette-primary);
    font-size: 11px;
    font-weight: 600;
}

/*
    The price is not the interesting number on this screen, so it is not the biggest. What
    changes is the argument; what it costs is the footnote to it.
*/
.billing__plan-price { margin: 0; display: flex; align-items: baseline; gap: 4px; }

.billing__plan-price .numeral {
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 20px;
    font-weight: 600;
    letter-spacing: -0.02em;
}

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

/*
    WHAT YOU WOULD GAIN. A plus, not a tick: it means added, and the arrow below means became.
    Two marks for two meanings beats one mark on everything.
*/
.billing__unlocks {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 9px;
}

.billing__unlocks li {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    font-size: 15px;
    line-height: 1.45;
}

.billing__unlocks .mud-icon-root {
    color: var(--mud-palette-primary);
    flex-shrink: 0;
    margin-top: 2px;
}

/*
    THE ONE BOLD THING.

    Both ends of every jump, with the new value right-aligned so the numbers stack into a
    column that can be read straight down. The old figure stays because the size of the step
    IS the argument - "2,000 recipes" is a fact, "200 -> 2,000" is a reason.

    No colour on the gain. Type scale carries it, which leaves the single accent colour free
    for the thing that is actually actionable: the button.
*/
.billing__deltas {
    margin: 0;
    padding-top: 2px;
    border-top: 1px solid var(--mud-palette-divider-light);
    display: flex;
    flex-direction: column;
}

.billing__delta {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: baseline;
    gap: 12px;
    padding: 9px 0;
    border-bottom: 1px solid var(--mud-palette-divider-light);
}

.billing__delta:last-child { border-bottom: 0; }

.billing__delta dt {
    font-size: 14px;
    color: var(--mud-palette-text-secondary);
}

.billing__delta dd {
    margin: 0;
    display: flex;
    align-items: baseline;
    gap: 7px;
    white-space: nowrap;
}

.billing__delta-from {
    font-size: 14px;
    color: var(--mud-palette-text-disabled);
}

.billing__delta-arrow {
    font-size: 13px;
    color: var(--mud-palette-text-disabled);
}

.billing__delta-to {
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 19px;
    font-weight: 600;
    letter-spacing: -0.02em;
}

.billing__plan-points {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 7px;
}

.billing__plan-points li { display: flex; align-items: flex-start; gap: 7px; font-size: 14px; line-height: 1.4; }

.billing__plan-points .mud-icon-root { color: var(--mud-palette-primary); flex-shrink: 0; margin-top: 1px; }

.billing__plan-action { align-self: flex-start; }

/*
    THE PLAN THEY HOLD. A line, not a card - it argues for nothing, and the header has already
    said what it is. Keeping it on the screen answers the other half of why somebody opens this
    page: what am I actually paying for.

    Bordered AND badged AND tinted: colour never carries meaning alone (design-principles.md).
*/
.billing__plan--held {
    flex-direction: row;
    align-items: center;
    gap: 10px;
    padding: 12px 16px;
}

/*
    Only the one they HOLD is tinted and bordered. Tinting every quiet row made Free read as
    something they also had - the tint is the whole signal for "this is yours", so spending it
    on a plan they are past says the opposite of what it means.
*/
.billing__plan--held.is-current {
    background: color-mix(in srgb, var(--mud-palette-primary) 6%, var(--mud-palette-surface));
    border-color: var(--mud-palette-primary);
}

.billing__plan--held .billing__plan-name { font-size: 17px; }

.billing__plan-price--held { margin-left: auto; }

.billing__plan-price--held .numeral { font-size: 16px; }

.billing__manage { display: flex; justify-content: center; margin-top: 18px; }

/*
    Said to a screen reader, shown to nobody. The delta rows read "200 -> 2,000" visually; the
    arrow is decorative, so the meaning has to exist in the text for anyone not seeing it.
*/
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}


/* ---- a published recipe, read before copying ---- */

.pubrec__facts { display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 14px; }

/*
    Carried with the copy because it changes what the grocery list does. Stated rather than
    tinted: a household that misses this ends up shopping for food that arrives in a box.
*/
.pubrec__kit {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    margin-bottom: 16px;
    padding: 10px 12px;
    border-radius: 10px;
    background: var(--mud-palette-background-gray);
    font-size: 14px;
}

.pubrec__kit .mud-icon-root { color: var(--mud-palette-primary); flex-shrink: 0; }

.pubrec__heading {
    margin: 18px 0 8px;
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 17px;
    font-weight: 600;
    letter-spacing: -0.015em;
}

.pubrec__list { list-style: none; margin: 0; padding: 0; }

/*
    Amount first in its own column, the way the recipe page sets an ingredient line out - a
    reader scanning for "have I got 2 lb of beef" reads down the numbers, not the names.
*/
.pubrec__line {
    display: grid;
    grid-template-columns: 84px minmax(0, 1fr);
    gap: 10px;
    padding: 7px 0;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    font-size: 15px;
}

.pubrec__amount { color: var(--mud-palette-primary); font-weight: 600; }

.pubrec__steps { margin: 0; padding-left: 20px; font-size: 15px; line-height: 1.55; }

.pubrec__steps li { margin-bottom: 8px; }

/* ---- rating a recipe ---- */

/*
    ASKED, not just displayed. The rating exists so a household can tell itself what worked, and
    the moment to say so is after cooking - which is where this page is read from. It sits under
    the meta row rather than in it, because a control and a fact should not look alike.
*/
.recipe-rate {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 14px;
}

.recipe-rate__ask { font-size: 14px; color: var(--mud-palette-text-secondary); }

/* The household count beside the average: "4.5 from 6". Quiet - the number leads. */
.recipe-meta__sub { color: var(--mud-palette-text-disabled); font-size: 12px; margin-left: 2px; }

/*
    The note, under the stars it belongs to. Full width because a sentence about why a recipe
    did not work is longer than a row of stars, and the Save button only exists while there is
    something unsaved to press it for.
*/
.recipe-rate__note { display: flex; align-items: flex-start; gap: 8px; margin-top: 10px; }

.recipe-rate__note-input { flex: 1; min-width: 0; }

.recipe-rate__save { flex-shrink: 0; margin-top: 4px; }

/*
    What everybody else said. NAMES, never a count - the same rule the weekly check-in follows.
    Quoted as a list rather than a thread: nobody is replying to anybody, they are each saying
    one thing about one recipe.
*/
.recipe-reviews {
    list-style: none;
    margin: 16px 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.recipe-review {
    padding: 11px 13px;
    border-radius: 10px;
    background: var(--mud-palette-background-gray);
}

.recipe-review__who {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 14px;
}

.recipe-review__stars { display: inline-flex; color: var(--mud-palette-primary); }

.recipe-review__stars .mud-icon-root { font-size: 14px; width: 14px; height: 14px; }

.recipe-review__text {
    display: block;
    margin-top: 3px;
    font-size: 14px;
    line-height: 1.45;
    color: var(--mud-palette-text-secondary);
}

/* ---- merging two spellings into one ---- */

.imerge__why { margin: 0 0 16px; color: var(--mud-palette-text-secondary); font-size: 14px; }

.imerge__into { width: 100%; }

/*
    What is about to happen, in numbers. Not a warning colour: this is a deliberate tidy-up, and
    painting it as danger would make people hesitate over the thing they came to do. The weight
    comes from the count and the word "undone", which is the part worth reading twice.
*/
.imerge__what {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    margin-top: 16px;
    padding: 11px 13px;
    border-radius: 10px;
    background: var(--mud-palette-background-gray);
    font-size: 14px;
    line-height: 1.45;
}

.imerge__what .mud-icon-root { color: var(--mud-palette-primary); flex-shrink: 0; margin-top: 2px; }

/* ---- IngredientPicker: the catalog first, adding last ---- */

.ipick__match,
.ipick__new {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
}

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

.ipick__name {
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/*
    The usage line is what makes the catalog match the obvious choice rather than a coincidence:
    "used in 4 recipes, usually teaspoons" is the evidence that this is the same thing.
*/
.ipick__meta { font-size: 12px; color: var(--mud-palette-text-secondary); }

/* Adding is available, never the first thing the eye lands on. */
.ipick__new .mud-icon-root { color: var(--mud-palette-primary); }

/*
    Swipe-left: the accelerator, per RowActions.dc.html. The sheet is the mechanism and these
    panels duplicate it, so nothing here is reachable by keyboard or screen reader on purpose -
    see the component for why that is deliberate rather than an oversight.
*/
.row-actions__swipe {
    position: relative;
    flex: 1;
    min-width: 0;

    /* Clips the panels until the row slides off them. */
    overflow: hidden;

    /*
        pan-y hands vertical scrolling back to the page while keeping horizontal for us. Without
        it the browser claims the gesture first and the row never moves; with `none` the list
        stops scrolling wherever a finger happens to land on a row.
    */
    touch-action: pan-y;
}

.row-actions__reveal {
    position: absolute;
    inset: 0 0 0 auto;
    display: flex;
    align-items: stretch;
}

.row-actions__reveal-action {
    /* 54px matches PanelWidth in RowActions.razor; the two must agree or the row snaps wrong. */
    width: 54px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;

    padding: 0;
    border: 0;
    cursor: pointer;

    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
}

.row-actions__reveal-action--destructive {
    background: var(--mud-palette-error);
    color: var(--mud-palette-error-text);
}

.row-actions__reveal-action .mud-icon-root { color: inherit; }

.row-actions__reveal-label {
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.01em;
}

/*
    The row rides over the panels, so it needs its own opaque ground - a transparent surface
    shows the action colours through the text as it moves.
*/
.row-actions__surface {
    position: relative;
    display: flex;
    align-items: center;

    /* Narrows to reveal the panels beside it, rather than sliding over them. */
    width: 100%;
    min-width: 0;
    background: var(--mud-palette-surface);
    transition: width 0.18s ease-out;
}

/* While a finger is down the transform follows it directly; easing that would lag the thumb. */
@media (prefers-reduced-motion: reduce) {
    .row-actions__surface { transition: none; }
}

@media (min-width: 960px) {
    /*
        At md+ the same actions are visible buttons that are always there. A second, hidden copy
        of them would be a third mechanism - and a touch-screen laptop would find it.
    */
    .row-actions__reveal { display: none; }
    .row-actions__surface { width: 100% !important; }
    .row-actions__swipe { touch-action: auto; }
}

/* ---- cook mode ---- */

/*
    The one screen that ignores the theme.

    Cook.dc.html and CookTablet.dc.html are both drawn dark, and the reason is the room rather
    than the taste: a white screen at full brightness, propped against a mixing bowl in a dim
    kitchen, is unpleasant to stand next to for forty minutes. So these are literal hex values
    rather than --mud-palette-*, which flip with the appearance setting - this is the deliberate
    exception to "never hardcode colours", and the values are the app's own dark palette.

    Fixed and covering everything, because full screen is the point: no tab bar to fat-finger
    with wet hands, no top bar competing with the step.
*/
.cook {
    position: fixed;
    inset: 0;

    /*
        ABOVE the tab bar, which is 1200.
        
        Both were 1200, and on a tie the later element in DOM order paints on top - the tab bar
        renders after @Body in MainLayout, so it won every time. Cook mode is a full-screen
        takeover of the app's own chrome, so "equal" was never the intent; it just looked fine on
        a desktop, where there is no tab bar to lose to.

        Below MudBlazor's dialog (1400) and appbar (1300) on purpose: cook mode must not cover a
        dialog it opened itself.
    */
    z-index: 1250;

    display: flex;
    flex-direction: column;

    background: #262429;
    color: #F5F3F7;

    /* The phone's own chrome, since this covers the app's. */
    padding: env(safe-area-inset-top) env(safe-area-inset-right)
             env(safe-area-inset-bottom) env(safe-area-inset-left);
}

/* Loading, error and empty share the frame but none of the layout. */
.cook--plain {
    justify-content: center;
    padding: 24px;
}

.cook__bar {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 16px 10px;
}

.cook__close {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;

    /* 44px, like every other target - and this one gets pressed with a knuckle. */
    width: 44px;
    height: 44px;
    border-radius: 50%;

    color: inherit;
    background: none;
    border: 1px solid #45424B;
    cursor: pointer;
}

.cook__heading { flex: 1; min-width: 0; }

.cook__title {
    margin: 0;
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 19px;
    font-weight: 700;
    letter-spacing: -0.02em;
    line-height: 1.2;
}

.cook__progress-text {
    margin: 2px 0 0;
    font-size: 13px;
    color: #A5A1AC;
}

.cook__awake {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: 6px;

    padding: 7px 13px;
    border-radius: 18px;
    background: #33303A;

    /* Emerald, lightened for this ground - the same value the dark theme uses. */
    color: #78D79B;
    font-size: 13px;
    font-weight: 600;
}

/* One segment per step: "how many left" is the question, not "what percentage". */
.cook__progress {
    display: flex;
    gap: 6px;
    padding: 4px 16px 0;
}

.cook__segment {
    flex: 1;
    height: 4px;
    border-radius: 2px;
    background: #45424B;
}

.cook__segment.is-done { background: #78D79B; }

/*
    The band above the step, drawn whether or not there is a photo.

    It was briefly hidden when empty - an iPad was spending a quarter of its height on a
    placeholder - but the band earns its place: it separates cook mode from the recipe page and
    gives the eye somewhere to land between the progress bar and the step. The step being too
    large for its column was the real complaint, and that is fixed where it was caused.
*/
.cook-hero {
    position: relative;
    height: 190px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    margin-top:20px;
    background: color-mix(in srgb, var(--mud-palette-primary) 13%, transparent);
    color: color-mix(in srgb, var(--mud-palette-primary) 55%, transparent);
}

.cook-hero-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* ---------- cook timers, against CookTimers.dc.html ---------- */

/*
    THE RAIL. Below the step and above the actions, which was decided by drawing it both ways:
    above pushes the step 40% down the screen and puts countdowns in front of the sentence being
    read from across a kitchen.

    Always present, even empty. That is what makes the "+ Timer" reachable for something the
    recipe never mentioned, and it means the row never appears from nowhere - so a finishing
    timer cannot shift the Next button under somebody's thumb.
*/
.ctrail {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 12px 20px;
    overflow-x: auto;
}

/*
    They keep their size and the rail scrolls, rather than three of them squeezing into 390px
    until "Done - tap" wraps onto three lines. That is what overflow-x on the rail is FOR, and
    flex: 1 was quietly cancelling it.
*/
.ctrail__timer {
    display: flex;
    align-items: center;
    gap: 10px;
    flex: 0 0 auto;
    max-width: 62%;
    padding: 9px 11px;
    border: 0;
    border-radius: 12px;
    background: #33303A;
    color: #F5F3F7;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

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

.ctrail__big {
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 19px;
    font-weight: 600;
    letter-spacing: -0.02em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ctrail__sub {
    font-size: 11px;
    color: #A5A1AC;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/*
    FINISHED, and never by colour alone (design-principles.md). The ground fills, the icon
    becomes a bell and the word "Done" appears - three signals, because this is the one moment in
    the app where missing one burns dinner and roughly 1 in 12 men cannot rely on the first.
*/
.ctrail__timer--done { background: var(--mud-palette-primary); }
.ctrail__timer--done .ctrail__sub { color: #E0D4F5; }

/* Quiet: it is the way in, not the point of the row. */
.ctrail__add {
    flex-shrink: 0;
    border-color: #45424B !important;
    color: #A5A1AC !important;
}

.ctrail__mute { flex-shrink: 0; color: #A5A1AC; }

/*
    THE OFFER, on a step that names a duration - where Cook.dc.html draws it. Unstarted, with the
    number already in it, and gone from here the moment it starts running on the rail.
*/
.cook__timer {
    display: flex;
    align-items: center;
    gap: 13px;
    width: 100%;
    padding: 14px 16px;
    border: 0;
    border-radius: 14px;
    background: #33303A;
    color: #F5F3F7;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.cook__timer:disabled { cursor: default; opacity: 0.55; }

.cook__timer-play {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 46px;
    width: 46px;
    flex-shrink: 0;
    border-radius: 23px;
    background: var(--mud-palette-primary);
    color: #fff;
}

.cook__timer-body { display: flex; flex-direction: column; min-width: 0; }

.cook__timer-num {
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 25px;
    font-weight: 600;
    letter-spacing: -0.02em;
}

.cook__timer-sub { font-size: 12px; color: #A5A1AC; }

.cook__body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 18px 16px 8px;
}

/*
    Read at arm's length, so it is the largest type in the product by a distance.

    MEASURED AGAINST ITS OWN COLUMN, not the viewport. It was clamp(26px, 7vw, 34px), and the
    comment beside it claimed that kept the step "sane on a small phone without capping it on a
    propped-up tablet". The reasoning missed that the SAME breakpoint which widens the screen
    also narrows this column: at 768px up the body becomes two columns and the step gets 1.6 of
    2.6 of what is left after the padding and the gap.

    So an iPad in portrait was the worst case in the product rather than the best. At 820px the
    step column is about 450px - a quarter wider than a phone's - while 7vw resolves past the cap
    and the type sits at its maximum 34px. Reported from use as the directions being too big for
    the space, and it is exactly that: the largest type in the smallest column it is ever given.

    Below 768px the viewport IS this column, so the original rule is correct there and is left
    exactly as it was - the fix is scoped to the widths that actually broke, in the media query
    that breaks them. Above it the step is measured in cqi, the inline size of .cook__main.
*/
.cook__main { container-type: inline-size; }

.cook__step {
    margin: 0;
    font-size: clamp(26px, 7vw, 34px);
    font-weight: 500;
    line-height: 1.3;
    letter-spacing: -0.01em;
}

/* Tablet only - a phone needs the height for the step itself. */
.cook__next { display: none; }

/* Mixed case, as the artboards draw it - not a tracked-out capital eyebrow. */
.cook__ing-label,
.cook__next-label {
    margin: 22px 0 8px;
    font-size: 12px;
    font-weight: 700;
    color: #A5A1AC;
}

.cook__ing-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.cook__ing {
    display: flex;
    gap: 16px;
    padding: 7px 0;
    font-size: 17px;
}

.cook__amount {
    flex-shrink: 0;
    min-width: 84px;
    font-weight: 600;

    /* Lavender: the dark theme's primary, and the same colour amounts take everywhere else. */
    color: #C9B6EC;
}

/* Still on screen, just not shouting. Nothing is hidden from somebody who is cooking. */
.cook__ing--rest { color: #A5A1AC; }
.cook__ing--rest .cook__amount { color: #8E88A0; }

/* The heading belongs to tablet, where there is a column for the list to sit under. */
.cook__ing-label--rest { display: none; }

.cook__rest-summary {
    display: flex;
    align-items: center;
    gap: 10px;

    width: 100%;
    min-height: 44px;
    margin-top: 14px;
    padding: 0 14px;
    border: 0;
    border-radius: 10px;
    background: #33303A;

    color: #A5A1AC;
    font-size: 15px;
    text-align: left;
    cursor: pointer;
}

.cook__rest-count { margin-left: auto; font-weight: 600; }

/* Collapsed on a phone, where the step needs the height. */
.cook__rest-list { display: none; }
.cook__rest-list.is-open { display: block; }

.cook__foot {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px calc(14px + env(safe-area-inset-bottom));
}

.cook__back {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;

    min-width: 64px;
    min-height: 56px;
    padding: 0 18px;
    border-radius: 28px;

    color: inherit;
    background: none;
    border: 1px solid #45424B;
    font-size: 15px;
    cursor: pointer;
}

.cook__back:disabled { opacity: 0.35; cursor: default; }

.cook__back-label { display: none; }

/*
    The one big target. 56px tall and most of the width, because it is pressed repeatedly by
    somebody not looking closely and often not with a fingertip.
*/
.cook__next-button {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;

    min-height: 56px;
    border: 0;
    border-radius: 28px;

    background: #7851A9;
    color: #FFFFFF;
    font-size: 18px;
    font-weight: 600;
    cursor: pointer;
}

@media (min-width: 768px) {
    /* Tablet: the step and the ingredients side by side, which is what the width buys. */
    .cook__body {
        display: grid;
        grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
        gap: 32px;
        padding: 22px 26px 10px;
    }

    .cook__main { display: flex; flex-direction: column; }

    /*
        Measured against the column rather than the screen, now that there are two columns.
        See the note on .cook__step: 7vw reads a width this element no longer has.
    */
    .cook__step { font-size: clamp(26px, 6.2cqi, 34px); }

    .cook__next {
        display: block;
        margin-top: auto;
        padding: 14px 16px;
        border: 1px solid #45424B;
        border-radius: 12px;
    }

    .cook__next-label { margin-top: 0; }

    .cook__next-text {
        margin: 0;
        font-size: 16px;
        color: #A5A1AC;
    }

    /* Room for the word, so the icon is not the only label. */
    .cook__back-label { display: inline; }

    /*
        At this width the list is simply shown: there is a column for it, so nothing has to be
        asked for. The toggle goes away and the heading takes its place.
    */
    .cook__ing-label--rest { display: block; }
    .cook__rest-summary { display: none; }
    .cook__rest-list { display: block; }
}

/* ---- the action sheet, and the catalog forms ---- */

.sheet__title {
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 17px;
    font-weight: 600;
    letter-spacing: -0.02em;
}

.sheet { display: flex; flex-direction: column; }

.sheet__action {
    display: flex;
    align-items: center;
    gap: 12px;

    /* One-handed targets, comfortably above the 44px floor. */
    min-height: 52px;
    padding: 12px 4px;

    width: 100%;
    font: inherit;
    font-size: 15px;
    text-align: left;
    color: var(--mud-palette-text-primary);
    background: none;
    border: 0;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    cursor: pointer;
}

.sheet__action:last-child { border-bottom: 0; }

.sheet__action--destructive { color: var(--mud-palette-error); }

.sheet__action:disabled {
    color: var(--mud-palette-text-disabled);
    cursor: not-allowed;
}

.sheet__label { flex-grow: 1; }

/* Why an action refuses, in its own place rather than a tooltip a phone cannot show. */
.sheet__reason { font-size: 12px; color: var(--mud-palette-text-disabled); }

.catalog-form { display: flex; flex-direction: column; gap: 14px; padding-top: 4px; }

.catalog-form__label {
    font-size: 12px;
    font-weight: 600;
    color: var(--mud-palette-text-secondary);
}

.catalog-form__areas { display: flex; flex-direction: column; gap: 2px; }

.catalog-form__hint {
    margin: -6px 0 0;
    font-size: 12px;
    line-height: 1.5;
    color: var(--mud-palette-text-disabled);
}

/*
    The near-duplicate warning, and the one about a tag losing an area. Amber rather than red:
    both are "did you mean this", not "you have broken something".
*/
.catalog-form__warn {
    padding: 12px 13px;
    border-radius: 11px;
    background: color-mix(in srgb, var(--mud-palette-warning) 13%, transparent);
    border: 1px solid color-mix(in srgb, var(--mud-palette-warning) 34%, transparent);
}

.catalog-form__warn--quiet {
    background: color-mix(in srgb, var(--mud-palette-primary) 8%, transparent);
    border-color: color-mix(in srgb, var(--mud-palette-primary) 22%, transparent);
}

.catalog-form__warn-head {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    font-weight: 600;
}

.catalog-form__warn-body {
    margin: 5px 0 0;
    font-size: 12px;
    line-height: 1.5;
    color: var(--mud-palette-text-secondary);
}

.catalog-form__warn-action { margin-top: 11px; width: 100%; min-height: 40px; }

@media (min-width: 960px) {
    /*
        The desktop sizing for .catalog lives in ONE place, at the end of this file. It used to be
        split: this block capped the header and the list at 760px while nothing centred the page
        or bounded .catalog itself, so the screen hugged the left edge of a 1440px window AND its
        sections disagreed about how wide they were - the header rule stopped 124px short of the
        one above the delete section. Measured, not guessed: 760 against 884.
    */
    .catalog__note { max-width: 68ch; }

    .catalog__new { flex: 0 0 auto; }
}

/* ---------------------------------------------------------------------------
   Writing a recipe, and importing one.

   One shell for both: a bar that stays put, a body that scrolls, and a bar on the bottom edge
   carrying the one action that commits anything. RecipeForm.dc.html and ImportReview.dc.html
   draw the same skeleton with different middles.
   --------------------------------------------------------------------------- */

.rform,
.import {
    display: flex;
    flex-direction: column;

    /* Bounded so the body scrolls under the header rather than carrying it off the screen. The
       tab bar's height comes off the top, since it is still on screen while this is. */
    height: calc(100dvh - 66px - env(safe-area-inset-bottom));
    overflow: hidden;
}

.rform__bar,
.import__bar {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 16px;
    background: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.rform__back {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    border: 1px solid var(--mud-palette-lines-default);
    background: var(--mud-palette-surface);
    color: var(--mud-palette-text-primary);
    cursor: pointer;
}

.rform__heading { flex: 1; min-width: 0; }

.rform__title {
    margin: 0;
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 21px;
    font-weight: 600;
    letter-spacing: -0.025em;
}

/* Said quietly. It is a status, not a warning - the save button is the thing that resolves it. */
.rform__dirty,
.import__sub {
    display: block;
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
}

.rform__cancel {
    flex-shrink: 0;
    min-height: 44px;
    padding: 0 6px;
    font: inherit;
    font-size: 15px;
    color: var(--mud-palette-text-secondary);
    background: none;
    border: 0;
    cursor: pointer;
}

.rform__body,
.import__recipe {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
}

.rform__head { padding: 14px 16px 4px; }

.rform__pair {
    display: flex;
    gap: 12px;
    align-items: flex-end;
    margin-top: 12px;
}

.rform__pair > * { flex: 1; }

/*
    Align the boxes you can see, not the ones you cannot.

    MudBlazor puts `margin: 8px 0 4px` on .mud-input-control to leave room for helper text. With
    `align-items: flex-end` the flex line aligns MARGIN edges, so Total time's visible border
    ended 4px above the Serves stepper beside it - close enough to look like a mistake rather
    than a gap. Zeroing the bottom margin inside this row makes both controls the same 44px box
    with the same top and bottom.

    Only the bottom: the 8px above still separates the row from the Name field, and nothing in
    this pair renders helper text under it.

    Four classes deep because MudBlazor sets this from
    `.mud-input-control.mud-input-control-margin-dense.mud-input-outlined-with-label`, which is
    three. The obvious `.rform__pair .mud-input-control` is two, loses on specificity, and loses
    SILENTLY - the rule is right there in the file doing nothing.
*/
.rform .rform__pair .mud-input-control.mud-input-control-margin-dense { margin-bottom: 0; }

.rform__label {
    display: block;
    margin-bottom: 5px;
    font-size: 12px;
    font-weight: 600;
    color: var(--mud-palette-text-secondary);
}

/* ---- the servings stepper ---- */

.stepper {
    display: flex;
    align-items: center;
    height: 44px;
    border: 1px solid var(--mud-palette-lines-inputs);
    border-radius: 10px;
    background: var(--mud-palette-surface);
    overflow: hidden;
}

.stepper__value {
    flex: 1;
    text-align: center;
    font-size: 16px;
}

.stepper__button {
    display: flex;
    align-items: center;
    justify-content: center;

    /* 44px is the floor, and a stepper is tapped repeatedly. */
    width: 44px;
    height: 44px;

    background: var(--mud-palette-background-gray);
    border: 0;
    color: var(--mud-palette-text-primary);
    cursor: pointer;
}

.stepper__button:disabled { color: var(--mud-palette-action-disabled); cursor: not-allowed; }

/* ---- the three sections: tabs on a phone, columns at lg ---- */

.rform__tabs {
    display: flex;
    gap: 3px;
    margin: 14px 16px 0;
    padding: 4px;
    background: var(--mud-palette-background-gray);
    border-radius: 13px;
}

.rform__tab {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 5px;

    min-height: 44px;
    border: 0;
    border-radius: 10px;
    background: none;
    font: inherit;
    font-size: 13px;
    font-weight: 500;
    color: var(--mud-palette-text-secondary);
    cursor: pointer;
}

/* Filled, not tinted: which section you are in changes what every control below does. */
.rform__tab.is-active {
    background: var(--mud-palette-surface);
    color: var(--mud-palette-text-primary);
    font-weight: 600;
    box-shadow: 0 1px 3px rgb(46 44 49 / 10%);
}

.rform__tab-count { color: var(--mud-palette-text-secondary); }

.rform__panes { padding: 14px 16px 24px; }

/* The phone shows one at a time; at lg every pane is shown and this stops applying. */
.rform__pane { display: none; }
.rform__pane.is-shown { display: block; }

.rform__pane-title {
    display: none;
    margin: 0 0 10px;
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 15px;
    font-weight: 600;
    letter-spacing: -0.02em;
}

.rform__pane-title .numeral { color: var(--mud-palette-text-secondary); margin-left: 5px; }

.rform__add {
    display: flex;
    align-items: center;
    gap: 8px;

    width: 100%;
    min-height: 48px;
    margin-top: 10px;
    padding: 0 14px;

    border: 1px dashed var(--mud-palette-lines-default);
    border-radius: 12px;
    background: none;
    font: inherit;
    font-size: 14px;
    text-align: left;
    color: var(--mud-palette-text-secondary);
    cursor: pointer;
}

.rform__add:hover { color: var(--mud-palette-primary); border-color: var(--mud-palette-primary); }

/* ---- steps ---- */

/* How steps are read: quiet until asked for, then a short key of what to type and what it does. */
.rform__guide-toggle.mud-button-root {
    margin: 0 0 8px -8px;
    text-transform: none;
    color: var(--mud-palette-text-secondary);
}

.rform__guide {
    display: grid;
    gap: 10px;
    margin: 0 0 14px;
    padding: 12px 14px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 12px;
    background: var(--mud-palette-surface);
}

.rform__guide-row { display: grid; gap: 2px; }

.rform__guide dt code {
    font: inherit;
    font-size: 13px;
    font-weight: 600;
    color: var(--mud-palette-primary);
}

.rform__guide dd {
    max-width: 62ch;
    margin: 0;
    font-size: 12px;
    line-height: 1.4;
    color: var(--mud-palette-text-secondary);
}

.rform__steps { margin: 0; padding: 0; list-style: none; }

.rform__step {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin-bottom: 10px;
}

/* The number is the meaning here, so it carries the display face. */
.rform__step-number {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 26px;
    height: 26px;
    margin-top: 9px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--mud-palette-primary) 13%, transparent);
    color: var(--mud-palette-primary);
    font-size: 12px;
}

.rform__step > .mud-input-control { flex: 1; }

.rform__step-remove {
    flex-shrink: 0;
    width: 36px;
    height: 36px;
    margin-top: 4px;
    border: 0;
    background: none;
    color: var(--mud-palette-text-disabled);
    cursor: pointer;
}

.rform__tags { display: flex; flex-wrap: wrap; gap: 7px; }

.rform__hint {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    color: var(--mud-palette-text-disabled);
}

/* ---- the bottom bar ---- */

.rform__foot,
.import__foot {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
    background: var(--mud-palette-surface);
    border-top: 1px solid var(--mud-palette-lines-default);
}

.rform__meter { flex: 1; min-width: 0; }

/*
    The plan ceiling where it is felt. A number that only appears as a refusal after twenty lines
    of typing is a worse experience than one that was visible the whole time.
*/
.rform__meter-track {
    height: 4px;
    margin-top: 6px;
    border-radius: 2px;
    background: var(--mud-palette-background-gray);
    overflow: hidden;
}

.rform__meter-fill {
    height: 100%;
    border-radius: 2px;
    background: var(--mud-palette-primary);
    transition: width 0.2s ease-out;
}

@media (prefers-reduced-motion: reduce) {
    .rform__meter-fill { transition: none; }
}

.rform__save { flex-shrink: 0; min-height: 46px; }

/* ---------------------------------------------------------------------------
   The ingredient line, and the autocomplete that is the point of the form.
   --------------------------------------------------------------------------- */

.iline {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 7px;
    padding: 7px 0;
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.iline__grip { flex-shrink: 0; color: var(--mud-palette-action-disabled); cursor: grab; }

/*
    MUDBLAZOR CONTROLS NOW, so what was here has gone: the border, the radius, the background,
    the focus ring, and the 16px that stops a phone zooming on focus - all of which the theme
    supplies once (see MealAzTheme's Subtitle1, which is what MudBlazor renders input text at).

    The whole autocomplete menu went too. It was a hand-rolled list of buttons duplicating what
    the import review did differently and worse; it is IngredientPicker for both now, styled by
    .ipick further up.

    What is left is only the row's own geometry and the one thing a shared control cannot know:
    that the quantity is a number and should read as one.
*/
.iline__qty {
    /* Wide enough for "1 1/2" and "2 3/4". It was 58px, sized for a spinner that could only ever
       hold "0.5" - which is the shape of the problem, not just its width. */
    width: 88px;
    flex-shrink: 0;
}

.iline__qty input {
    text-align: center;
    color: var(--mud-palette-primary);
    font-weight: 600;
}

.iline__unit { width: 104px; flex-shrink: 0; }

/*
    THE INGREDIENT GETS ITS OWN LINE ON A PHONE, and the row it shares above it.

    These were hand-rolled boxes with no labels, and three of them fitted across 390px. They are
    MudBlazor fields now - outlined, with a floating label - and three no longer do: the
    screenshot showed "cou..." and "Ingredi...", with the most important of the three squeezed
    smallest. Wrapping is what that width can actually hold.

    Ordered rather than reordered in the markup, so the tab order still runs amount, unit, name:
    the remove button takes the space beside the two small fields, and the name wraps beneath.
*/
.iline__name { flex: 1 1 100%; min-width: 0; order: 1; }

@media (min-width: 560px) {
    /* Room for all three, so the line reads as one line again. */
    .iline__name { flex: 1 1 auto; order: 0; }
    .iline__remove { order: 0; margin-left: 0; }
}

/*
    A line that has been typed but not resolved to the catalog. It will not be saved, so it says
    so quietly while there is still time to pick something.
*/
.iline__name.is-unresolved .mud-input-outlined-border {
    border-color: color-mix(in srgb, var(--mud-palette-warning) 55%, transparent);
    border-style: dashed;
}

.iline__remove {
    flex-shrink: 0;
    order: 0;
    margin-left: auto;
    width: 36px;
    height: 36px;
    border: 0;
    background: none;
    color: var(--mud-palette-text-disabled);
    cursor: pointer;
}

/* ---------------------------------------------------------------------------
   Import.
   --------------------------------------------------------------------------- */

.import__entry {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 12px 16px;
    background: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.import__url { flex: 1; }
.import__go { flex-shrink: 0; min-height: 44px; }

/*
    Each way an import can fail gets its own card, its own wording and its own way forward. The
    link stays in the box above every one of them.
*/
.import__state {
    margin: 14px 16px;
    padding: 15px 16px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 14px;
    background: var(--mud-palette-surface);
}

.import__state--quota {
    background: color-mix(in srgb, var(--mud-palette-warning) 12%, transparent);
    border-color: color-mix(in srgb, var(--mud-palette-warning) 32%, transparent);
}

.import__state--duplicate {
    background: color-mix(in srgb, var(--mud-palette-primary) 8%, transparent);
    border-color: color-mix(in srgb, var(--mud-palette-primary) 24%, transparent);
}

.import__state-head {
    display: flex;
    align-items: center;
    gap: 8px;
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 15px;
    font-weight: 600;
    letter-spacing: -0.02em;
}

.import__state-body {
    margin: 7px 0 0;
    font-size: 13px;
    line-height: 1.55;
    color: var(--mud-palette-text-secondary);
}

.import__state-actions { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 13px; }
.import__state-actions .mud-button-root { min-height: 44px; }

.import__state-foot {
    margin: 12px 0 0;
    font-size: 12px;
    line-height: 1.5;
    color: var(--mud-palette-text-disabled);
}

.import__provenance {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 16px;
    background: var(--mud-palette-background-gray);
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
}

.import__source { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.import__quota { flex-shrink: 0; font-weight: 600; }

.import__recipe { padding: 14px 16px 24px; }

.import__facts { display: flex; gap: 9px; margin-top: 11px; }

.import__fact {
    display: flex;
    align-items: center;
    gap: 7px;
    flex: 1;
    min-height: 44px;
    padding: 0 13px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 11px;
    font-size: 14px;
}

.import__section {
    display: flex;
    align-items: baseline;
    gap: 12px;
    margin: 20px 0 4px;
}

.import__section-title {
    margin: 0;
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 14px;
    font-weight: 600;
}

.import__section-title .numeral { color: var(--mud-palette-text-secondary); }

/* Amber, an icon and a count: three signals, so it still reads without colour. */
.import__attention {
    display: flex;
    align-items: center;
    gap: 5px;
    margin-left: auto;
    font-size: 12px;
    font-weight: 600;
    color: var(--mud-palette-warning-darken);
}

.import__lines { margin: 0; padding: 0; list-style: none; }

.import-line {
    display: flex;
    gap: 12px;
    padding: 12px 0;
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.import-line--flagged {
    margin: 0 -16px;
    padding: 14px 16px;
    background: color-mix(in srgb, var(--mud-palette-warning) 11%, transparent);
    border-bottom-color: color-mix(in srgb, var(--mud-palette-warning) 26%, transparent);
}

.import-line__amount {
    flex-shrink: 0;
    width: 76px;
    font-size: 14px;
    color: var(--mud-palette-primary);
}

.import-line__mark { flex-shrink: 0; color: var(--mud-palette-warning-darken); }

.import-line__body { flex: 1; min-width: 0; }

.import-line__name { display: block; font-size: 15px; font-weight: 500; }

.import-line__status {
    display: flex;
    align-items: center;
    gap: 5px;
    margin-top: 3px;
    font-size: 12px;
}

.import-line__status--known { color: var(--mud-palette-success); }
.import-line__status--new { color: var(--mud-palette-primary); }

/* The site's own words, whenever we changed them. */
.import-line__original {
    display: block;
    margin-top: 3px;
    font-size: 12px;
    color: var(--mud-palette-text-disabled);
}

.import-line__why {
    margin: 5px 0 0;
    font-size: 13px;
    line-height: 1.5;
    color: var(--mud-palette-warning-darken);
}

.import-line__actions { display: flex; gap: 9px; margin-top: 11px; }
.import-line__actions .mud-button-root { min-height: 40px; }

.import__steps {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 16px;
    padding: 14px 15px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 12px;
    color: var(--mud-palette-text-secondary);
}

.import__steps-count { display: block; font-size: 15px; font-weight: 600; color: var(--mud-palette-text-primary); }
.import__steps-note { font-size: 12px; }

.import__foot { flex-direction: column; align-items: stretch; gap: 7px; }
.import__add { min-height: 50px; }

.import__adds {
    margin: 0;
    text-align: center;
    font-size: 12px;
    color: var(--mud-palette-text-disabled);
}

/* ---------------------------------------------------------------------------
   md+ : the three sections stop taking turns and sit beside each other.

   This is what the width actually buys - the ingredient list stays visible while the step that
   uses it is being written. Desktop adds the affordance; it adds no capability the phone lacks.
   --------------------------------------------------------------------------- */

@media (min-width: 960px) {
    .rform,
    .import { height: calc(100dvh - 60px); }

    .rform__tabs { display: none; }

    /*
        INGREDIENTS IS A FIXED COLUMN; DIRECTIONS TAKES THE REST.

        RecipeFormWide.dc.html draws exactly that - "width: 370px; flex-shrink: 0" beside
        "flex-grow: 1" - and this shipped as 1fr / 1.15fr instead, which is a different idea: it
        made the two columns fight over the width in proportion. Measured on a 1452px window, the
        ingredient list took 521px and the directions 599px, so the long-form half of the screen
        was the smaller half. An ingredient row is a number, a unit and a name; it does not get
        wider with a bigger monitor, and giving it the space is giving it to nothing.
    */
    .rform__panes {
        display: grid;
        grid-template-columns: 370px minmax(0, 1fr);
        gap: 28px;
        align-items: start;
        max-width: 1180px;
    }

    .rform__pane,
    .rform__pane.is-shown { display: block; }

    .rform__pane-title { display: block; }

    /* Details belongs with the name and timings, not in a column of its own. */
    .rform__pane--details { grid-column: 1 / -1; }

    /*
        Centred, not just capped. Both of these already had a max-width and neither had
        `margin-inline: auto`, so the body sat hard against the left edge while the bar and the
        footer ran the full width of the window - the form read as shoved into a corner of a
        screen it was ignoring, which is worse than either extreme on its own.
    */
    .rform__head {
        max-width: 1180px;
        margin-inline: auto;
        padding-inline: 16px;
    }

    .rform__panes { margin-inline: auto; }

    /*
        The bar and the footer stay full-bleed - their background and border should reach the
        window edges like any app bar - but their CONTENTS line up with the column above. The
        max() keeps the 16px gutter on a narrow desktop and grows it once there is room.
    */
    .rform__bar,
    .rform__foot,
    .import__bar,
    .import__foot {
        padding-inline: max(16px, calc((100% - 1180px) / 2));
    }

    .import__recipe,
    .import__state { max-width: 780px; }

    .import__foot { flex-direction: row; align-items: center; justify-content: flex-end; }
    .import__adds { text-align: right; }
}

/* ---- the account menu, and the sheet it mirrors ---- */

.app-avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    flex-shrink: 0;

    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
    font-size: 12px;
    font-weight: 700;
    cursor: pointer;
}

.app-avatar--lg {
    width: 42px;
    height: 42px;
    font-size: 14px;
    cursor: default;
}

.app-accountcard {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 13px 14px;
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.app-accountcard__name { display: block; font-weight: 600; font-size: 14px; }

.app-accountcard__plan {
    display: block;
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
}

/* The current value of a setting, beside its label - so the next tap is predictable. */
.app-menu__value {
    margin-left: auto;
    padding-left: 18px;
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
}

/* Not destructive, so not the error colour. */
.app-menu__signout { color: var(--mud-palette-text-secondary); }

/* ---- the More sheet ---- */

.more {
    display: flex;
    flex-direction: column;
    gap: 13px;

    /* The same gutter every other screen keeps. Without it the rounded groups sit flush to the
       screen edge and their corners read as clipped rather than rounded. */
    padding: 8px 16px 24px;
}

.more__account {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 4px 2px 12px;
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.more__name { font-family: "Bricolage Grotesque", Georgia, serif; font-size: 19px; font-weight: 600; letter-spacing: -0.02em; }
.more__plan { font-size: 12px; color: var(--mud-palette-text-secondary); margin-top: 1px; }

.more__group {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 12px;
    background: var(--mud-palette-surface);
}

.more__row {
    display: flex;
    align-items: center;
    gap: 11px;

    /* Every row in the app's own 44px floor, and these are one-handed targets. */
    min-height: 48px;
    padding: 10px 14px;

    width: 100%;
    box-sizing: border-box;
    font-size: 15px;
    text-align: left;
    color: var(--mud-palette-text-primary);
    text-decoration: none;
    background: none;
    border: 0;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    cursor: pointer;
    font-family: inherit;
}

.more__row:last-child { border-bottom: 0; }

/* Settings, tinted apart from the catalog rows below them - the artboard's distinction. */
.more__row--tinted {
    background: color-mix(in srgb, var(--mud-palette-primary) 7%, transparent);
    color: var(--mud-palette-primary);
}

.more__row--quiet { color: var(--mud-palette-text-secondary); }

.more__label { flex-grow: 1; }

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

@media (min-width: 960px) {
    /* The sheet is the phone's shape. At md+ the same items live in the bar's two menus. */
    .more { max-width: 460px; }
}

/* ---- tab bar: the base, replaced by the top bar at md+ ---- */

.app-tabbar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1200;

    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    align-items: end;

    background: var(--mud-palette-surface);
    border-top: 1px solid var(--mud-palette-lines-default);
    padding: 8px 0 calc(10px + env(safe-area-inset-bottom));
}

.app-tabbar__item,
.app-tabbar__surprise {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;

    /* 44px is the floor for anything anybody has to hit. */
    min-height: 44px;

    font-size: 10px;
    font-weight: 500;
    color: var(--mud-palette-text-disabled);
    text-decoration: none;
    background: none;
    border: 0;
    cursor: pointer;
}

.app-tabbar__item.is-active {
    color: var(--mud-palette-primary);
    font-weight: 700;
}

/*
    Surprise Me is a core action, not a destination, so it does not look like one.

    WeekMobile.dc.html draws it as a disc that BREAKS the top edge of the bar - lifted clear,
    ringed in the page background so the bar's border appears to part around it, and carrying
    the only shadow in the chrome. A tinted rounded rectangle sitting inside the bar, which is
    what this was, reads as a fifth tab that someone coloured in.

    The label stays outside the disc, in the same place and size as the other four, because the
    row of labels is what makes the bar scan as one row of five.
*/
.app-tabbar__surprise {
    gap: 5px;
    color: var(--mud-palette-primary);
    font-weight: 700;
    background: none;

    /* The lift has to survive the bar's own padding, so it is the disc that moves, not this. */
    justify-content: flex-end;
}

.app-tabbar__surprise .mud-icon-root {
    box-sizing: content-box;
    width: 27px;
    height: 27px;
    font-size: 27px;
    padding: 12px;

    color: var(--mud-palette-primary-text);
    background: var(--mud-palette-primary);
    border-radius: 50%;

    /* Rings the disc in the page ground so the bar's top border parts around it. */
    border: 4px solid var(--mud-palette-background);
    box-shadow: 0 4px 14px color-mix(in srgb, var(--mud-palette-primary) 34%, transparent);

    /* Clear of the bar's top edge. The negative margin is what makes it read as lifted. */
    margin-top: -30px;
}

.app-tabbar__surprise > span { margin-top: -2px; }

@media (min-width: 960px) {
    .app-topbar { display: block; }
    .app-tabbar { display: none; }
    .app-main { padding-bottom: 32px; }
}

/*
    The routing junction at "/". It has no chrome of its own - it borrows the loading state and
    gives it the height to centre in, because the alternative is a message pinned under the
    header with the page empty beneath it.
*/
.app-junction { min-height: 60dvh; }

/* ---- the three data states ---- */

.loading-state {
    display: flex;
    flex-direction: column;
    align-items: center;

    /* Centred in the space it has, not parked at the top of it. A cold start can hold this
       screen for half a minute, and a message pinned under the header with emptiness below
       reads as a page that failed to finish rather than one still working. */
    justify-content: center;
    min-height: 60dvh;

    gap: 13px;
    padding: 32px 22px;
    text-align: center;
}

/* The mark and wordmark, shared by the loading and signed-out screens (BrandLockup). */
.brand-lockup {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 13px;
}

.brand-lockup__word {
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 18px;
    font-weight: 700;
    letter-spacing: -0.025em;
}

.brand-lockup__meal { color: var(--mud-palette-primary); }
.brand-lockup__az { color: var(--mud-palette-success); }

.loading-state__bar { width: 190px; }

.loading-state__message {
    max-width: 34ch;
    color: var(--mud-palette-text-secondary);
}

.loading-state__skeleton {
    width: 100%;
    max-width: 520px;
    margin-top: 10px;
    text-align: left;
}

.empty-state,
.error-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 60dvh;
    gap: 11px;
    padding: 32px 22px;
    text-align: center;
}

.empty-state__icon,
.error-state__icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 74px;
    height: 74px;
    border-radius: 22px;

    /* The primary at low opacity rather than a second token, so it follows the theme. */
    background: color-mix(in srgb, var(--mud-palette-primary) 13%, transparent);
}

.error-state__icon {
    background: color-mix(in srgb, var(--mud-palette-error) 11%, transparent);
}

.empty-state__body,
.error-state__body {
    max-width: 36ch;
    color: var(--mud-palette-text-secondary);
}

/*
    Stacked and full width, the way the states artboard draws them - not a centred row of
    content-width pills. The artboard panel is roughly a phone's width, and the phone is the
    base layout, so this is the designed treatment rather than the narrow-screen fallback.

    Capped, because two buttons stretched across a desktop empty state look like a mistake.
*/
.empty-state__actions {
    display: flex;
    flex-direction: column;
    gap: 9px;
    margin-top: 6px;
    width: 100%;
    max-width: 320px;
}

.error-state__hint { color: var(--mud-palette-text-disabled); }
.error-state__retry { margin-top: 6px; }

/*
    Touch sizing. MudBlazor's default button is around 36px, which is under the 44px minimum
    hit target design-principles.md sets - and these are the buttons that matter most, because
    an empty or failed screen offers nothing else to hit.

    46px on the empty state matches the artboard. The artboard draws the retry button at 42px,
    but 44 is the floor we committed to, so the floor wins by two pixels.
*/
.empty-state__actions .mud-button-root { min-height: 46px; }
.error-state__retry.mud-button-root { min-height: 44px; }

/* ---- recipes ---- */

.recipe-card {
    display: flex;
    gap: 13px;
    align-items: center;
    padding: 13px 16px;
    min-height: 44px;
    background: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-divider-light);
    text-decoration: none;
    color: inherit;
    cursor: pointer;
    width: 100%;
    border-left: 0;
    border-right: 0;
    border-top: 0;
    text-align: left;
    font: inherit;
}

.recipe-card:hover { background: var(--mud-palette-action-default-hover); }

.recipe-card__thumb {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 58px;
    height: 58px;
    flex-shrink: 0;
    border-radius: 10px;
    background: color-mix(in srgb, var(--mud-palette-primary) 13%, transparent);
    color: var(--mud-palette-primary);
}

.recipe-card__body { flex: 1; min-width: 0; }

.recipe-card__name {
    font-size: 15px;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
}

.recipe-card__meta {
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
    margin-top: 2px;
}

.recipe-card__tags {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
    margin-top: 7px;
}

.recipe-tag {
    font-size: 10px;
    font-weight: 600;
    padding: 3px 8px;
    border-radius: 9px;
    background: color-mix(in srgb, var(--mud-palette-primary) 11%, transparent);
    color: var(--mud-palette-primary);
}

.recipe-card__rating {
    display: flex;
    align-items: center;
    gap: 3px;
    flex-shrink: 0;
    font-size: 12px;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
}

.recipe-list {
    border-top: 1px solid var(--mud-palette-divider-light);
    margin-top: 14px;
}

.recipe-list__end {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 20px;
    font-size: 12px;
    color: var(--mud-palette-text-disabled);
}

.recipe-header {
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding-top: 18px;
}

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

.recipe-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 14px;
    margin-top: 9px;
    font-size: 13px;
    color: var(--mud-palette-text-secondary);
}

.recipe-meta__item {
    display: flex;
    align-items: center;
    gap: 5px;
}

.recipe-source {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 14px;
    padding: 9px 12px;
    border-radius: 9px;
    background: var(--mud-palette-background-gray);
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
}

.recipe-steps {
    list-style: none;
    counter-reset: step;
    padding: 0;
    margin: 8px 0 0;
}

.recipe-steps__step {
    counter-increment: step;
    display: flex;
    gap: 13px;
    padding: 11px 0;
    border-bottom: 1px solid var(--mud-palette-divider-light);
}

/* The ordinal is the meaning, so it is drawn rather than left to a default marker. */
.recipe-steps__step::before {
    content: counter(step);
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border-radius: 12px;
    background: color-mix(in srgb, var(--mud-palette-primary) 13%, transparent);
    color: var(--mud-palette-primary);
    font-size: 12px;
    font-weight: 700;
}

.recipe-ingredient {
    display: flex;
    align-items: baseline;
    gap: 12px;
    padding: 10px 0;
    border-bottom: 1px solid var(--mud-palette-divider-light);
}

.recipe-ingredient__amount {
    width: 88px;
    flex-shrink: 0;
    font-size: 14px;
    font-weight: 600;
    color: var(--mud-palette-primary);
}

/* ---------------------------------------------------------------------------
   Quantities, and the labels above them.

   design-principles.md reserves Bricolage Grotesque for headings AND NUMBERS, but every
   quantity in the app was rendering as body text, which made half that reservation
   decorative. This app is mostly numbers - servings, prep time, counts, amounts - so the
   display face doing that job is most of what it sounds like.

   Tabular figures because these sit in lists: proportional digits make a column of counts
   ripple, and a 1 that is narrower than a 4 is the usual cause.
   --------------------------------------------------------------------------- */

.numeral {
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-weight: 600;
    letter-spacing: -0.01em;
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
}

/*
    Sentence case, and no tracking. This was 11px/700 at 0.09em in hardcoded capitals - the
    tracked-out eyebrow label that appears above every heading on a templated page. The count
    beside it is the thing worth seeing, so it keeps the weight and the label gives it up.
*/
.section-label {
    display: flex;
    align-items: baseline;
    gap: 7px;
    font-size: 13px;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
    margin: 26px 0 2px;
}

/*
    Spaced by margin, not by the whitespace between the label and the span. Razor collapses that
    text node away and the chips rendered as "All3" and "Weeknight19" - the same reason the
    design guidance says to space siblings with gap rather than source whitespace.
*/
.section-label__count,
.recipe-chip__count,
.recipe-tab__count {
    margin-left: 5px;
    color: var(--mud-palette-text-secondary);
    font-size: 0.95em;
}

/* The section label is already a flex row with its own gap, so it does not need the margin. */
.section-label .section-label__count { margin-left: 0; }

/* On the active chip the count has to stay legible against the filled purple. */
.recipe-chip.is-active .recipe-chip__count {
    color: var(--mud-palette-primary-text);
    opacity: 0.75;
}

/*
    Facts separated by space rather than by a dot. The gap has to be wide enough to read as a
    break on its own - at a narrower gap the eye wants the separator back, which is how meta
    strings end up strung together with middle dots in the first place.
*/
.recipe-fact + .recipe-fact { margin-left: 14px; }

/* Pill buttons, on top of the theme's 10px default. */
.pill,
.pill.mud-button-root { border-radius: 999px !important; }

/* ---------------------------------------------------------------------------
   Recipes: one design at two widths.

   The phone is the base - a list, and the detail replaces it when you pick
   something. At md+ they sit side by side, which is the real enhancement:
   reading a recipe no longer means leaving the list. No capability is added;
   everything here can be reached on the phone, one tap apart.
   --------------------------------------------------------------------------- */

.recipes__detail { display: none; }
.recipes.has-selection .recipes__list { display: none; }
.recipes.has-selection .recipes__detail { display: block; }

/*
    Bounded, so the bar on the bottom edge has something to sit against. dvh rather than vh:
    a collapsing address bar must not push it under the browser chrome. The tab bar's height
    comes off the top of it, since that is still on screen while the list is.
*/
.recipes {
    height: calc(100dvh - 66px - env(safe-area-inset-bottom));
    overflow: hidden;

    /*
        A column, so the list pane fills the height rather than stopping at its content. Without
        this the pane is a plain block sized to what is in it, the bottom bar sits wherever the
        last recipe happens to end, and a short library leaves a band of page background between
        the two buttons and the tab bar.
    */
    display: flex;
    flex-direction: column;
}

.recipes > .recipes__list { flex: 1; min-height: 0; }

/* Reading a recipe hides the tab bar, so the screen is all of it. */
.recipes.has-selection { height: 100dvh; }

.recipes.has-selection .recipes__detail { height: 100%; }

.recipe-detail__idle { display: none; }

@media (min-width: 960px) {
    .recipes {
        display: grid;

        /* A list wide enough for a name, a meta line and two tags without wrapping, and the
           rest to the recipe. Full width: a 4K monitor should not leave two dead margins,
           so the measure is capped on the text that needs it rather than on the panes. */
        grid-template-columns: 400px minmax(0, 1fr);
    }

    /* Both panes are always present at this width - picking a recipe changes what is in the
       right one, it does not take the list away. */
    .recipes__list,
    .recipes.has-selection .recipes__list,
    .recipes__detail,
    .recipes.has-selection .recipes__detail {
        display: block;
    }

    .recipes__list {
        border-right: 1px solid var(--mud-palette-lines-default);
        background: var(--mud-palette-surface);
    }

    /* The top bar takes 60px at this width; the tab bar is gone. */
    .recipes,
    .recipes.has-selection {
        height: calc(100dvh - 60px);
    }

    /* Each pane scrolls on its own, so a long recipe does not drag the list away with it. */
    .recipes__list { overflow: hidden; }

    .recipes__detail {
        display: flex;
        flex-direction: column;
        overflow: hidden;
    }

    .recipe-detail__idle {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 11px;
        min-height: 60dvh;
        color: var(--mud-palette-text-disabled);
    }

    /* Selected row: a tint and a left bar as well as the fact its contents fill the pane
       beside it. Three signals, so the selection never rests on colour alone. */
    .recipe-card.is-selected {
        background: color-mix(in srgb, var(--mud-palette-primary) 11%, transparent);
        box-shadow: inset 3px 0 0 var(--mud-palette-primary);
    }
}

/* Which kitchen you are in, in the top bar. */
.app-board {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 14px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 16px;
    font-size: 13px;
    font-weight: 500;
    color: var(--mud-palette-text-primary);
    background: var(--mud-palette-surface);
    cursor: pointer;
    white-space: nowrap;
}

.app-board__role {
    margin-left: 8px;
    font-size: 11px;
    color: var(--mud-palette-text-secondary);
}

/* Who else is in this kitchen. */
.app-members {
    display: flex;
    align-items: center;
    margin-left: 12px;
}

.app-members__avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    border: 2px solid var(--mud-palette-surface);
    margin-left: -8px;

    font-size: 11px;
    font-weight: 600;
    color: #fff;
    cursor: default;
}

.app-members__avatar:first-child { margin-left: 0; }

.app-members__avatar--more {
    background: var(--mud-palette-action-disabled);
    color: var(--mud-palette-text-primary);
}

/* ---------------------------------------------------------------------------
   Recipe detail: hero, tabs on a phone, columns on a laptop, and an action
   bar pinned to the bottom edge at both widths.
   --------------------------------------------------------------------------- */

/*
    A bounded column with a scrolling body, so the action bar is on the bottom edge rather
    than wherever the ingredients happen to end. Sticky does not do this: an element that is
    the last child has nothing below it to scroll past, so it never pins - which is why the
    buttons had to be scrolled to. The onboarding wizard solves it the same way.
*/
.recipe-detail {
    display: flex;
    flex-direction: column;
    height: 100%;
}

.recipe-detail__scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
}

.recipe-hero {
    position: relative;
    height: 190px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    background: color-mix(in srgb, var(--mud-palette-primary) 13%, transparent);
    color: color-mix(in srgb, var(--mud-palette-primary) 55%, transparent);
}

.recipe-hero__image {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/*
   The empty hero is the control, not a button drawn on top of one. It fills the hero so the
   whole rectangle is the target rather than the 11px caption - the hint says "tap to add" and
   the thing worth tapping is the picture-shaped hole, which is where a finger goes anyway.
*/
.recipe-hero__empty {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    padding: 0;
    background: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
}

.recipe-hero__empty:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -3px;
}

.recipe-hero__hint {
    position: absolute;
    bottom: 12px;
    left: 18px;
    padding: 5px 11px;
    border-radius: 9px;
    background: color-mix(in srgb, var(--mud-palette-surface) 88%, transparent);
    font-size: 11px;
    font-weight: 600;
    color: var(--mud-palette-primary);
}

.recipe-hero__back,
.recipe-hero__button {
    position: absolute;
    top: 14px;

    /* 44px is the floor for anything anybody has to hit, and these sit over a photo where a
       near miss scrolls the page instead. */
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 22px;
    background: color-mix(in srgb, var(--mud-palette-surface) 92%, transparent);
    color: var(--mud-palette-text-primary);
    cursor: pointer;
}

.recipe-hero__back { left: 14px; }

.recipe-hero__actions {
    position: absolute;
    top: 14px;
    right: 14px;
    display: flex;
    gap: 8px;
}

.recipe-hero__actions .recipe-hero__button { position: static; }

/*
    A hearted recipe. COLOUR IS NOT DOING THIS ALONE - the icon itself changes from an outline to
    a filled heart, and aria-pressed says it in words, because emerald and UT orange collide under
    red-green colour blindness and a tint on its own would be the only signal.
*/
.recipe-hero__button.is-on .mud-icon-root { color: var(--mud-palette-primary); }

/* A heart mid-flight should not look tappable again. */
.recipe-hero__button:disabled { opacity: 0.55; cursor: default; }

/* Drawn but not wired yet. Dimmed rather than hidden, so the shape of the screen is right
   before the plumbing exists. */
.recipe-hero__button:disabled { cursor: default; opacity: 0.65; }

.recipe-detail__body { padding: 18px 16px 24px; }
.recipe-detail__title { margin-bottom: 2px; }

/* ---- tabs: the base, replaced by columns at md+ ---- */

.recipe-tabs {
    display: flex;
    gap: 4px;
    margin-top: 18px;
    padding: 4px;
    border-radius: 12px;
    background: var(--mud-palette-background-gray);
}

.recipe-tabs__tab {
    flex: 1;
    min-height: 44px;
    border: 0;
    border-radius: 9px;
    background: none;
    font: inherit;
    font-size: 13px;
    font-weight: 500;
    color: var(--mud-palette-text-secondary);
    cursor: pointer;
}

/* A filled surface and a weight change, not just a colour. */
.recipe-tabs__tab.is-active {
    background: var(--mud-palette-surface);
    color: var(--mud-palette-text-primary);
    font-weight: 600;
}

.recipe-panels { margin-top: 12px; }

/* One panel at a time on a phone. */
.recipe-panels .recipe-panel { display: none; }
.recipe-panels--ingredients .recipe-panel--ingredients { display: block; }
.recipe-panels--directions .recipe-panel--directions { display: block; }
.recipe-panels--notes .recipe-panel--notes { display: block; }

/* The label is the tab on a phone, so it would be said twice. */
.recipe-panels .recipe-panel .section-label { display: none; }

.recipe-panel__empty { color: var(--mud-palette-text-disabled); }
.recipe-notes { white-space: pre-line; }

/* ---- action bar ---- */

.recipe-actions {
    flex-shrink: 0;

    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 16px calc(14px + env(safe-area-inset-bottom));

    background: var(--mud-palette-surface);
    border-top: 1px solid var(--mud-palette-lines-default);
}

.recipe-actions__primary,
.recipe-actions__secondary {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    height: 52px;
    border-radius: 26px;
    font: inherit;
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
}

.recipe-actions__primary {
    flex: 1;
    border: 0;
    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
}

.recipe-actions__secondary {
    width: 52px;
    flex-shrink: 0;
    border: 1px solid var(--mud-palette-lines-default);
    background: var(--mud-palette-surface);
    color: var(--mud-palette-text-primary);
}

/* The label rides along only where there is room for it. */
.recipe-actions__secondary span { display: none; }

.recipe-actions__primary:disabled,
.recipe-actions__secondary:disabled { cursor: default; opacity: 0.9; }

.recipe-actions__hint { display: none; }

@media (min-width: 960px) {
    .recipe-detail__body { padding: 24px 28px 32px; }

    .recipe-hero { height: 210px; }

    /* The list is right there at this width, so there is nothing to go back to. */
    .recipe-hero__back { display: none; }

    /* Columns instead of tabs. */
    .recipe-tabs { display: none; }

    .recipe-panels {
        display: grid;
        grid-template-columns: 320px minmax(0, 1fr);
        gap: 0 34px;
        align-items: start;
        margin-top: 4px;
    }

    .recipe-panels .recipe-panel { display: block; }
    .recipe-panels .recipe-panel .section-label { display: block; }

    .recipe-panel--ingredients { grid-column: 1; grid-row: 1 / span 2; }
    .recipe-panel--directions { grid-column: 2; grid-row: 1; }

    /* Notes under the directions it belongs with, rather than in a third column nobody
       looks at. */
    .recipe-panel--notes { grid-column: 2; grid-row: 2; }

    /* The panes fill the window, but a line of directions does not: past about 70
       characters the eye loses its place coming back to the left margin, and a 4K monitor
       would otherwise set them 2000px wide. */
    .recipe-steps,
    .recipe-notes,
    .recipe-panel--notes .recipe-panel__empty { max-width: 72ch; }

    .recipe-actions { padding: 14px 28px; }

    .recipe-actions__primary { flex: 0 0 auto; padding: 0 26px; }

    .recipe-actions__secondary {
        width: auto;
        padding: 0 22px;
    }

    .recipe-actions__secondary span { display: inline; }

    .recipe-actions__hint {
        display: block;
        margin-left: auto;
        font-size: 12px;
        color: var(--mud-palette-text-disabled);
    }
}

/*
    A screen that puts up its own bottom action bar owns the bottom edge. Two stacked bars on
    a 390px phone is most of the thumb zone spent on chrome, and the recipe's own actions are
    the ones somebody opened it for. The back chevron in the hero is the way out, which is why
    it is there.
*/
.app-shell:has(.recipe-actions) .app-tabbar { display: none; }
.app-shell:has(.recipe-actions) .app-main { padding-bottom: 0; }


/* ---------------------------------------------------------------------------
   The library's list pane: header, tools, tag filter, and the two ways in.
   --------------------------------------------------------------------------- */

.recipes__list {
    display: flex;
    flex-direction: column;
    min-height: 0;
}

.recipes__header {
    flex-shrink: 0;
    padding: 0 16px;
    background: var(--mud-palette-surface);
}

.recipes__results {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
}

.recipes__tools {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 12px;
}

.recipes__search { flex: 1; }

/* On a phone these live in the bar at the bottom edge instead, where the thumb is. */
.recipes__tool { display: none; }

.recipes__filters {
    display: flex;
    gap: 7px;
    padding: 12px 0;

    /* Chips scroll sideways rather than wrapping into a second row that pushes the list
       down the screen. The pane itself never scrolls horizontally. */
    overflow-x: auto;
    scrollbar-width: none;
}

.recipes__filters::-webkit-scrollbar { display: none; }

.recipe-chip {
    flex-shrink: 0;
    min-height: 34px;
    padding: 0 15px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 17px;
    background: var(--mud-palette-surface);
    font: inherit;
    font-size: 12px;
    font-weight: 500;
    color: var(--mud-palette-text-primary);
    white-space: nowrap;
    cursor: pointer;
}

/* Filled and bolder, not just tinted - the active filter changes what the list contains, so
   it should not be the sort of thing you have to look twice at. */
.recipe-chip.is-active {
    background: var(--mud-palette-primary);
    border-color: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
    font-weight: 600;
}

/*
    The ingredient filter, arrived at from a catalog row rather than chosen here.

    Deliberately not shaped like its neighbours. A tag chip toggles - tap it again and it lets go
    - so it needs no affordance for leaving. This one is a state somebody was handed, and the X
    is the way out; without it the only exit is the browser's back button, which is not a control.
    The icon on the left says which KIND of filter it is, so a chip reading "Butter" is not
    mistaken for a tag by that name.
*/
.recipe-chip--filter {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding-right: 11px;
}

.recipe-chip--filter .mud-icon-root {
    font-size: 15px;
    width: 15px;
    height: 15px;
}

/* Lighter than the label: it is how to leave, not what the filter is. */
.recipe-chip--filter .recipe-chip__clear {
    opacity: 0.7;
}

/* ---- the two ways in, as a bar on the bottom edge ---- */

/*
    The bar sits directly on the tab bar, and its surface runs to the bottom edge so there is no
    seam between the two. The Surprise disc is lifted 30px clear of the tab bar's top edge and is
    horizontally centred - right where "Import a link" is - so the content is pushed up past it
    while the background stays put. Padding rather than margin is what keeps the fill unbroken.
*/
.recipes__bottom {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 16px calc(34px + env(safe-area-inset-bottom));
    background: var(--mud-palette-surface);
    border-top: 1px solid var(--mud-palette-lines-default);
}

.recipes__import {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    height: 50px;
    border: 0;
    border-radius: 25px;
    background: color-mix(in srgb, var(--mud-palette-primary) 12%, transparent);
    color: var(--mud-palette-primary);
    font: inherit;
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
}

.recipes__add {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 50px;
    height: 50px;
    border: 0;
    border-radius: 25px;
    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
    cursor: pointer;
}

@media (min-width: 960px) {
    .recipes__header { padding: 16px 16px 0; }

    /* Beside the search, where a pointer already is. The bar below would push the list down
       a screen that has plenty of room across instead. */
    .recipes__tool {
        display: flex;
        align-items: center;
        justify-content: center;
        flex-shrink: 0;
        width: 40px;
        height: 40px;
        border: 0;
        border-radius: 10px;
        cursor: pointer;
    }

    .recipes__tool--import {
        background: color-mix(in srgb, var(--mud-palette-primary) 12%, transparent);
        color: var(--mud-palette-primary);
    }

    .recipes__tool--add {
        background: var(--mud-palette-primary);
        color: var(--mud-palette-primary-text);
    }

    .recipes__bottom { display: none; }
}

/* ---------------------------------------------------------------------------
   The three states, matched to the states artboard.
   --------------------------------------------------------------------------- */

.brand-lockup__logo {
    width: auto;
    height: 34px;
    opacity: 0.9;
}

.empty-state__footnote {
    margin-top: 2px;
    max-width: 36ch;
    color: var(--mud-palette-text-disabled);
}

/*
    A failure is a card, not bare text on the page background. Text that looks like the rest of
    the page reads as content; a bordered card reads as something that happened.
*/
.state-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;

    width: 100%;
    max-width: 380px;
    padding: 24px 20px;

    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 14px;
    text-align: center;
}

.state-card__icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 46px;
    height: 46px;
    border-radius: 23px;
    background: color-mix(in srgb, var(--mud-palette-error) 11%, transparent);
}

/* Offline is not a fault, so it does not get the alarm colour. */
.state-card__icon--quiet {
    background: var(--mud-palette-background-gray);
}


/* ==================================================================================
   THE PLANNER
   PlanWeek.dc.html, PlanSlots.dc.html, PlanStart.dc.html, PlanDay.dc.html

   Mobile-first, as everything here is: the DAY is the base layout and the grid is the
   md+ enhancement of it. Both are in the markup and this file decides which is seen,
   so the two widths cannot drift into being two components.
   ================================================================================== */

.plan { padding-bottom: 26px; }

/* ---------- the week header ---------- */

.plan__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    padding: 18px 16px 14px;
}

.plan__range {
    margin: 0;
    font-family: var(--mealaz-display, inherit);
    font-size: 23px;
    font-weight: 600;
    letter-spacing: -0.025em;
}

.plan__steps { display: flex; gap: 6px; }

.plan__step {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 44px;
    width: 44px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 22px;
    background: var(--mud-palette-surface);
    color: var(--mud-palette-text-secondary);
    cursor: pointer;
}

/*
    The answer, at the size of the question.

    "5 of 7 dinners planned" was 13px grey beside the date range on the old artboard, which is
    the one thing somebody opens this screen to find out.
*/
.plan__score {
    margin: 0;
    font-size: 18px;
    font-weight: 600;
    letter-spacing: -0.015em;
}

.plan__score-label {
    font-size: 13px;
    font-weight: 400;
    color: var(--mud-palette-text-secondary);
    margin-left: 4px;
}

/* No score on an unplanned week. Nobody opens one wanting to be marked out of seven. */
.plan__score--none { font-size: 13.5px; font-weight: 400; color: var(--mud-palette-text-secondary); }

.plan__tools { display: flex; gap: 8px; flex-wrap: wrap; width: 100%; }

/* ---------- the plate ---------- */

.plate { flex-shrink: 0; }
.plate__ring { stroke: var(--mud-palette-primary); }
.plate__centre { fill: var(--mud-palette-primary); }
.plate__ring--empty { stroke: var(--mud-palette-lines-default); }

/* A night out keeps the outline and takes a fork. Warm rather than purple, and a different
   SHAPE - colour is never the only difference between two states. */
.plate__ring--out { stroke: #E8B77A; }
.plate__fork { stroke: #8A4A05; }

/* ---------- the empty week's preview ---------- */

.plan__start {
    margin: 0 16px 14px;
    padding: 15px 16px;
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 15px;
}

.plan__start-head { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; }

.plan__start-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 34px;
    width: 34px;
    flex-shrink: 0;
    border-radius: 10px;
    background: color-mix(in srgb, var(--mud-palette-primary) 12%, transparent);
    color: var(--mud-palette-primary);
}

.plan__start-title { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -0.015em; }

.plan__start-body {
    margin: 3px 0 0;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--mud-palette-text-secondary);
}

/*
    The two answers, together. "Not this time" is text and "Keep these" is filled, because they
    are not equal offers - one is the thing the screen is for and the other is the way past it.
*/
.plan__start-actions { display: flex; align-items: center; gap: 6px; margin-top: 6px; }

/* The other way in, when last week is not the answer. Quiet: one of these is right for most
   weeks and a row of equal buttons says the opposite (PlanStart.dc.html). */

.plan__start-skip { color: var(--mud-palette-text-secondary); }

.plan__start-keep { margin-top: 0; }

/*
    A GHOST, not a card - PlanStart.dc.html's own words, and the whole argument of that screen.

    Last week's dinner sitting in this week's night: dashed, unsaturated, and deliberately NOT
    the same object as a planned meal. A solid card here would say the week had been planned
    behind somebody's back, and the one thing this screen must not do is claim a decision nobody
    made. It is the difference between a glance and a leap - "Keep these 5" now commits what is
    already on the screen.
*/
.plan__ghost {
    margin: 0 0 10px;
    padding: 11px 12px;
    border: 1px dashed color-mix(in srgb, var(--mud-palette-primary) 28%, transparent);
    border-radius: 12px;
    background: color-mix(in srgb, var(--mud-palette-primary) 4%, var(--mud-palette-surface));
}

.plan__ghost-dish {
    font-size: 13.5px;
    font-weight: 500;
    line-height: 1.35;
    color: color-mix(in srgb, var(--mud-palette-primary) 72%, var(--mud-palette-text-primary));
}

/* The second dish of a meal is quieter than the first, as the artboard draws it. */
.plan__ghost-dish + .plan__ghost-dish { opacity: 0.7; }

.plan__ghost-sub {
    margin-top: 5px;
    font-size: 11px;
    color: var(--mud-palette-text-secondary);
}

/*
    Copying a week copies its GAPS. Papering over an empty night would make "keep these 5" a lie
    about what it is keeping, so the night says it was empty and stays empty.
*/
.plan__ghost--none {
    border-color: var(--mud-palette-lines-default);
    background: none;
    text-align: center;
    font-size: 12px;
    color: var(--mud-palette-text-disabled);
}

/* The first week, which has no past to offer and so needs a sentence of its own. */
.plan__first { margin: 0 16px 16px; }

.plan__first-title { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -0.015em; }

.plan__first-body {
    margin: 5px 0 0;
    max-width: 54ch;
    font-size: 13px;
    line-height: 1.6;
    color: var(--mud-palette-text-secondary);
}

/* ---------- the day, which is the base layout ---------- */

.plan__grid { display: none; }

.plan__strip {
    display: flex;
    gap: 2px;
    padding: 0 12px 12px;
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.plan__jump {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    min-height: 68px;
    padding: 7px 0;
    border: 0;
    border-radius: 12px;
    background: transparent;
    cursor: pointer;
}

.plan__jump.is-selected { background: color-mix(in srgb, var(--mud-palette-primary) 10%, transparent); }

.plan__jump-letter { font-size: 11px; font-weight: 600; color: var(--mud-palette-text-secondary); }
.plan__jump-num { font-size: 14px; color: var(--mud-palette-text-primary); }
.plan__jump.is-selected .plan__jump-letter,
.plan__jump.is-selected .plan__jump-num { color: var(--mud-palette-primary); }

.plan__daytitle {
    display: flex;
    align-items: baseline;
    gap: 9px;
    margin: 16px 16px 12px;
    font-size: 23px;
    font-weight: 600;
    letter-spacing: -0.025em;
}

.plan__daydate { font-size: 15px; font-weight: 500; color: var(--mud-palette-text-secondary); }
.plan__todaymark { margin-left: auto; font-size: 12px; font-weight: 700; color: var(--mud-palette-primary); }

.plan__slot { display: flex; align-items: center; gap: 10px; padding: 0 16px 8px; }
.plan__slotname { font-size: 13px; font-weight: 600; }

/* An unused slot is ONE LINE, not an empty card. Three of them cost ninety pixels between
   them rather than three screens of guilt. */
.plan__slotempty {
    display: flex;
    align-items: center;
    gap: 9px;
    width: calc(100% - 32px);
    min-height: 44px;
    margin: 0 16px 14px;
    padding: 11px 14px;
    border: 0;
    border-radius: 12px;
    background: var(--mud-palette-background-gray);
    font: inherit;
    font-size: 13px;
    color: var(--mud-palette-text-secondary);
    text-align: left;
    cursor: pointer;
}

.plan__slotempty:disabled { cursor: default; }

.plan__card {
    display: block;
    width: calc(100% - 32px);
    margin: 0 16px 14px;
    padding: 14px 15px;
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 15px;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.plan__card:disabled { cursor: default; }

.plan__dish { display: flex; align-items: center; gap: 12px; }
.plan__dish + .plan__dish { margin-top: 13px; padding-top: 13px; border-top: 1px solid var(--mud-palette-lines-default); }

.plan__thumb {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 40px;
    width: 40px;
    flex-shrink: 0;
    border-radius: 11px;
    background: color-mix(in srgb, var(--mud-palette-primary) 12%, transparent);
    color: var(--mud-palette-primary);
}

.plan__dish-body { flex: 1; min-width: 0; }
.plan__dish-name { display: block; font-size: 15px; font-weight: 500; }
.plan__dish-meta { display: block; font-size: 12px; color: var(--mud-palette-text-secondary); margin-top: 2px; }

.plan__mins { font-size: 12px; color: var(--mud-palette-text-secondary); }
.plan__mins .numeral { font-size: 14px; color: var(--mud-palette-text-primary); }

.plan__cooks { display: flex; align-items: center; gap: 6px; margin-left: auto; }
.plan__cooks-label { font-size: 12px; color: var(--mud-palette-text-secondary); }

.plan__avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 24px;
    width: 24px;
    border-radius: 12px;
    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
    font-size: 9px;
    font-weight: 700;
}

/* The one accent, on the one night that needs an afternoon cleared. */
.plan__longest {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    margin-top: 8px;
    padding: 2px 9px 2px 7px;
    border-radius: 11px;
    background: color-mix(in srgb, var(--mud-palette-primary) 11%, transparent);
    color: var(--mud-palette-primary);
    font-size: 10.5px;
    font-weight: 700;
}

.plan__longest .mud-icon-root { font-size: 12px; width: 12px; height: 12px; }

.plan__out-label {
    display: block;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.04em;
    color: #8A4A05;
}

/* The note lives in its day. Not in a band under the grid - a note about Wednesday three
   hundred pixels below Wednesday is a note somebody writes once and never reads again. */
.plan__note {
    display: block;
    width: calc(100% - 32px);
    font: inherit;
    text-align: left;
    cursor: pointer;
    margin: 0 16px 14px;
    padding: 12px 14px;
    border-radius: 12px;
    background: color-mix(in srgb, #E8B77A 16%, var(--mud-palette-surface));
    border: 1px solid color-mix(in srgb, #E8B77A 45%, transparent);
    font-size: 13px;
    line-height: 1.5;
    color: var(--mud-palette-text-primary);
}

.plan__note:disabled { cursor: default; }

.plan__note--empty {
    min-height: 44px;
    background: transparent;
    border: 1px dashed var(--mud-palette-lines-default);
    color: var(--mud-palette-text-disabled);
    text-align: center;
    font-size: 12px;
}

/* ---------- moving a meal ---------- */

/*
    Move mode says what is in the air and how to put it down, on every surface. Without it the
    only thing telling somebody a meal is mid-move is a set of dashed outlines, which is a state
    that can be entered by accident and left running.
*/
.plan__moving {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0 16px 12px;
    padding: 11px 12px 11px 14px;
    border-radius: 12px;
    background: color-mix(in srgb, var(--mud-palette-primary) 10%, transparent);
    color: var(--mud-palette-primary);
    font-size: 13px;
}

.plan__moving strong { font-weight: 600; }
.plan__moving-cancel { margin-left: auto; }

/* Where it came from stays marked rather than going blank - an empty hole re-flows the grid
   under the pointer and loses the thread of what is being moved. */
.plan__jump.is-from { opacity: .45; }

/* Every open night at once, not only the one under the pointer. The question while a meal is in
   the air is "which nights are free", and lighting one at a time makes somebody hunt for an
   answer they are already looking at. */
.plan__jump.is-open {
    background: color-mix(in srgb, var(--mud-palette-primary) 5%, transparent);
    outline: 1px dashed color-mix(in srgb, var(--mud-palette-primary) 40%, transparent);
    outline-offset: -1px;
}

/* ---------- the review panel and the slot line ---------- */

.plan__review { margin: 6px 16px 0; }
.plan__review-title { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -0.015em; }

.plan__review-body {
    margin: 6px 0 0;
    max-width: 60ch;
    font-size: 13.5px;
    line-height: 1.65;
    color: var(--mud-palette-text-secondary);
}

/*
    The slot line, which is the bottom of the list of meal rows rather than the bottom of the page.

    It used to be a paragraph under the review panel - four hundred pixels from the gutter that
    raises the question it answers. Somebody wondering why a board has only a dinner row is
    counting rows, so the answer sits at the end of the rows.
*/
.plan__slotline {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 12px;
    line-height: 1.5;
    color: var(--mud-palette-text-secondary);
}

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

.plan__slotline--day {
    margin: 4px 16px 0;
    padding: 12px 14px 0;
    border-top: 1px solid var(--mud-palette-lines-default);
}

/* ---------- the day note ---------- */

.note-dialog__title { display: flex; flex-direction: column; }

.note-dialog__who {
    margin: 12px 0 0;
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
}

/* ---------- the picker ---------- */

.picker__title { display: flex; flex-direction: column; }
.picker__search { margin-top: 4px; }
.picker__empty { margin: 14px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--mud-palette-text-secondary); }
.picker__list { list-style: none; margin: 12px 0 0; padding: 0; }

.picker__row {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 44px;
    padding: 4px 0 4px 2px;
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.picker__name { flex: 1; min-width: 0; font-size: 14px; font-weight: 500; }
.picker__meta { font-size: 12px; color: var(--mud-palette-text-secondary); }
.picker__limit { margin: 10px 0 0; font-size: 12px; color: var(--mud-palette-text-secondary); }

/* ==================================================================================
   md+ : the grid.

   The day columns stay and a slot gutter turns the stack into a matrix, so a
   dinner-only board is this grid with one row rather than a different screen.
   ================================================================================== */

@media (min-width: 960px) {

    .plan__day { display: none; }

    .plan__head { padding: 20px 22px 14px; }
    .plan__range { font-size: 27px; }
    .plan__score { font-size: 20px; }
    .plan__tools { width: auto; margin-left: auto; }

    /*
        A CARD OF ITS OWN, and a deliberate divergence from PlanStart.dc.html.

        The artboard fuses this strip to the grid below it - square bottom corners, no bottom
        border - and it is right to, because in the artboard the grid underneath holds last
        week's meals as ghosts. Strip and grid are one object there, so a button on the right of
        the strip plainly acts on what is beneath it.

        That preview is not built yet, so the grid below shows empty "Add" cells instead. Fused
        to those, the strip reads as their header and the button looks like it belongs to the
        empty week - which is exactly how it was reported: "it bleeds into the plan so it doesn't
        look like the button is for that information".

        THE GHOSTS HAVE ARRIVED, so the join is correct again and is restored below. The card is
        the header of the preview it is offering, and the grid beneath it is now that preview
        rather than seven empty cells - which is exactly the condition the separation was waiting
        on. Note that it is conditional: :has keeps the two fused only while the card is on
        screen, so an ordinary planned week still gets a grid that stands on its own.
    */
    .plan__start { margin: 0 22px 14px; border-radius: 16px; }
    .plan__start-actions { margin-top: 0; margin-left: auto; }

    .plan:has(.plan__start) .plan__start {
        margin-bottom: 0;
        border-radius: 16px 16px 0 0;
        border-bottom: 0;
    }

    .plan:has(.plan__start) .plan__grid {
        border-top: 0;
        border-radius: 0 0 16px 16px;
    }

    .plan__grid {
        display: grid;
        margin: 0 22px;
        background: var(--mud-palette-surface);
        border: 1px solid var(--mud-palette-lines-default);
        border-radius: 14px;
        overflow: hidden;
    }

    /*
        Left whole, for the same reason: the grid is not this card's body while it has none of
        last week in it. See .plan__start above.
    */

    .plan__gutter {
        display: flex;
        align-items: flex-start;
        padding: 13px 12px 13px 18px;
        border-bottom: 1px solid var(--mud-palette-lines-default);
        font-size: 12px;
        font-weight: 600;
        color: var(--mud-palette-text-secondary);
    }

    .plan__gutter.is-anchor { color: var(--mud-palette-text-primary); }
    .plan__gutter--head { padding: 0; }
    .plan__gutter--notes { border-bottom: 0; }

    .plan__dayhead {
        display: flex;
        align-items: center;
        gap: 8px;
        padding: 12px 11px 11px;
        border-left: 1px solid var(--mud-palette-lines-default);
        border-bottom: 1px solid var(--mud-palette-lines-default);
    }

    .plan__dayname { font-size: 11.5px; font-weight: 600; color: var(--mud-palette-text-secondary); }
    .plan__daynum { font-size: 14px; }

    /*
        Today is a RULE down the whole column, not a pill beside a date. In a grid, "where am I"
        is a question asked of the column, and a 20px pill answers it seven hundred pixels from
        where the eye is.
    */
    .plan__dayhead.is-today,
    .plan__cell.is-today { background: color-mix(in srgb, var(--mud-palette-primary) 4%, var(--mud-palette-surface)); }
    .plan__dayhead.is-today { box-shadow: inset 0 3px 0 var(--mud-palette-primary); }
    .plan__dayhead.is-today .plan__dayname { color: var(--mud-palette-primary); }

    .plan__cell {
        border-left: 1px solid var(--mud-palette-lines-default);
        border-bottom: 1px solid var(--mud-palette-lines-default);
        padding: 9px 11px;
        min-width: 0;
    }

    .plan__cell--note { border-bottom: 0; }

    .plan__rowempty {
        display: flex;
        align-items: center;
        gap: 9px;
        padding: 11px 14px;
        border-left: 1px solid var(--mud-palette-lines-default);
        border-bottom: 1px solid var(--mud-palette-lines-default);
        font-size: 12px;
        color: var(--mud-palette-text-secondary);
    }

    .plan__meal {
        display: block;
        width: 100%;
        padding: 0;
        border: 0;
        background: transparent;
        font: inherit;
        text-align: left;
        cursor: pointer;
    }

    .plan__meal:disabled { cursor: default; }
    .plan__meal-name { display: block; font-size: 13.5px; font-weight: 500; line-height: 1.35; }
    .plan__meal-name ~ .plan__meal-name { font-weight: 400; color: var(--mud-palette-text-secondary); }
    .plan__meal-foot { display: flex; align-items: center; gap: 6px; margin-top: 7px; }
    .plan__meal--out { color: #8A4A05; }

    /*
        ONE empty cell, everywhere.

        This varied its prominence by how full its row was, so a gap in the dinner row shouted and
        a gap in the lunch row was a bare "+". Two things killed it: nothing on screen explained
        why two empty cells looked different, and the plate on the day header ALREADY says which
        nights are unplanned - so the cell was re-encoding a fact that was already on the screen,
        in a language nobody had been taught.

        The "+" it replaced measured 1.88:1 against the cell. Three is the floor for a glyph
        somebody is expected to find and press, and this clears it without becoming a third thing
        competing with the meals for attention.
    */
    .plan__gap {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        justify-content: center;
        gap: 4px;
        width: 100%;
        padding: 7px;
        border: 1px dashed color-mix(in srgb, var(--mud-palette-text-secondary) 38%, transparent);
        border-radius: 10px;
    }

    .plan__gap-add,
    .plan__surprise {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 5px;
        width: 100%;
        min-height: 34px;
        padding: 5px 8px;
        border: 0;
        border-radius: 7px;
        background: transparent;
        font: inherit;
        font-size: 12.5px;
        font-weight: 500;
        cursor: pointer;
    }

    /* Together they clear the 44px floor; the pair is the target, not each half. */
    .plan__gap-add { color: var(--mud-palette-text-secondary); }
    .plan__gap-add:hover { background: var(--mud-palette-action-default-hover); }

    /*
        The second offer is the LOUDER one, which looks backwards until you read the two: Add is
        "I know what I want" and only ever needs to be findable, while this one answers "I do not
        want to decide" - the thing somebody is actually stuck on when they are staring at an
        empty Thursday.
    */
    .plan__surprise {
        background: color-mix(in srgb, var(--mud-palette-primary) 9%, transparent);
        color: var(--mud-palette-primary);
        font-weight: 600;
    }

    .plan__surprise:hover { background: color-mix(in srgb, var(--mud-palette-primary) 17%, transparent); }

    .plan__gap .mud-icon-root { font-size: 15px; width: 15px; height: 15px; }

    /*
        The drag's three states, drawn by MudDropContainer.

        DraggingClass puts is-open on every zone for the duration of a drag; CanDropClass puts
        is-over on the one under the pointer; ItemDraggingClass puts is-lifted on the card in the
        air. Nothing here computes which is which - that was the mistake the hand-rolled version
        kept making.
    */
    .plan__cell.is-open {
        outline: 1px dashed color-mix(in srgb, var(--mud-palette-primary) 40%, transparent);
        outline-offset: -5px;
        border-radius: 10px;
    }

    .plan__cell.is-over {
        outline: 2px solid var(--mud-palette-primary);
        outline-offset: -4px;
        border-radius: 10px;
        background: color-mix(in srgb, var(--mud-palette-primary) 9%, var(--mud-palette-surface));
    }

    .plan__cell .mud-drop-item.is-lifted .plan__meal { opacity: .4; }

    /*
        The whole cell, while something is in the air.

        A tablet in landscape is over the breakpoint, so it gets the grid rather than the phone's
        week strip - and "tap the meal, tap the night" has to work there, because the drag is the
        accelerator and this is the mechanism. It covers the cell rather than sitting beside the
        contents, so the target is the night, not a control within it.
    */
    .plan__land {
        position: absolute;
        inset: 0;
        z-index: 2;
        width: 100%;
        border: 0;
        background: transparent;
        cursor: pointer;
    }

    .plan__cell { position: relative; }

    .mud-drop-item-draggable .plan__meal { cursor: grab; }
    .mud-drop-item-draggable:active .plan__meal { cursor: grabbing; }

    .plan__moving { margin: 0 22px 12px; }

    .plan__note { margin: 0; width: 100%; padding: 9px 10px; font-size: 11px; }
    .plan__note--empty { min-height: 38px; display: flex; align-items: center; justify-content: center; }

    .plan__first { margin: 0 22px 16px; }

    .plan__review { margin: 20px 22px 0; }

    .plan__slotline--day { display: none; }

    .plan__slotline {
        grid-column-start: 2;
        padding: 11px 14px;
        border-left: 1px solid var(--mud-palette-lines-default);
        background: color-mix(in srgb, var(--mud-palette-background-gray) 55%, transparent);
    }

    .plan__gutter--slots {
        border-bottom: 0;
        background: color-mix(in srgb, var(--mud-palette-background-gray) 55%, transparent);
        color: var(--mud-palette-text-disabled);
    }

    /*
        The pointer-events hack that used to be here is gone with the hand-rolled drag.

        It existed because dragenter and dragleave fire per ELEMENT, so the card filling a cell
        stole the pointer from the cell underneath it and the highlight only lit in the few pixels
        of padding around the card. MudDropZone puts its handlers on the zone and accepts events
        from anything inside it, so there is nothing to make inert.
    */
    /*
        THE NIGHTS SAY THEY ARE NIGHTS, while something is in hand.

        is-open and is-over above are put on by MudDropContainer, so they only ever appeared
        during a DRAG. Tapping - the mechanism, per RowActions' one-mechanism-one-accelerator
        rule - got the invisible .plan__land button over each cell and no outline at all. The
        banner said "pick a night" and the grid looked exactly as it had a moment earlier.
        Reported from use, arriving from a recipe: "there doesn't seem to be a way to add it".

        The same dashed resting outline a drag gets, for the same reason and in the same colour -
        these are the places it can land. :has picks the cells that actually carry a landing
        button, so the notes row and the gutter stay out of it.
    */
    .plan.is-moving .plan__cell:has(.plan__land) {
        outline: 1px dashed color-mix(in srgb, var(--mud-palette-primary) 40%, transparent);
        outline-offset: -5px;
        border-radius: 10px;
    }

    .plan.is-moving .plan__cell:has(.plan__land:hover) {
        outline: 2px solid var(--mud-palette-primary);
        outline-offset: -4px;
        background: color-mix(in srgb, var(--mud-palette-primary) 9%, var(--mud-palette-surface));
    }

    .plan.is-moving .plan__cell:has(.plan__land:focus-visible) {
        outline: 2px solid var(--mud-palette-primary);
        outline-offset: -4px;
    }

    .plan.is-moving .plan__cell { border-radius: 10px; }
}

/*
    WHY THE LIST IS SHORT. Quiet: it explains rather than warns, and somebody who already knows
    about their kit nights should read past it without it asking for attention.
*/
.grocery__supplied {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    margin: 0 0 14px;
    padding: 10px 12px;
    border-radius: 10px;
    background: var(--mud-palette-background-gray);
    color: var(--mud-palette-text-secondary);
    font-size: 12.5px;
    line-height: 1.45;
}

/*
    SHOPPING FOR THIS NIGHT.

    One row per recipe, because a kit main can share a night with a salad from the fridge. The
    sentence under each switch carries the meaning: "off" is a box that arrived with everything
    in it on one row and "we already have it" on the next, and the switch alone cannot say which.
*/
.shopfor__row {
    padding: 10px 0;
}

.shopfor__row + .shopfor__row {
    border-top: 1px solid var(--mud-palette-lines-default);
}

.shopfor__why {
    margin: 2px 0 0 48px;
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
}

/*
    THE MEAL'S MENU, against Main.dc.html.

    A list of things to do, not a question with answers - which is why it stopped being a message
    box. Full-width rows at the 44px floor, because on a phone this is the sheet and every row has
    to be a target somebody can hit without aiming.
*/
.mealmenu { display: flex; flex-direction: column; gap: 2px; }

.mealmenu__title { display: flex; flex-direction: column; gap: 2px; }
.mealmenu__name { font-size: 16px; font-weight: 600; line-height: 1.25; }
.mealmenu__where { font-size: 12px; color: var(--mud-palette-text-secondary); }

.mealmenu__item {
    display: flex;
    align-items: center;
    gap: 11px;
    width: 100%;
    min-height: 44px;
    padding: 9px 11px;
    border: 0;
    border-radius: 8px;
    background: transparent;
    font: inherit;
    font-size: 13.5px;
    font-weight: 500;
    color: var(--mud-palette-text-primary);
    text-align: left;
    cursor: pointer;
}

.mealmenu__item:hover { background: var(--mud-palette-action-default-hover); }

/* The one item that destroys something, and it sits next to the one somebody usually meant. */
.mealmenu__item--danger,
.mealmenu__item--danger .mud-icon-root { color: var(--mud-palette-error); }
.mealmenu__item--danger:hover { background: color-mix(in srgb, var(--mud-palette-error) 8%, transparent); }

.mealmenu__rule {
    height: 1px;
    margin: 5px 8px;
    background: var(--mud-palette-lines-default);
}

/*
    WHO IS COOKING. Checkboxes, not a single-select: a roast with the parents coming is two people
    in the kitchen, and a picker that allowed one would make MealCook's join a lie.
*/
.cookpick { display: flex; flex-direction: column; }

.cookpick__row {
    display: flex;
    align-items: center;
    gap: 6px;
    min-height: 44px;
    padding-right: 6px;
    border-radius: 8px;
    cursor: pointer;
}

.cookpick__row:hover { background: var(--mud-palette-action-default-hover); }

.cookpick__avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
    font-size: 10px;
    font-weight: 700;
}

.cookpick__name { font-size: 13.5px; font-weight: 500; }

.cookpick__alone {
    margin: 0;
    padding: 10px 4px;
    font-size: 12.5px;
    color: var(--mud-palette-text-secondary);
}

/*
    RESTAURANTS.

    The list rides on the shared .catalog shell, so what is here is only the two things a
    restaurant has that an ingredient does not: a rating, and a form with more than one field.
*/

/* A number AND a shape, never colour alone - the rule the grocery checkoff set. */
.rest-row__rating {
    display: flex;
    align-items: center;
    gap: 3px;
    margin-left: auto;
    padding-left: 8px;
    flex-shrink: 0;
    font-size: 12px;
    font-weight: 600;
    color: var(--mud-palette-text-secondary);
}

.rest-row__rating .mud-icon-root {
    font-size: 14px;
    width: 14px;
    height: 14px;
    color: var(--mud-palette-warning);
}

/*
    THE RESTAURANT FORM.

    NAMED .rest-form, not .rform. ".rform" was already the RECIPE form, which is a full-height
    PAGE shell - height: calc(100dvh - 66px) - so the restaurant dialog inherited that and
    stretched to nearly the whole viewport with its fields huddled at the top. The collision was
    invisible in the markup and only showed up as "still a ton of empty space" once the field
    heights were fixed and there was nothing else left to blame.

    Reported as a hot mess, and it was: MudBlazor reserves a line of helper-text space under every
    input, so eleven fields with a 4px gap between them rendered with about fifty pixels of blank
    between each - the form ran off the bottom of a phone and the fast-food switch and the tags
    were below the fold of a dialog nobody could tell scrolled.

    The cause, measured rather than guessed at: MudBlazor gives .mud-input-control
    "flex: 1 1 auto", so in a COLUMN flex container every field grows to fill the height. A
    single-line text box measured 249px tall against its natural 56. The gaps between them were
    never the problem - they were 16 to 24px the whole time.
*/
.rest-form { display: flex; flex-direction: column; gap: 12px; }

/* Natural height. Without this each field stretches to share the dialog out between them. */
.rest-form .mud-input-control { flex: 0 0 auto; margin-top: 0; margin-bottom: 0; }

/* Phone and website. Stacks on a phone rather than shrinking each to two thirds of nothing. */
.rest-form__pair { display: grid; grid-template-columns: 1fr; gap: 12px; }

/* City, state, ZIP - the line an address actually ends with, in the order it is written. */
.rest-form__place { display: grid; grid-template-columns: 1fr; gap: 12px; }

@media (min-width: 560px) {
    .rest-form__pair { grid-template-columns: 1fr 1fr; }
    .rest-form__place { grid-template-columns: minmax(0, 2fr) minmax(0, 1.2fr) minmax(0, 0.9fr); }
}

/*
    Rating, fast food and tags are not address fields, so a rule divides them from the ones that
    are. Without it the form is eleven boxes in a column and nothing says where one idea ends.
*/
.rest-form__about {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin-top: 2px;
    padding-top: 14px;
    border-top: 1px solid var(--mud-palette-lines-default);
}

/* The label above, and the stars beside the words they mean. */
.rest-form__rating { display: flex; flex-direction: column; gap: 2px; }

.rest-form__stars { display: flex; align-items: center; gap: 10px; }

.rest-form__label {
    font-size: 12.5px;
    font-weight: 600;
    color: var(--mud-palette-text-secondary);
}

/* The words are the point; the stars are only how you set them. */
.rest-form__word { font-size: 12.5px; color: var(--mud-palette-text-secondary); }

.rest-form__tags { display: flex; flex-direction: column; gap: 8px; }
.rest-form__chips { display: flex; flex-wrap: wrap; gap: 6px; }

.cookpick__alone {
    margin: 0;
    padding: 10px 4px;
    font-size: 12.5px;
    color: var(--mud-palette-text-secondary);
}


/*
    EATING OUT, in the meal picker.

    A section below the recipes, not the other half of a switch. The rule divides it from the
    recipe list so the two read as two answers rather than one field changing its mind.
*/
.picker__out {
    margin-top: 14px;
    padding-top: 13px;
    border-top: 1px solid var(--mud-palette-lines-default);
}

.picker__hint,
.picker__more {
    margin: 6px 2px 0;
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
}

/*
    The error boundary's own two lines, on top of the shared .state-card.

    The trace id is a thing somebody reads out loud or pastes into a message, so it is monospace
    and selectable rather than decorative - and quiet, because it is the least important thing on
    a screen whose job is to say "nothing is lost".
*/
.errorboundary__trace {
    margin-top: 2px;
    color: var(--mud-palette-text-secondary);
}

.errorboundary__id {
    padding: 2px 6px;
    border-radius: 5px;
    background: var(--mud-palette-background-gray);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 11.5px;
    user-select: all;
}

.errorboundary__actions {
    display: flex;
    flex-direction: column;
    gap: 7px;
    width: 100%;
    margin-top: 10px;
}

.errorboundary__actions .mud-button-root { min-height: 46px; }

/*
    "2 recipes", from Main.dc.html.

    A meal could always hold a roast AND a crumble, and nothing on screen said so - two names
    stacked in a cell read as one dish with a long name. The count is the smallest thing that
    teaches a meal is a LIST, and somebody who has seen it once knows there is a question to ask.
*/
.plan__count {
    font-size: 10.5px;
    font-weight: 600;
    color: var(--mud-palette-text-secondary);
}

.plan__count .numeral { font-size: 11px; }

/* The menu names what is in the meal, because the title only ever says the first thing. */
.mealmenu__contents {
    margin-top: 3px;
    font-size: 12px;
    line-height: 1.4;
    color: var(--mud-palette-text-secondary);
}

/*
    THE CATALOG ON A DESKTOP.

    It had no desktop treatment at all, so ingredients, tags and restaurants each sat in a column
    on the far left of a 1440px window with half the screen empty beside them. Mobile-first is the
    rule; "the phone layout, unchanged, on a monitor" is the failure mode it exists to prevent.

    A CENTRED COLUMN rather than full-bleed rows. These are scan-and-pick lists, and a row
    stretched across a wide monitor puts the name at the far left and its actions at the far right
    with nothing in between - the eye has to cross the whole screen to connect the two.

    The height bound comes off as well. It exists so the list scrolls UNDER a fixed header while
    the phone's tab bar is on screen; at this width there is no tab bar, and holding the page to
    the viewport only clipped the list early.
*/
@media (min-width: 960px) {
    .catalog {
        height: auto;
        overflow: visible;
        max-width: 940px;
        margin: 0 auto;
        padding: 0 28px 48px;
    }

    .catalog__header {
        padding: 26px 0 16px;
        background: transparent;
    }

    .catalog__name { font-size: 32px; }
    .catalog__blurb { max-width: 62ch; font-size: 13px; }
    .catalog__tools { margin-top: 18px; }

    /* Room to type without the field running the whole width of the page. */
    .catalog__search { max-width: 420px; flex: 0 1 420px; }

    .catalog__results { overflow: visible; }

    /* The rows become one card, so the list reads as an object rather than as bands of page. */
    .catalog__list {
        border: 1px solid var(--mud-palette-lines-default);
        border-radius: 14px;
        overflow: hidden;
        background: var(--mud-palette-surface);
    }

    /* Not sticky here: the whole list is in view, so a header that follows the scroll follows
       nothing. */
    .catalog__letter {
        position: static;
        padding-left: 18px;
    }

    /*
        Board settings carries the phone's 16px gutter INSIDE its sections. The page already has
        its own padding at this width, so the two stacked up and the name field sat 16px inside
        the member list below it. One left edge for everything.
    */
    .board-section .section-label,
    .board-rename,
    .board-readonly,
    .board-danger__body,
    .board-danger__action,
    .board-note { padding-left: 0; padding-right: 0; margin-left: 0; margin-right: 0; }
}

/*
    THE DROP HIGHLIGHT, FOR FINGERS.

    MudBlazor moves a meal on touch correctly - its chess-board example does the same thing. What
    it never draws on touch is the highlight, because MudDropZone listens for dragenter and
    dragleave and for no touch event at all. plan-move.js watches the finger and sets these, so a
    tablet and a laptop show the same two looks.
*/
.plan__grid.is-touchdragging .plan__cell {
    outline: 1px dashed color-mix(in srgb, var(--mud-palette-primary) 40%, transparent);
    outline-offset: -5px;
    border-radius: 10px;
}

/*
    A meal card is a drag handle on touch, so the browser must not read a finger on it as the
    start of a scroll. MudBlazor ships touch-action:none for [draggable=true] - but on the item
    WRAPPER, and the card inside it is what a finger actually lands on.
*/
.plan__grid .plan__meal { touch-action: none; }

/*
    The kitchen picker when the board list could not be fetched.

    Quiet rather than alarming - the app still works, this one thing did not load - but present,
    because the alternative was an empty space that looked identical to having no kitchens.
*/
.app-board--failed {
    border: 0;
    background: transparent;
    font: inherit;
    cursor: pointer;
    color: var(--mud-palette-text-secondary);
}

.app-board--failed .mud-icon-root { color: var(--mud-palette-warning); }

/*
    An invitation's state, as a WORD.

    Never colour alone (design-principles.md): "Needs an Owner" is the only one carrying an
    action, and it says so in text before it says so in ink. The rest are history and read quietly.
*/
.invite-state {
    margin-left: auto;
    padding-left: 10px;
    flex-shrink: 0;
    font-size: 11px;
    font-weight: 600;
    color: var(--mud-palette-text-secondary);
}

.invite-state--pendingapproval { color: var(--mud-palette-warning); }
.invite-state--accepted { color: var(--mud-palette-success); }
.invite-state--expired,
.invite-state--rejected { color: var(--mud-palette-text-disabled); }

/* The way out of a held invitation, under the note that explains it. */
.board-upgrade { padding: 10px 0 2px; }

/*
    SURPRISE ME, against Surprise.dc.html.

    One answer, confidently. The card is the loudest thing in the dialog because a single answer is
    what ends a negotiation - and the reason under it is what stops that answer feeling arbitrary.
*/
.surprise { display: flex; flex-direction: column; gap: 14px; }

.surprise__title { display: flex; flex-direction: column; gap: 2px; }
.surprise__name { font-size: 16px; font-weight: 600; }
.surprise__where { font-size: 12px; color: var(--mud-palette-text-secondary); }

.surprise__pools { display: flex; gap: 7px; }

.surprise__thinking {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 132px;
    font-size: 13px;
    color: var(--mud-palette-text-secondary);
}

.surprise__nothing {
    margin: 0;
    min-height: 132px;
    display: flex;
    align-items: center;
    font-size: 13px;
    line-height: 1.5;
    color: var(--mud-palette-text-secondary);
}

.surprise__card {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 16px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 14px;
    background: var(--mud-palette-surface);
}

.surprise__pick {
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 21px;
    font-weight: 600;
    line-height: 1.2;
    letter-spacing: -0.02em;
}

.surprise__meta {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
}

.surprise__rating { display: flex; align-items: center; gap: 3px; }
.surprise__rating .mud-icon-root { font-size: 14px; width: 14px; height: 14px; color: var(--mud-palette-warning); }

.surprise__tags { display: flex; flex-wrap: wrap; gap: 6px; }

/* A chip that is a label rather than a control - no hover, no press, no pointer. */
.recipe-chip--static { cursor: default; pointer-events: none; }

.surprise__why {
    margin: 2px 0 0;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--mud-palette-text-primary);
}

/* The number, dialled from the card. 44px, because it is a target on a phone. */
.surprise__call {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    min-height: 44px;
    margin-top: 2px;
    font-size: 13px;
    font-weight: 600;
    color: var(--mud-palette-primary);
    text-decoration: none;
}

.surprise__call:hover { text-decoration: underline; }

.surprise__rule {
    margin: 0;
    font-size: 11.5px;
    color: var(--mud-palette-text-disabled);
}

/*
    FIXING A FLAGGED IMPORT LINE.

    The screen used to offer one button - "Skip this line" - so the only way past a line the
    parser could not read was to throw it away. The site said "ounce Spaghetti" and the household
    lost the spaghetti.

    Three fields in the order the line is written, matching the recipe form so the same job looks
    the same in both places. Stacks on a phone rather than squeezing a name into a third of a
    small screen.
*/
.import-fix {
    display: grid;
    grid-template-columns: 1fr;
    gap: 8px;
    margin-top: 10px;
}

@media (min-width: 560px) {
    .import-fix { grid-template-columns: 76px 96px minmax(0, 1fr); }
}

/*
    These three were a hand-rolled input, select and input: 13px text, their own border, their own
    focus ring. They are MudBlazor controls now, which bring all of that and a 16px field a phone
    will not zoom on - so what is left here is only what makes the quantity read as a number.

    The rules are on the inner input because the class lands on the control that wraps it.
*/
.import-fix__qty input {
    text-align: center;
    color: var(--mud-palette-primary);
    font-weight: 600;
}

/* Two actions now, so they sit on a row rather than stacking one full-width button. */
.import-line__actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }

/*
    A DIRECTION AND ITS SUB-STEPS.

    Some directions are one action and some are a heading over several - "while the pasta boils,
    do these three things". The list is drawn as a list so a cook can see at a glance that this
    step is three things, and so a screen reader says so too.

    Sized in em rather than px: the same component sets the type in a recipe and in cook mode,
    where the step is deliberately much larger, and the list has to grow with it.
*/
.stepbody__lead { margin: 0; }

.stepbody__subs {
    margin: 0.5em 0 0;
    padding-left: 1.15em;
    list-style: none;
}

.stepbody__sub {
    position: relative;
    margin-top: 0.4em;
    line-height: 1.5;
}

/*
    The marker is drawn rather than left to list-style, so it keeps the app's own colour and does
    not inherit the browser's bullet at whatever size the surrounding type happens to be.
*/
.stepbody__sub::before {
    content: "";
    position: absolute;
    left: -0.9em;
    top: 0.55em;
    width: 0.3em;
    height: 0.3em;
    border-radius: 50%;
    background: currentColor;
    opacity: .45;
}

/* A lead with a list under it is the heading for it, so it carries a little more weight. */
.stepbody__lead:not(:only-child) { font-weight: 600; }

/* The one toolbar button a direction gets: start a sub-step. Beside remove, same size, quieter. */
.rform__step-bullet {
    flex-shrink: 0;
    width: 36px;
    height: 36px;
    margin-top: 4px;
    border: 0;
    border-radius: 9px;
    background: transparent;
    color: var(--mud-palette-text-secondary);
    cursor: pointer;
}

.rform__step-bullet:hover {
    background: var(--mud-palette-action-default-hover);
    color: var(--mud-palette-primary);
}

/*
    Settings: the choices under each question. Position only - MudBlazor draws the controls
    themselves, and a gap of ours inside a toggle group would pull its joined segments apart.
*/
.setting__choices { padding: 2px 16px 0; }

/* Chips wrap; a toggle group does not. Five meals do not fit on one phone-wide line. */
.setting__chips { padding: 2px 16px 0; }

/*
    The 44px floor from principle 2. MudBlazor's medium chip measures 32px and its toggle item
    31 - comfortable with a mouse, under the floor with a thumb, and these are the only controls
    on the screen. Not the filter strip's problem: these three groups stand alone, so raising
    them changes nothing else's height.
*/
.setting__chips .mud-chip,
.setting__choices .mud-toggle-item { min-height: 44px; }

.setting__away { padding: 10px 16px 0; }

@media (min-width: 960px) {
    .setting__choices,
    .setting__chips,
    .setting__away { padding-left: 0; padding-right: 0; }
}

/* ================= the grocery list =================
   Grocery.dc.html. The one screen held at arm's length in a shop, so everything here is sized
   for a thumb first and a mouse second, and the checked state is carried by four signals -
   box, strikethrough, section and only then colour - because emerald and UT orange collide
   under red-green colour blindness (design-principles.md). */

.grocery {
    display: flex;
    flex-direction: column;
    min-height: 100%;
}

.grocery__header {
    padding: 16px 16px 0;
    background: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.grocery__title {
    display: flex;
    align-items: baseline;
    gap: 10px;
}

.grocery__name {
    margin: 0;
    font-family: "Bricolage Grotesque", Georgia, serif;
    font-size: 25px;
    font-weight: 600;
    letter-spacing: -0.025em;
}

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

/* Pushed to the end of the title line: which week this is matters, stepping between them is
   occasional, and the arrows should not compete with the heading for the eye. */
.grocery__weeks {
    margin-left: auto;
    display: flex;
    gap: 2px;
}

.grocery__filters { padding: 8px 0 10px; }

.grocery__chips { gap: 7px; }

.grocery__body { flex: 1 1 auto; }

.grocery__section {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 15px 16px 7px;
}

.grocery__section-label {
    margin: 0;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
}

.grocery__section--cart .grocery__section-label,
.grocery__section--cart .mud-icon-root { color: var(--mud-palette-success); }

.grocery__rule {
    flex: 1 1 auto;
    height: 1px;
    background: var(--mud-palette-lines-default);
}

/* On the section header rather than in a toolbar: they only mean anything once something is
   checked, and a control that is meaningless most of the time should not be permanently there. */
.grocery__section-action {
    min-width: 0;
    padding: 2px 8px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.03em;
}

.grocery__list {
    margin: 0;
    padding: 0;
    list-style: none;
    border-top: 1px solid var(--mud-palette-lines-default);
}

.grocery-row {
    display: flex;
    align-items: center;
    gap: 4px;

    min-height: 56px;
    padding: 4px 12px 4px 6px;
    background: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

/* The row is the sheet's target and the box is its own, so the box has to stop the tap from
   reaching the row - otherwise every checkoff opens the actions behind it. */
.grocery-row__box {
    flex-shrink: 0;
    margin: 0;
}

.grocery-row__body { flex: 1; min-width: 0; }

.grocery-row__head {
    display: flex;
    align-items: baseline;
    gap: 8px;
}

.grocery-row__name {
    font-size: 15px;
    font-weight: 500;
}

.grocery-row__qty {
    font-size: 16px;
    font-weight: 700;
    color: var(--mud-palette-primary);
}

.grocery-row__meta {
    display: block;
    margin-top: 2px;
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
}

/* Signal two and three: struck through, and in a section of its own. Colour is signal four and
   arrives last, which is why the text greys rather than turning green. */
.grocery-row--in-cart .grocery-row__name {
    color: var(--mud-palette-text-disabled);
    text-decoration: line-through;
}

.grocery-row--in-cart .grocery-row__qty { color: var(--mud-palette-text-disabled); }

.grocery-row--in-cart .grocery-row__meta { color: var(--mud-palette-text-disabled); }

.grocery-row__who {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;

    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: var(--mud-palette-success);
    color: var(--mud-palette-success-text);
    font-size: 9px;
    font-weight: 700;
}

.grocery-row--in-cart .grocery-row__who {
    background: var(--mud-palette-lines-default);
    color: var(--mud-palette-text-secondary);
}

.grocery-row--dismissed {
    gap: 12px;
    padding-left: 16px;
    opacity: 0.72;
}

.grocery-row--dismissed .grocery-row__name { color: var(--mud-palette-text-secondary); }

/* The trip, and where it has got to. Sticky to the bottom because the number is the answer to
   "am I nearly done", which is asked while scrolling rather than after it. */
.grocery__bar {
    position: sticky;
    bottom: 0;

    display: flex;
    align-items: center;
    gap: 12px;

    padding: 12px 16px;
    background: var(--mud-palette-surface);
    border-top: 1px solid var(--mud-palette-lines-default);
}

.grocery__progress { flex: 1 1 auto; }

.grocery__progress-text {
    font-size: 13px;
    font-weight: 600;
}

.grocery__progress-bar {
    height: 5px;
    margin-top: 6px;
}

.grocery__done {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}

.grocery__done-text {
    font-size: 13px;
    font-weight: 600;
}

.grocery__add {
    flex-shrink: 0;
    width: 50px;
    height: 50px;
}

/* ---- the add and edit sheets ---- */

.gadd,
.gedit {
    display: flex;
    flex-direction: column;
    gap: 14px;
    min-width: min(420px, 74vw);
}

.gadd__amount,
.gedit__amount {
    display: flex;
    gap: 10px;
}

.gadd__qty,
.gedit__qty { flex: 0 0 40%; }

.gadd__unit,
.gedit__unit { flex: 1 1 auto; }

.gadd__match { display: flex; flex-direction: column; }

.gadd__match-name { font-size: 14px; font-weight: 500; }

.gadd__match-meta { font-size: 12px; color: var(--mud-palette-text-secondary); }

.gadd__none,
.gadd__note,
.gedit__why,
.gedit__name-note {
    margin: 0;
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
}

.gassign__person {
    display: flex;
    align-items: center;
    gap: 12px;
}

.gassign__avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--mud-palette-primary) 15%, transparent);
    color: var(--mud-palette-primary);
    font-size: 11px;
    font-weight: 700;
}

.gassign__avatar--nobody {
    background: var(--mud-palette-background-gray);
    color: var(--mud-palette-text-secondary);
}

.gassign__name { flex: 1 1 auto; font-size: 14px; }

/*
    THE GROCERY LIST ON A DESKTOP.

    The same centred column the catalog gets, and for the same reason: these are scan-and-pick
    rows, and one stretched across a 1440px window puts the name at the far left and its actions
    at the far right with nothing in between - the eye has to cross the whole screen to connect
    the two.

    The two rules below were already here, written as though the column existed. It did not:
    nothing bounded .grocery, so zeroing the header's gutter only pushed the title flush against
    the left edge of the window, and rounding the list rounded something running the full width.
*/
@media (min-width: 960px) {
    .grocery {
        max-width: 940px;
        margin: 0 auto;
        padding: 0 28px 48px;
    }

    .grocery__header {
        padding-left: 0;
        padding-right: 0;
        background: transparent;
    }

    /* The rows become one card, so the list reads as an object rather than as bands of page -
       matching .catalog__list exactly, because they are the same kind of list. */
    .grocery__list {
        border: 1px solid var(--mud-palette-lines-default);
        border-radius: 14px;
        overflow: hidden;
        background: var(--mud-palette-surface);
    }

    /* Inside the column rather than across the window, so the progress and the add button stay
       beside the list they belong to. */
    .grocery__bar {
        padding-left: 0;
        padding-right: 0;
        background: var(--mud-palette-background);
    }

    .kitchen__lead { padding-left: 0; padding-right: 0; }
}

/* ================= feedback =================
   Built on the catalog shape - header, sections - because that is what the screen is. Only the
   parts it has no equivalent for live here. */

.feedback__field { padding: 2px 16px 0; }

/* Said out loud rather than attached quietly. Somebody reporting a bug should not discover
   afterwards that their route and their board went with it. */
.feedback__attached {
    display: flex;
    align-items: flex-start;
    gap: 7px;
}

.feedback__attached .mud-icon-root { flex-shrink: 0; margin-top: 1px; }

.feedback__error { margin: 10px 16px 0; }

.feedback__done {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    justify-content: center;
}

@media (min-width: 960px) {
    .feedback__field { padding-left: 0; padding-right: 0; }

    .feedback__error { margin-left: 0; margin-right: 0; }
}

/* ---------------------------------------------------------------------------
   Checking the kitchen before shopping (docs/product/pre-shop-kitchen-check.md).

   Deliberately built out of the grocery row rather than beside it: it is the same list,
   answering a different question, and a second visual language for the same rows would make
   the two modes feel like two features.
   --------------------------------------------------------------------------- */

.kitchen__lead {
    margin: 0;
    padding: 14px 16px 10px;
    font-size: 14px;
    line-height: 1.5;
    color: var(--mud-palette-text-secondary);
}

.kitchen-row {
    border-bottom: none;
    padding-bottom: 0;
}

/* Covered is a state, and colour is not allowed to carry it alone (CLAUDE.md). The words
   "Nothing to buy" do the work; this only reinforces them. */
.kitchen-row--covered .grocery-row__name {
    color: var(--mud-palette-text-disabled);
}

.kitchen-row__sum {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 6px;

    margin-top: 2px;
    font-size: 13px;
    line-height: 1.5;
    color: var(--mud-palette-text-secondary);
}

.kitchen-row__buy { font-weight: 600; color: var(--mud-palette-text-primary); }

.kitchen-row__mismatch {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    margin-top: 4px;
}

/* The three answers. Wraps on a phone rather than shrinking below the hit-target floor. */
.kitchen-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;

    padding: 0 12px 14px 12px;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    background: var(--mud-palette-surface);
}

.kitchen-entry {
    display: flex;
    flex-direction: column;
    gap: 8px;

    padding: 4px 12px 14px 12px;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    background: var(--mud-palette-surface);
}

.kitchen-entry__actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}

.kitchen__done {
    display: flex;
    justify-content: center;
    padding: 18px 16px 28px;
}

/* Outlined rather than filled, so it cannot compete with Add - which is the button people
   actually reach for while shopping. */
.grocery__check { flex-shrink: 0; }

/* ---------------------------------------------------------------------------
   What the household asked for (docs/product/household-check-in.md).

   Quiet on purpose. This sits under the day's plan and must read as a footnote to it: the
   research says a large count of ignored requests is failure rather than engagement, so
   nothing here competes with the meal above it for attention.
   --------------------------------------------------------------------------- */

/*
    The 16px gutter is supplied HERE, not by the container. .plan__day has no horizontal padding
    of its own - every child insets itself, the way .plan__note does with margin: 0 16px - and
    without matching that this section sat flush against the edge of the screen while the meal
    above it and the note below it did not. Caught by a screenshot; bUnit applies no CSS.
*/
.asked { margin: 22px 16px 0; }

.asked__head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 8px;
}

.asked__title {
    margin: 0;
    font-size: 13px;
    font-weight: 700;
    color: var(--mud-palette-text-primary);
}

.asked__head .mud-button-root { margin-left: auto; }

.asked__none {
    margin: 0;
    font-size: 13px;
    color: var(--mud-palette-text-secondary);
}

.asked__card {
    padding: 13px 14px;
    border-radius: 12px;
    background: var(--mud-palette-surface);
}

.asked__card + .asked__card { margin-top: 9px; }

.asked__row {
    display: flex;
    align-items: baseline;
    gap: 8px;
}

.asked__what {
    font-size: 14px;
    font-weight: 600;
    min-width: 0;
}

.asked__chip { margin-left: auto; }

.asked__who {
    margin-top: 4px;
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
}

/* The night somebody had in mind. A warm accent rather than the purple that carries actions,
   so it reads as information rather than as something to press. */
.asked__when { color: var(--mud-palette-warning-darken); font-weight: 600; }

.asked__actions {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 11px;
}

/* "Not this week" is quiet by design: turning something down should never be the easiest
   thing on the card. */
.asked__no { margin-left: auto; color: var(--mud-palette-text-secondary); }

/* Scheduled says so in WORDS as well as colour - a green tick alone fails the colour-blindness
   rule in CLAUDE.md. */
.asked__done {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-top: 9px;
    font-size: 12px;
    color: var(--mud-palette-success-darken);
}

/* ---------------------------------------------------------------------------
   Things to do before a meal (docs/product/household-check-in.md).

   Inside the meal's card, indented under it, because a task belongs to its dinner. Its own
   screen would be a chore list, which is the thing the spec says this must not become.
   --------------------------------------------------------------------------- */

.prep {
    padding: 2px 0 6px 10px;
    border-left: 2px solid var(--mud-palette-lines-default);
    margin: 4px 0 0 14px;
}

.prep__row {
    display: flex;
    align-items: center;
    gap: 6px;
    min-height: 36px;
}

.prep__box { margin: 0; flex-shrink: 0; }

.prep__what {
    flex: 1;
    min-width: 0;
    font-size: 13px;
}

/* Done is struck through as well as greyed - colour never carries it alone (CLAUDE.md). */
.prep__row--done .prep__what {
    color: var(--mud-palette-text-disabled);
    text-decoration: line-through;
}

.prep__who { width: 22px; height: 22px; font-size: 9px; flex-shrink: 0; }

/* Unassigned is a real answer, so it says so rather than leaving a hole. */
.prep__anyone {
    font-size: 11px;
    color: var(--mud-palette-text-secondary);
    flex-shrink: 0;
}

.prep__more { flex-shrink: 0; }

.prep__new {
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
    justify-content: flex-start;
}

.prep__add {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}

.prep__add .mud-input-control { flex: 1 1 140px; }

/*
    Requests below the WEEK GRID, at desktop width (design/CheckInWide.dc.html).

    The grid view and the day view have different gutters, so the section is wrapped here rather
    than taking .asked's own 16px - which belongs to the phone's day column.
*/
.plan__asked { display: none; }

@media (min-width: 960px) {
    .plan__asked { display: block; }

    /*
        22px, matching .plan__head's padding and .plan__start's margin - the gutter this layout
        uses. Not the phone's 16px: the day view and the grid view inset differently, and taking
        the wrong one left this section a few pixels out of line with the week above it.
    */
    .plan__asked .asked { margin: 24px 22px 0; }

    /*
        THE ONE THING THE WIDTH IS SPENT ON: a row instead of a column, so the week and what was
        asked of it are on screen together. Same cards, same words, same order.
    */
    .plan__asked .asked__card {
        display: inline-block;
        vertical-align: top;
        width: min(320px, 100%);
        margin: 0 12px 12px 0;
    }

    .plan__asked .asked__card + .asked__card { margin-top: 0; }

    /* Tasks inside a grid cell are tighter than on a phone - the column is narrower and the
       cell already carries the meal above them. */
    .plan__cell .prep { margin-left: 0; padding-left: 8px; }
    .plan__cell .prep__row { min-height: 30px; font-size: 12px; }

    /* A seventh of 1440 is a narrow column, and the full phrase wrapped to two lines in every
       cell. Smaller and left-aligned keeps it on one. */
    .plan__cell .prep__new { font-size: 11px; padding: 2px 4px; min-width: 0; }
}

/* ---- a new board ---- */

/*
    Indented to the checkbox's LABEL, so it reads as the explanation of that choice rather than a
    separate paragraph - the control is 48px wide, and its label starts there. Muted, because it
    answers a question somebody may not have, and full-strength text insists on being read.
*/
.new-board__hint {
    margin: 0 0 0 48px;
    font-size: 13px;
    line-height: 1.45;
    color: var(--mud-palette-text-secondary);
}

/* ---- signed out ---- */

/*
    The same centred column as the loading screen it replaces, at the same height, so the brand
    stays put and only the words beneath it change.
*/
.signedout {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    min-height: min(78dvh, 640px);
    max-width: 30rem;
    margin-inline: auto;
    padding: 32px 22px;
    text-align: center;
}

.signedout__title { margin-top: 12px; }

.signedout__body {
    max-width: 34ch;
    color: var(--mud-palette-text-secondary);
}

.signedout .copyright { margin-top: 20px; }
.mud-menu-list .copyright { padding: 10px 16px 6px; }

/* ---- copyright ---- */

/* Reached rather than read: small and quiet, at the foot of More and of the account menu -
   never under every page, where it made screens that fitted scroll for it. */
.copyright {
    margin: 0;
    padding: 24px 16px 8px;
    text-align: center;
    font-size: 12px;
    color: var(--mud-palette-text-disabled);
}

/* ---- a meal kit's ingredients (SuppliedSwitch) ---- */

.supplied-switch__hint {
    margin: 2px 0 0;
    font-size: 12px;
    line-height: 1.45;
    color: var(--mud-palette-text-secondary);
}

.import__supplied { margin-top: 14px; }

/* The way to the plans that can publish, inline with the sentence that says they exist. */
.rform__public-plans {
    margin-left: 4px;
    color: var(--mud-palette-primary);
    font-weight: 600;
}

/* ---- the other ways into a week (PlanStart.dc.html .start) ----
   plan__way*, NOT plan__start-*: that namespace is the "Last week, again" banner's, and the first
   version of these redefined .plan__start-icon from further down the file - overriding the
   banner's own icon while looking close enough to it that nothing seemed wrong. */

/* Stacked on a phone - the artboard only drew a desktop - and two equal columns from md up, so a
   board with no favourites keeps its one card at half width rather than a stretched strip. */
.plan__ways {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 10px;

    /* The page's own gutter - its sections carry their margins rather than the page padding them,
       which is what the rule this replaced (.plan__rollweek) did. */
    margin: 0 16px 14px;
}

/* The grid's own width and gutter on a wide screen - 22px, not the day view's 16 - with room
   above it, since it now follows the grid rather than a banner. */
@media (min-width: 960px) {
    .plan__ways {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 14px;
        margin: 16px 22px 0;
    }
}

/* Quieter than the preview above it: a hairline, the page's surface, no shadow. */
.plan__way.mud-button-root {
    justify-content: flex-start;
    width: 100%;
    min-height: 62px;
    padding: 14px 16px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 13px;
    background: var(--mud-palette-surface);
    text-align: left;
    text-transform: none;
}

.plan__way .mud-button-label {
    justify-content: flex-start;
    gap: 12px;
}

.plan__way.mud-button-root:hover {
    border-color: color-mix(in srgb, var(--mud-palette-primary) 45%, var(--mud-palette-lines-default));
    background: var(--mud-palette-surface);
}

.plan__way-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    flex-shrink: 0;
    border-radius: 10px;
    background: color-mix(in srgb, var(--mud-palette-primary) 10%, var(--mud-palette-surface));
    color: var(--mud-palette-info);
}

.plan__way-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.plan__way-name {
    font-size: 14px;
    font-weight: 600;
    line-height: 1.3;
    color: var(--mud-palette-text-primary);
}

/* Secondary rather than the artboard's #A5A1AC, which is about 2.5:1 at this size. */
.plan__way-note {
    font-size: 12px;
    font-weight: 400;
    line-height: 1.35;
    color: var(--mud-palette-text-secondary);
}

/* ---- adding a photo from the recipe's empty band (RecipePhotoDialog) ---- */

/* The band's own tint and proportions, so the preview shows the photo as it will sit. */
.photo-dialog__preview {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    aspect-ratio: 16 / 9;
    border-radius: 13px;
    overflow: hidden;
    background: color-mix(in srgb, var(--mud-palette-primary) 13%, transparent);
    color: color-mix(in srgb, var(--mud-palette-primary) 55%, transparent);
}

.photo-dialog__preview img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.photo-dialog__preview-note {
    font-size: 12px;
    font-weight: 600;
    color: var(--mud-palette-primary);
}

/* ---- over the board's plan: locked rows, the notice, the read-only bar ----
   subscriptions.md, "Downgrade and over-limit boards". Nothing here is an error, so nothing here
   wears a warning colour: what is locked is kept, and the page says so in its own quiet tokens.
   The lock and the words carry the meaning; the muting only supports them. */

/* A locked tile is the ordinary tile with its colour taken out - the same shape, so a locked row
   still reads as one of the list rather than as a broken one. */
.recipe-card.is-locked .recipe-card__thumb,
.catalog-row.is-locked .catalog-row__tile {
    background: var(--mud-palette-action-disabled-background);
    color: var(--mud-palette-text-secondary);
}

/* The name stays readable - secondary, not disabled - because it is still theirs and still the
   thing they would delete. */
.recipe-card.is-locked .recipe-card__name,
.catalog-row.is-locked .catalog-row__name {
    color: var(--mud-palette-text-secondary);
    font-weight: 500;
}

.recipe-card__locked {
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
}

.over-limit {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    margin: 12px 16px;
    padding: 14px 16px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 13px;
    background: var(--mud-palette-surface);
}

.over-limit__icon {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 34px;
    height: 34px;
    border-radius: 10px;
    background: var(--mud-palette-action-disabled-background);
    color: var(--mud-palette-text-secondary);
}

/* The words claim the rest of the row on a phone, so the button has to wrap under them rather
   than squeezing them into a column - which the first version did, at 390px. */
.over-limit__text { flex: 1 1 calc(100% - 46px); min-width: 0; }

.over-limit__title {
    margin: 0;
    font-size: 14px;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
}

.over-limit__body {
    margin: 2px 0 0;
    max-width: 62ch;
    font-size: 13px;
    line-height: 1.45;
    color: var(--mud-palette-text-secondary);
}

/* Phone first: the button under the words, lined up with them, rather than squeezing them. */
.over-limit { flex-wrap: wrap; }
.over-limit__action.mud-button-root { flex-shrink: 0; margin-left: 46px; }

@media (min-width: 600px) {
    .over-limit { flex-wrap: nowrap; }
    .over-limit__text { flex: 1 1 auto; }
    .over-limit__action.mud-button-root { align-self: center; margin-left: 0; }
}

.read-only-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 12px;
    padding: 10px 16px;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    background: var(--mud-palette-surface);
}

.read-only-bar__icon { color: var(--mud-palette-text-secondary); flex-shrink: 0; }

/* Same rule as the notice: the sentence takes the row on a phone and the actions go under it.
   Letting it shrink set it one word to a line at 390px. */
.read-only-bar__text {
    flex: 1 1 calc(100% - 32px);
    min-width: 0;
    margin: 0;
    font-size: 13px;
    line-height: 1.4;
    color: var(--mud-palette-text-primary);
}

.read-only-bar__actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }

.read-only-bar__switch.mud-button-root { text-transform: none; }

@media (min-width: 960px) {
    .read-only-bar { flex-wrap: nowrap; }
    .read-only-bar__text { flex: 1 1 auto; }
}
