html, body {
    margin: 0;
    font-family: 'DM Sans', 'Helvetica Neue', Helvetica, system-ui, sans-serif;
}

/* —————————————————————————————————————————————————————————————
   Mud surface overrides — brand: no white boxes.

   The MudBlazor theme sets `Surface` to the cream page background
   (`#F5F2E8`, see HeadwayflowTheme.cs / PaperSurface), so every
   MudPaper / MudCard / dialog / menu shares one warm tone with the
   page. To still read as a defined card the way the calendar
   integrations page does, every MudPaper picks up a `--stone-200`
   border and a soft warm shadow here. Tooltips and popovers are
   excluded because they have their own chrome and would look heavy
   with an extra border.
   ————————————————————————————————————————————————————————————— */
.mud-paper:not(.mud-tooltip):not(.mud-popover-paper) {
    border: 1px solid #DDD0B8; /* --stone-200 */
}

.mud-paper.mud-elevation-1,
.mud-paper.mud-elevation-2 {
    box-shadow:
        0 4px 14px rgba(74, 62, 46, 0.05),
        0 1px 2px rgba(40, 25, 18, 0.04);
}

/* —————————————————————————————————————————————————————————————
   Menu popovers — keep a scrolling menu from taking the page with it.

   HeadwayflowPopoverDefaults means a menu now flips above its anchor
   rather than being capped whenever it has room either side, so this
   is for the case that is left: a viewport too short to hold the menu
   in either direction, where MudBlazor caps the list and it scrolls
   for real. Scroll chaining is worse there than usual — reaching the
   end of the list hands the wheel to the timeline, MudBlazor
   repositions the popover to follow the anchor it is pinned to, and
   the menu walks up the screen out from under the pointer.
   ————————————————————————————————————————————————————————————— */
.mud-popover .mud-menu-list {
    overscroll-behavior: contain;
}

/* —————————————————————————————————————————————————————————————
   Mud button overrides — flat by default.

   Drop MudBlazor's default Material elevation shadow from every
   MudButton / MudIconButton / MudFab so the warm-cream brand
   reads as flat surfaces. Only the prominent "Add task" CTAs
   keep their elevation so they remain the most visible
   affordance on each page:

     • `.timeline-add-task-btn` — desktop "Add Task" on Timeline
     • `.timeline-fab`          — mobile add-task FAB

   EmptyDayState's "Add a task" (`.empty-day-cta-btn`) is a native
   button, not a MudButton, so it declares its own elevation in
   timeline.css rather than appearing here.

   The `:hover` selectors are restated so MudBlazor's heavier
   hover elevation rule (higher specificity) doesn't reintroduce
   a shadow on the flattened buttons. New add-task surfaces?
   Append the class here, no per-page CSS needed.
   ————————————————————————————————————————————————————————————— */
.mud-button-root,
.mud-button-root:hover,
.mud-button-root:focus,
.mud-button-root:active,
.mud-fab,
.mud-fab:hover,
.mud-fab:focus,
.mud-fab:active {
    box-shadow: none !important;
}

.mud-button-root.timeline-add-task-btn,
.mud-fab.timeline-fab {
    box-shadow:
        0 3px 1px -2px rgba(74, 62, 46, 0.20),
        0 2px 2px 0    rgba(74, 62, 46, 0.14),
        0 1px 5px 0    rgba(74, 62, 46, 0.12) !important;
}

.mud-button-root.timeline-add-task-btn:hover,
.mud-fab.timeline-fab:hover {
    box-shadow:
        0 2px 4px -1px rgba(74, 62, 46, 0.20),
        0 4px 5px 0    rgba(74, 62, 46, 0.14),
        0 1px 10px 0   rgba(74, 62, 46, 0.12) !important;
}

/* —————————————————————————————————————————————————————————————
   Snackbar action button — legible on the dark toast.

   Every snackbar action in the app (Undo on completion, on a Plan
   or Timeline change, the Someday soft nudge) is added with
   `ActionColor = Color.Inherit` so the label tracks the toast's own
   text colour, and none of them set ActionVariant. MudBlazor's
   fallback chain is `ActionVariant ?? SnackbarVariant ?? Text`, and
   SnackbarVariant defaults to `Variant.Filled` — so these render as
   FILLED buttons carrying no palette colour.

   MudBlazor has no `.mud-button-filled-inherit` rule, so such a
   button has no background of its own and inherits white text from
   `.mud-alert-filled-success`. That reads fine at rest. The moment
   it is hovered, focused or pressed, this fires:

     .mud-button-filled:hover { background-color:
         var(--mud-palette-action-disabled-background) }

   — the DISABLED background token, `#EDE5D6` cream in our palette,
   borrowed as a generic fallback for a colourless filled button. On
   our Success toast (`#4A3E2E`) that paints pale cream under white
   text: a contrast ratio of 1.25:1, i.e. the label disappears
   exactly when the user reaches for it.

   Replaced with a translucent white overlay, which stays keyed to
   whatever the toast underneath is: 7.7:1 at rest, 5.2:1 hovered on
   the Success brown, and it cannot go pale on a dark surface the way
   a fixed cream can. Specificity here is (0,3,0) against MudBlazor's
   (0,2,0), so no !important is needed.
   ————————————————————————————————————————————————————————————— */
.mud-snackbar .mud-snackbar-action-button {
    color: inherit;
    font-weight: 600;
    background-color: rgba(255, 255, 255, 0.10);
}

.mud-snackbar .mud-snackbar-action-button:hover,
.mud-snackbar .mud-snackbar-action-button:focus-visible,
.mud-snackbar .mud-snackbar-action-button:active {
    background-color: rgba(255, 255, 255, 0.24);
}

/* —————————————————————————————————————————————————————————————
   Snackbar shadow.

   MudBlazor ships the stock Material elevation: three layers of flat black
   at 20/14/12% opacity. Every other shadow in the product is warm brown —
   rgba(40, 25, 18, …) over an rgba(74, 62, 46, …) spread, throughout
   timeline.css and in the Mud surface overrides above — so the toast was the
   one surface in the app dropping a grey shadow onto cream. Matched to the
   floating tooltip's, which is the nearest thing to it: something drawn on
   top of the page rather than sitting in it.
   ————————————————————————————————————————————————————————————— */
.mud-snackbar {
    box-shadow:
        0 6px 20px rgba(40, 25, 18, 0.18),
        0 2px 6px  rgba(40, 25, 18, 0.12);
}

/* —————————————————————————————————————————————————————————————
   Undo vs. the close ✕ — pointer targets.

   MudSnackbarElement renders the action button and the close button as
   siblings, separated by nothing but the `ms-2` utility (8px) that MudBlazor
   puts on the close button. With a 26px close target that is a mis-tap
   waiting to happen, and the undo window is precisely when someone is moving
   fast: tapping ✕ when they meant Undo dismisses the only route back.

   `gap` rather than a margin override, because `.ms-2` carries !important —
   flex gap composes with the existing margin instead of fighting it, so the
   two controls end up 16px apart with no specificity war.
   ————————————————————————————————————————————————————————————— */
.mud-snackbar .mud-snackbar-content-action {
    gap: 0.5rem;
}

/* —————————————————————————————————————————————————————————————
   The toast on a phone.

   MudBlazor gives the snackbar no responsive treatment whatsoever:
   `min-width:288px; max-width:500px` on the toast and `top:24px; right:24px`
   on the fixed container, at every viewport. On a 375px screen that resolves
   to a toast pinned FLUSH to the left screen edge with a 24px gutter only on
   the right — the asymmetry is most of why it reads as a misplaced overlay
   rather than part of the app — carrying 16px body type in a box 97px tall,
   which covers the page heading and the menu button behind it. Two of them
   stacked cover the whole top third of the screen.

   Below 640px it becomes a conventional full-bleed mobile snackbar instead:
   symmetric gutters, one type step down, and the chrome (icon, button
   padding, the -8px pull on the action group) tightened so the extra width
   goes to the message. Same three-line message now fits in two lines at
   roughly 60px. Nothing here applies above 640px — MudBlazor's own geometry
   is fine once there is room for it.
   ————————————————————————————————————————————————————————————— */
@media (max-width: 640px) {
    /* Id specificity, so this wins over whichever
       `.mud-snackbar-location-*` class the configured position supplies.
       `transform` is reset for the two `-center` positions, which centre
       themselves with translateX and would otherwise pull the toast
       half a screen sideways once it spans the full width. */
    #mud-snackbar-container {
        left: 0.625rem;
        right: 0.625rem;
        transform: none;
    }

    /* No `width: 100%` — the toast is a block-level flex container, so
       clearing MudBlazor's 288/500px floor and ceiling is enough for it to
       take the container's width on its own. */
    .mud-snackbar {
        min-width: 0;
        max-width: none;
        margin-top: 0.5rem;
        padding: 0.25rem 0.5rem 0.25rem 0.625rem;
        border-radius: 14px;
        font-size: 0.875rem;
        line-height: 1.45;
    }

    /* MudIcon carries its own font-size, so the size lives on .mud-icon-root
       and not on the wrapper MudBlazor sets 22px on. */
    .mud-snackbar .mud-snackbar-icon {
        flex: 0 0 auto;
        padding: 0;
        margin-inline-end: 0.5rem;
    }

    .mud-snackbar .mud-snackbar-icon .mud-icon-root {
        font-size: 1.25rem;
    }

    .mud-snackbar .mud-snackbar-content-message {
        padding: 0.375rem 0;
    }

    /* MudBlazor's -8px pull is halved rather than dropped. Kept, because the
       ✕ below is grown well past its glyph and the pull lets that extra
       target sit in the toast's own padding instead of charging the message
       column for it; halved, because at the full -8px a 40px target starts
       1px from the screen edge. */
    .mud-snackbar .mud-snackbar-content-action {
        margin-inline-end: -0.25rem;
        padding-inline-start: 0.375rem;
    }

    .mud-snackbar .mud-snackbar-action-button {
        min-width: 0;
        padding: 0.3rem 0.55rem;
        font-size: 0.8125rem;
    }

    /* 26px square as MudBlazor sizes it (Size.Small, 3px of padding) — under
       every phone-target guideline going, and 8px from a 74px Undo button.
       Sized explicitly rather than by padding so the target is stated as the
       40px it needs to be, and centred by .mud-button-root's inline-flex. */
    .mud-snackbar .mud-snackbar-close-button {
        width: 2.5rem;
        height: 2.5rem;
        padding: 0;
    }
}

/* The default link colour.
 *
 * This was #594ae2 — MudBlazor's violet, inherited from the project template and the only
 * off-brand hue anywhere in the product. Three stylesheets had grown per-site overrides whose
 * comments named this rule as the reason they existed, which is what a wrong default looks like
 * from the inside.
 *
 * --stone-700 rather than --amber-600, which is the token labelled "amber text on cream" and
 * would have been the more characterful pick: it measures 4.35:1 against --surface-base, just
 * under the 4.5:1 AA needs for body-size text. stone-700 is 9.3:1 and still clearly warmer and
 * lighter than --ink-primary body copy, so a link reads as a link without the underline having
 * to do all the work.
 *
 * Declared as a var with a literal fallback: every host that loads this sheet also loads either
 * tokens.css or timeline.css, both of which declare --stone-700, but the fallback means a
 * stylesheet-order accident degrades to the right colour rather than to browser blue. */
a {
    color: var(--stone-700, #4A3E2E);
}

.content {
    padding-top: 1.1rem;
}

h1:focus {
    outline: none;
}

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

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

.validation-message {
    color: #e50000;
}

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

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

/* Failed-payment banner — sits above every page in the web shell -------------
   Amber, not red: most declines are an expired card rather than a refusal to
   pay, and an accusatory banner pushes people toward cancelling instead of
   fixing. It appears from the first failed charge, through the whole grace
   period, so the user has the full window to act rather than discovering the
   problem when the app stops accepting new tasks. */

.hf-pay-banner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 0 0 1rem;
  padding: 0.8rem 1rem;
  border: 1px solid rgba(200, 152, 72, 0.45);
  border-radius: 12px;
  background: rgba(200, 152, 72, 0.12);
  font-family: 'DM Sans', system-ui, sans-serif;
}

.hf-pay-banner-copy {
  flex: 1 1 20rem;
  min-width: 0;
}

.hf-pay-banner-title {
  margin: 0 0 0.1rem;
  font-size: 0.92rem;
  font-weight: 700;
  color: var(--ink-primary);
}

.hf-pay-banner-msg {
  margin: 0;
  font-size: 0.87rem;
  line-height: 1.5;
  color: var(--stone-700);
}

/* Overrides the global violet anchor colour set earlier in this file. */
.hf-pay-banner-action {
  flex: 0 0 auto;
  padding: 0.45rem 0.85rem;
  border: 1px solid var(--stone-500);
  border-radius: 10px;
  background: var(--stone-500);
  color: var(--surface-base) !important;
  font-size: 0.85rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}

.hf-pay-banner-action:hover {
  background: var(--stone-600);
  border-color: var(--stone-600);
}

.hf-pay-banner-close {
  flex: 0 0 auto;
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--stone-700);
  cursor: pointer;
  padding: 0.2rem;
  border-radius: 6px;
  line-height: 0;
}

.hf-pay-banner-close svg {
  width: 1rem;
  height: 1rem;
}

.hf-pay-banner-close:focus-visible,
.hf-pay-banner-action:focus-visible {
  outline: 2px solid var(--stone-500);
  outline-offset: 2px;
}

@media (max-width: 560px) {
  .hf-pay-banner-action {
    flex: 1 1 auto;
    text-align: center;
  }
}

/* —————————————————————————————————————————————————————————————
   App start-up — one spinner for every stage of it.

   Opening the app walks through several independent waits: the host
   page before Blazor exists, route authorization, the subscription
   gate, then the routed page's own first fetch. Each used to bring its
   own indicator — bare "Loading..." text, then a medium spinner, then
   a large one — so a cold start read as three unrelated waits stacked
   end to end rather than one app opening.

   This is the single treatment they all share. Same spinner, same
   size, same distance down the screen, so the chrome fades in around
   an indicator that never moves or resizes. The size is
   MudProgressCircular's default (medium / 40px); every stage but one
   renders that component, and the exception — the pre-boot splash in
   the MAUI host page, which has no Blazor to render it — hand-rolls a
   copy in Client.Mobile/wwwroot/app.css.
   ————————————————————————————————————————————————————————————— */
.hf-loading-pane {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 40vh;
}

/* —————————————————————————————————————————————————————————————
   Web: Today fits the window.

   timeline.css gives the timeline `min-height: 100vh`, but in the web
   layout it sits below .hf-web-page-topbar and above MudContainer's
   bottom padding, so that claim overshot the window by both and an
   empty day — everything on one screen — still scrolled. The phone app
   had the same overshoot and takes the same cure (Client.Mobile's
   app.css, "Today fits the screen"): every box from a full-height root
   down to the timeline passes the height on, and the timeline grows
   into what is really left. Taller content still grows the page, so a
   full day scrolls as before.

   Here rather than in the web layout's scoped MainLayout.razor.css:
   MudMainContent and MudContainer are component roots, which never
   carry the scope attribute and have no ancestor that does, so no
   scoped rule — ::deep or not — can reach them. The root is
   MudMainContent, not MudLayout, because MudLayout is `height: 100%`
   of a body that has no height.

   Every selector goes through :has(), so the other pages keep the
   plain block layout they were built against, and a browser without
   :has() drops these rules together and keeps the old 100vh.
   ————————————————————————————————————————————————————————————— */
.mud-main-content.hf-web-content:has(.timeline-page) {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    min-height: 100dvh;
}

.hf-web-content > .hf-web-main-container:has(.timeline-page),
.hf-web-main-container > .hf-web-page-shell:has(.timeline-page),
.hf-web-page-shell > .hf-web-page-body:has(> .timeline-page),
.hf-web-page-body > .timeline-page:has(> .timeline) {
    flex: 1 0 auto;
    display: flex;
    flex-direction: column;
}

.hf-web-page-body > .timeline-page:has(> .timeline) > .timeline {
    flex: 1 0 auto;
    min-height: 0;
}

/* Fresh morning and All-cleared render neither the Done-today pill nor
   the FAB (Timeline.razor), so the 4.5rem the timeline keeps clear for
   them is empty space under the composition, and since both centre
   their column it would also lift it 2.25rem above the middle. */
.hf-web-content .timeline:has(> .empty-day-fresh, > .empty-day-cleared) {
    padding-bottom: 0;
}
