/* ============================================================================
   JEM Groom App — standalone stylesheet.
   Ports the design tokens + base classes from assets/jem.css and the kept screen
   styles: screen 20 (20-mobile-responsive-grids.html), screen 20c (the 4-step New
   Movement wizard, 20c-mobile-new-movement.html) and screen 20d (Movement details,
   20d-mobile-movement-details.html).
   No external stylesheet and no third-party font CDN: the brand faces are SELF-HOSTED
   from wwwroot/fonts, so the app stays fully self-contained / offline capable.
   Dropped from the mock-ups: .stage, the explain panels, the gesture demo, the phone
   bezel / status bar / notch and the story-export chrome — the real device frames it.
   ============================================================================ */

/* ============================================================================
   Brand faces — Proxima Nova (body) and Minerva Modern (display).

   The same files and the same declarations as JEM (Juddmonte.Jem.Web
   Assets/styles/fonts.scss), copied rather than referenced: the two apps deploy
   separately, and this one is meant to run with no external asset host.

   Minerva ships three real cuts mapped to the standard CSS weight scale, so
   font-weight picks the drawn weight rather than a browser-synthesised bold.
   font-display: swap so a slow face never blanks the text — it falls through
   --font-display's stack to Proxima Nova and then to the system UI font.
   ============================================================================ */
@font-face {
  font-family: 'proxima_nova';
  src: url('../fonts/proxima_nova_font-webfont.woff2') format('woff2'),
       url('../fonts/proxima_nova_font-webfont.woff') format('woff');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'proxima_nova';
  src: url('../fonts/proxima_nova_bold_font-webfont.woff2') format('woff2'),
       url('../fonts/proxima_nova_bold_font-webfont.woff') format('woff');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'minerva_modern';
  src: url('../fonts/minervamodern_regular-webfont.woff2') format('woff2'),
       url('../fonts/minervamodern_regular-webfont.woff') format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'minerva_modern';
  src: url('../fonts/minervamodern_bold-webfont.woff2') format('woff2'),
       url('../fonts/minervamodern_bold-webfont.woff') format('woff');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'minerva_modern';
  src: url('../fonts/minervamodern_black-webfont.woff2') format('woff2'),
       url('../fonts/minervamodern_black-webfont.woff') format('woff');
  font-weight: 900;
  font-style: normal;
  font-display: swap;
}

/* ============================================================================
   JEM design tokens — the same palette as JEM (Juddmonte.Jem.Web
   Assets/styles/_jem-tokens.scss), so the two Juddmonte apps look like one family.
   Written as plain CSS: the source is SCSS and its `//` comments are NOT valid
   here — CSS would treat the rest of the line as a declaration and silently drop it.

   ── HOW THIS PALETTE WORKS — read before adding or picking a colour ──────────

   THE NUMERIC SUFFIXES ARE BRAND-MANDATED TINTS, NOT ARBITRARY SHADES. Juddmonte's
   brand guidelines supply five core colours and state: "Percentages of 100%, 80%,
   60%, 40% and 20% can be used from any of the core palette colours. No other
   percentage may be used." The -80/-60/-40/-20 suffixes ARE those percentages, so:
     - DO pick from the five steps that exist. DO NOT invent a -70, a -90, or a
       one-off hex.
     - Every step is a TINT — lower number = LIGHTER (closer to white), never darker.
       There is no darker-than-100% step. If you need a darker state (e.g. a hover),
       do it with a filter or an overlay, not by inventing a token.

   THE BRAND SECTION IS BRAND ONLY. pink / green / forest / charcoal / gainsboro are
   Juddmonte's own identity. Semantic UI colours (info / warning / danger) are
   deliberately NOT in it — they are web convention, named the way Bootstrap and
   every other UI framework names them, and are free to change without touching brand.

   A TOKEN THAT IS NOT DEFINED BELOW DOES NOT EXIST. An undefined var() does not fail
   the build — it silently drops the declaration — so a plausible-looking name will
   compile and simply render nothing. Check the list before reaching for one.

   --jem-forest and --jem-gainsboro were added for completeness of the brand set;
   they may show as unused. That is intentional — do not prune them.
   ============================================================================ */
:root {
  /* ── Juddmonte brand palette ──────────────────────────── */
  --jem-pink:         #f277c6;
  --jem-pink-80:      #f592d1;
  --jem-pink-60:      #f7addd;
  --jem-pink-40:      #fac9e8;
  --jem-pink-20:      #fce4f4;

  --jem-green:        #00af66;
  --jem-green-80:     #33bf85;
  --jem-green-60:     #66cfa3;
  --jem-green-40:     #99dfc2;
  --jem-green-20:     #ccefe0;

  --jem-forest:       #005844;
  --jem-forest-80:    #337969;
  --jem-forest-60:    #669b8f;
  --jem-forest-40:    #99bcb4;
  --jem-forest-20:    #ccdeda;

  --jem-charcoal:     #333f48;
  --jem-charcoal-80:  #5c656d;
  --jem-charcoal-60:  #858c91;
  --jem-charcoal-40:  #adb2b6;
  --jem-charcoal-20:  #d6d9da;

  --jem-gainsboro:    #d9e1e2;
  --jem-gainsboro-80: #e1e7e8;
  --jem-gainsboro-60: #e8edee;
  --jem-gainsboro-40: #f0f3f3;
  --jem-gainsboro-20: #f7f9f9;

  /* ── Other web colours ────────────────────────────────── */
  --jem-info:         #5fa888;
  --jem-info-soft:    #cfe5d9;
  --jem-warning:      #d97706;
  --jem-warning-soft: #fef3c7;
  --jem-danger:       #d12b3f;
  /* LATE, not broken. A softer red than --jem-danger on purpose: it reads against the charcoal
     header, where the danger red goes muddy, and overdue work is a nudge rather than an error. */
  --jem-late:         #ff7878;
  --jem-danger-soft:  #fde2e5;

  /* ── Neutrals ─────────────────────────────────────────── */
  --ink:    #0f172a;
  --ink-2:  #334155;
  --ink-3:  #64748b;
  --ink-4:  #94a3b8;
  --line:   #e2e8f0;
  --line-2: #cbd5e1;
  --bg:     #f6f7f9;
  --bg-2:   #eef1f5;
  --card:   #ffffff;

  /* ── Typography ───────────────────────────────────────── */
  --font:      proxima_nova, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  /* Display face for headings and top-level navigation. Falls back through the
     whole of --font's stack, so a failed/slow Minerva load degrades to
     proxima_nova rather than to a system serif. */
  --font-display: minerva_modern, proxima_nova, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  /* ── Shape / elevation ────────────────────────────────── */
  --radius: 6px;
  --radius-lg: 10px;
  --shadow-1: 0 1px 2px rgba(15, 23, 42, 0.06);
  --shadow-2: 0 4px 14px rgba(15, 23, 42, 0.08);
  --shadow-3: 0 10px 30px rgba(15, 23, 42, 0.12);
}

/* ---------- Base (from jem.css) ---------- */
* { box-sizing: border-box; }
html, body {
  margin: 0; padding: 0;
  height: 100%;
  background: var(--jem-charcoal); /* frames the app column on wider screens */
  color: var(--ink);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
  /* NO DOUBLE-TAP ZOOM, anywhere (JGMA-46). `manipulation` is pan + pinch-zoom without double-tap,
     and touch-action INTERSECTS down the tree, so this changes no gesture region the app already
     declares: pan-y, pinch-zoom and none stay as they are, and an `auto` (declared, or the default
     on a region that declares nothing) loses only double-tap zoom. Pinch is stopped separately — by
     the viewport tag on Android and the gesture listeners in index.html on iOS. ZoomPreventionTests. */
  touch-action: manipulation;
}
a { color: inherit; text-decoration: none; }
button { font-family: inherit; }

/* ── Display face on every heading, as in JEM ────────────────────────────────
   Minerva Modern for h1-h6, plus the nav-bar titles: those are <div class="title">
   rather than headings (the bar is chrome, not document structure), so an element
   selector alone would miss the one place the app most looks like a header.
   Deliberately element-based otherwise: headings are written as plain <h1>-<h4>
   across Home, the wizard and Movement details, so anything class-based would have
   to be chased across files and would silently miss the next one added.
   No font-weight here — each heading keeps the weight its own rule sets, and the
   three Minerva cuts (400/700/900) mean the real weight is chosen rather than a
   browser-synthesised bold. No !important either: unlike JEM there is no Radzen
   here setting its own font-family on headings. */
h1, h2, h3, h4, h5, h6,
.m-nav .title,
.m-header .title { font-family: var(--font-display); }
/* Force native controls to render light so text stays visible even under an OS dark theme. */
input, select, textarea { color-scheme: light; }
select option { color: var(--ink); background: #fff; }

/* ---------- Layout: the one viewport-height column (JGMA-84) ----------
   MainLayout's wrapper. The page — an .app-shell or an .auth-status — FILLS what is left under the
   environment banner, instead of each claiming the full viewport height itself. With a full 100dvh
   of its own, the banner would push it 15px past the bottom of the screen, where
   `html, body { overflow: hidden }` would cut it off. */
.layout {
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
}
/* has-env-banner hands the status-bar inset to the banner, so .m-nav does not pad it twice. */
.layout.has-env-banner { --top-inset: 0px; }

/* The red DEV ENVIRONMENT banner (JGMA-84), shown only in UAT. Above the page rather than inside
   .m-header, so it takes its own space and needs no negative margin. padding-top clears the
   status bar in the installed iPhone app (viewport-fit=cover + black-translucent). */
.env-banner {
  flex: 0 0 auto;
  background: #db0000;
  width: 100%;
  text-align: center;
  color: #fff;
  font-weight: bold;
  font-size: 12px;
  padding-top: env(safe-area-inset-top, 0px);
}

/* ---------- App shell (replaces each mock-up's phone .inner) ---------- */
.app-shell {
  position: relative;
  width: 100%;
  max-width: 480px;
  margin: 0 auto;
  /* Fills .layout below the banner (JGMA-84); it used to be a fixed 100vh / 100dvh of its own. */
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  overflow: hidden;
}

/* ============================================================================
   Screen 20 — Today's moves (Pages/Home.razor)
   ============================================================================ */

/* ---------- Header ---------- */
.m-header {
  background: var(--jem-charcoal); color: #fff;
  /* An even 10px (JGMA-9). It read `calc(env(safe-area-inset-top,0px) + 14px) 18px 14px`, which made
     a header now carrying four rows — title, day strip, farm, tabs — taller than the list it
     introduces. */
  padding: 10px;
  /* Positioned ONLY to win the paint order against a pulled .m-list (JGMA-10). A transform creates a
     stacking context, so while the list is being pulled it paints in the positioned layer — above
     every non-positioned sibling. Unpositioned, this header would have the list slide OVER it, tabs
     and all. Pairs with .m-list's z-index below; .m-fab's 20 still clears both.

     BOTH OF THESE MAKE A SHEET RENDERED INSIDE THIS HEADER UNUSABLE, so do not put one here (JGMA-58).
     Positioned, it becomes the containing block, and a `.sheet`'s `bottom: 0` pins to the bottom of
     this 228px header rather than the screen; the stacking context then traps the sheet in the
     z-index-2 layer, under .m-fab's 20. The farm switcher hit both and now renders through a section
     outlet into .app-shell, beside the other three sheets. See FarmSelector. */
  position: relative;
  z-index: 2;
}
.m-header .top { display: flex; align-items: center; gap: 5px; }
.m-header .top .title { font-size: 19px; font-weight: 700; margin: 0; }
/* Titles are <h1> for semantics/FocusOnNavigate; hide the programmatic focus ring. */
h1.title:focus { outline: none; }
/* The slot JGMA-12 fills with the magnifier. A FIXED 32x32, so the top row does not reflow when it
   arrives.

   IT ALSO CARRIES THE AUTO MARGIN NOW, which is what right-aligns the trailing group. That margin
   belongs to whichever control is FIRST in that group and has moved each time the first one changed:
   the Online pill, then the log-out button, then Reset — and now this, because the Reset button was
   removed. `.m-header .top .reset` went with it; nothing else used that class. */
.m-header .top .search-slot {
  width: 32px; height: 32px; flex: 0 0 32px; display: block;
  margin-left: auto;
}
/* The name-and-date line lives inside .dpick-head now and takes that row's treatment entirely. It
   was its own 12px flex row here, and held the reset button. */
.m-header .who { display: inline; }

/* Logout button (header + wizard nav) */
.logout-btn {
  width: 30px; height: 30px; border-radius: 8px; flex: 0 0 30px;
  display: grid; place-items: center; padding: 0;
  color: var(--ink-4); background: transparent; border: 0; cursor: pointer;
  transition: background 140ms, color 140ms;
  -webkit-tap-highlight-color: transparent;
}
/* Hover AND press, the same treatment every other button in this app got — it only had :active, so
   on a pointer it gave no sign it was a control until you clicked it. */
@media (hover: hover) {
  .logout-btn:hover { background: rgba(255, 255, 255, 0.14); color: #fff; }
}
.logout-btn:active { background: rgba(255, 255, 255, 0.14); color: #fff; }
.logout-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/* Logout / delete confirmation dialog actions */
.logout-actions { display: flex; gap: 10px; margin-top: 4px; }
.logout-actions button { flex: 1; border: 0; padding: 14px; border-radius: 12px; font-weight: 700; font-size: 15px; cursor: pointer; font-family: inherit; }
.logout-actions .la-cancel { background: var(--jem-gainsboro-40); color: var(--ink-2); }
/* Same reasoning as .sheet-actions .destructive — this is the "Delete Move?" confirm, the most
   destructive action in the app, so it cannot be the one button still wearing the accent colour. */
.logout-actions .la-confirm { background: var(--jem-danger); color: #fff; }


/* Location bar (full-width switcher in the header / wizard) */
.m-locbar { background: var(--jem-charcoal); padding: 0 14px 12px; flex: 0 0 auto; }
/* CENTRED, AND NO CARET. The name is the whole content of this control now, so it is centred across
   the full width of the bar rather than set left with a chevron pushed to the right margin. The
   caret went with it: .lb-caret is gone, and with nothing left to space away from the label the
   flex `gap` went too. */
.m-locbar .lb-btn { width: 100%; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.1); border: 0; border-radius: 9px; padding: 9px 12px; color: #fff; cursor: pointer; font: inherit; }
.m-locbar .lb-btn:active { background: rgba(255,255,255,0.2); }
.m-locbar .lb-name { font-size: 13px; font-weight: 700; }
/* EXACTLY ONE FARM: the same text with no button around it (§6.2 asks for static text, not a
   one-item dropdown), centred to match the switchable form above.
   5px OF VERTICAL PADDING, which is where this stops matching .lb-btn. It was 9px 12px, so the row
   kept the same height whether or not this groom could switch; there is no button here for that to
   be the inside of, so it sits tighter — but not flush. Zero was tried and read as cramped against
   the day strip above and the tabs below. */
.m-locbar .lb-static { display: block; text-align: center; padding: 5px 12px; font-size: 13px; font-weight: 700; color: #fff; }
/* Zero farms (§6.2) and the inline retryable failure (§8). Both stand where the selector would. */
/* Left padding only on the right, so the Retry beside it sits against the bar's own edge — the same
   shape as .loc-fail above. 5px vertically, matching .lb-static. */
.m-locbar .loc-none { margin: 0; padding: 5px 0 5px 12px; font-size: 13px; color: var(--ink-4); }
/* THE RETRYABLE FAILURE STATE. One row: what went wrong on the left, what to do about it hard right,
   so the action is where the thumb already is rather than tucked under the sentence. Both take the
   late red — it is the same "something needs you" register as an overdue day, and it stops the row
   reading as ordinary header furniture. */
/* Both states put a message against a button, so both lay out the same way. */
.m-locbar:has(.loc-fail),
.m-locbar:has(.loc-none-retry) { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.m-locbar .loc-fail { margin: 0; padding: 5px 0 5px 12px; font-size: 13px; font-weight: 700; color: var(--jem-late); }
/* Resting state only. The hover wash is .mini-btn's and stays as it is everywhere else. */
.m-locbar .loc-retry { flex: 0 0 auto; color: var(--jem-late); border-color: var(--jem-late); }
/* The zero-farm Retry keeps .mini-btn's neutral chrome. NOT .loc-retry's red: nothing has failed
   here — the derivation succeeded and the answer was none — and colouring it as a fault would say
   otherwise. */
.m-locbar .loc-none-retry { flex: 0 0 auto; }
/* Inside the navy header the bar has no chrome of its own — just spacing. */
.m-header .m-locbar { background: transparent; padding: 0; margin: 8px 0 10px; }

/* Location switcher sheet list */
.sheet .loc-list { display: grid; gap: 8px; margin-top: 6px; }
.loc-list { display: grid; gap: 8px; }
/* A BUTTON, not a div (JGMA-58). It was a div with an @onclick, which no keyboard can reach and no
   screen reader announces as actionable — and a :focus style was asked for, which a div cannot have
   without a tabindex. `font: inherit` and `width: 100%` undo what the button element brings with it. */
.loc-row { display: flex; align-items: center; gap: 12px; padding: 12px; border: 1px solid var(--line); border-radius: 12px; background: #fff; cursor: pointer; width: 100%; font: inherit; text-align: inherit; color: inherit; }
/* The CURRENT farm, when one is shown. Section 6.3 excludes it from this list today, so nothing
   carries .on — this is here so the styling is right the moment that changes, and because the
   trailing-count rules below already depend on it. */
.loc-row.on { border-color: var(--jem-green); background: var(--jem-green); color: #fff; }
.loc-row.on .loc-name { color: #fff; }
/* Every other row. :focus-visible rather than :focus so a pointer tap does not leave a ring behind. */
.loc-row:not(.on):hover,
.loc-row:not(.on):focus-visible { background: var(--jem-green-20); }
.loc-row .loc-meta { flex: 1; min-width: 0; }
/* Centred, now that the tile is gone and the name is the row's whole content. */
.loc-row .loc-name { font-weight: 700; font-size: 14px; color: var(--ink); text-align: center; }
.loc-row .loc-sub { font-size: 11px; color: var(--ink-3); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Counts stack, to-do above overdue, right-aligned so the pills share a right edge. */
.loc-row .loc-counts { flex: 0 0 auto; display: grid; gap: 4px; justify-items: end; }
.loc-row .loc-count { font-size: 11px; font-weight: 700; color: var(--jem-forest); background: var(--jem-green-20); border-radius: 999px; padding: 3px 9px; white-space: nowrap; }
.loc-row.on .loc-count { background: #fff; }
/* Overdue keeps its red on the selected row too — a white pill there would read as "fine". */
.loc-row .loc-count.late, .loc-row.on .loc-count.late { color: var(--jem-danger); background: var(--jem-danger-soft); }

/* ---------- Tabs ---------- */
/* margin-top RESTORED, and it is NOT paid for twice. JGMA-9 removed it on the grounds that the farm
   bar above already carries a 10px bottom margin — but the two are adjacent siblings in a block
   container (.m-header is not flex), so their margins COLLAPSE to a single 10px rather than
   stacking. Without it the gap was right only while the farm bar rendered: Loading draws nothing at
   all, so the tabs sat flush against the day strip until the farm list resolved. Collapsing gives
   one rule that is correct in every farm state. */
.m-tabs { display: flex; background: rgba(0, 0, 0, 0.18); border-radius: 10px; padding: 4px; margin-top: 10px; }
/* white-space: nowrap keeps "Requires Action" (JGMA-70) on one line. Without it the third tab wraps
   at 360px and stands a row taller than its two neighbours, which drags the whole strip with it.
   min-width: 0 lets the three share the row instead of each demanding its content width. */
.m-tabs button {
  flex: 1; min-width: 0; background: transparent; border: 0; padding: 9px 6px; border-radius: 7px;
  font-size: 14px; font-weight: 600; color: var(--ink-4); cursor: pointer; white-space: nowrap;
}
.m-tabs button.on { background: var(--jem-green); color: #fff; }
.m-tabs button .count { background: rgba(255,255,255,0.22); padding: 1px 7px; border-radius: 999px; margin-left: 4px; font-size: 11px; }
/* The Requires Action count, which is a warning rather than a tally — so it keeps its colour when
   the tab is NOT selected, unlike the other two. That is the point of the tab: the number has to
   read as something outstanding from across a yard, not as a neutral count of rows. */
.m-tabs button .count.alert { background: var(--jem-warning); color: #fff; }
.m-tabs button.on .count.alert { background: rgba(255,255,255,0.28); }

/* ---------- List area ---------- */
/* background, position and z-index are JGMA-10's, and all three serve the pull-to-refresh spinner
   that sits BEHIND this list in .ptr-anchor: opaque so the spinner does not show through the list at
   rest, positioned so it paints above the spinner rather than relying on source order, and below
   .m-header's 2 so a pulled list slides UNDER the header. overscroll-behavior-y is belt and braces —
   a pull that wins calls preventDefault() outright — and is `none` rather than `contain` because
   contain preserves this element's own bounce and only stops chaining, and there is nothing here to
   chain to. */
.m-list {
  padding: 12px; flex: 1 1 auto; overflow-y: auto; -webkit-overflow-scrolling: touch;
  background: var(--bg);
  position: relative;
  z-index: 1;
  overscroll-behavior-y: none;
  /* Carries the list home after a pull, and down to the parked offset on the Retry path. swipe.js
     suppresses it with an inline `transition: none` while the thumb is actually dragging, so the
     list tracks the finger 1:1 and only ANIMATES when it is let go. */
  transition: transform 240ms ease;
}
.m-list::-webkit-scrollbar { width: 0; }

/* ---------- Pull to refresh (JGMA-10) ----------
   New selectors, so this is a new section rather than an override block — nothing here shadows an
   existing rule. The three edits this feature DOES make to existing rules are in place, at .m-header
   and .m-list above, per CLAUDE.md's "edit rules in place" house rule.

   .ptr-anchor is zero-height and IN FLOW. In flow, because .app-shell is overflow:hidden and so is
   still a scroll container whose scrollHeight exceeds its height — anything positioned against the
   SHELL is dragged out of place when the browser scrolls it to reveal a focused control (the reason
   .busy-veil is fixed rather than absolute; see its block below). Zero height, because it must not
   change the flex column's layout: .m-list is flex:1 1 auto; overflow-y:auto only by virtue of
   .app-shell being that column, and boxing it in a wrapper would stop it scrolling altogether. */
.ptr-anchor {
  position: relative;
  flex: 0 0 0;
  height: 0;
}

/* Sits in the gap the pull opens, painted beneath the opaque .m-list and revealed as it translates
   down. The sweep is a conic gradient driven by --ptr-fill, which swipe.js writes on :root — on the
   document root deliberately, because Blazor's diff does not manage it and OnAfterRenderAsync
   re-renders this element on every pass. */
.ptr-spin {
  position: absolute;
  top: 10px;
  left: 50%;
  width: 28px;
  height: 28px;
  margin-left: -14px;
  border-radius: 50%;
  background: conic-gradient(var(--jem-green) calc(var(--ptr-fill, 0) * 360deg), transparent 0);
  /* Punched out to a ring rather than a filled disc. */
  mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
  opacity: calc(0.35 + var(--ptr-fill, 0) * 0.65);
}

/* The refresh itself. A FIXED arc that rotates, ignoring --ptr-fill entirely: swipe.js clears that
   property when the gesture ends, and on the Retry-from-the-dialog path it was never set at all, so
   a fill-driven sweep would render at zero for the whole refresh. */
.ptr-spin.spinning {
  background: conic-gradient(var(--jem-green) 0deg 90deg, transparent 90deg);
  opacity: 1;
  animation: busy-spin 700ms linear infinite;
}

/* C# parks the list here for the duration, which is what makes the Retry path — where no finger ever
   touched the screen — look identical to a pulled refresh. On the drag path swipe.js has already
   written the same offset inline, and inline wins, so the two agree rather than fight. */
.ptr-refreshing .m-list { transform: translateY(56px); }

/* Visually hidden but ANNOUNCED. The clip-rect pattern, not display:none or visibility:hidden —
   neither of which is read out, which would defeat the point of the element. app.css carried no
   such utility before this. */
.ptr-live {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Same bargain the busy veil strikes: the information survives, the motion does not. The FILL still
   tracks the thumb, because that is the affordance telling a groom when letting go will refresh —
   it is information, not decoration. Only the rotation and the travel are dropped. */
@media (prefers-reduced-motion: reduce) {
  /* THE SHAPE CHANGES TOO, and it has to. Swapping only the rotation for a pulse — which is what
     .busy-spinner does — leaves a 90deg arc that neither turns nor fills, and that reads as a
     progress ring STUCK at a quarter rather than as work in progress. .busy-spinner survives the
     same swap because it sits beside a "Saving..." label; this indicator deliberately has no text,
     so its shape is the only thing carrying the meaning. A full ring, pulsing, says "working"
     without implying a position. */
  .ptr-spin.spinning {
    background: conic-gradient(var(--jem-green) 0deg 360deg);
    animation: busy-pulse 1.4s ease-in-out infinite;
  }
  .m-list { transition: none; }
}

/* Floating action button — start a new movement */
.m-fab {
  position: absolute; right: 16px; bottom: calc(env(safe-area-inset-bottom, 0px) + 18px); z-index: 20;
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--jem-pink); color: #fff;
  padding: 12px 18px 12px 14px; border-radius: 999px;
  box-shadow: 0 6px 18px color-mix(in srgb, var(--jem-pink) 45%, transparent);
  font-weight: 700; font-size: 14px;
  text-decoration: none;
}
.m-fab:active { filter: brightness(0.92); }
.m-fab .plus { font-size: 20px; line-height: 1; }

.list-hint {
  text-align: center; font-size: 11px; color: var(--ink-3); padding: 4px 0 12px;
  display: flex; align-items: center; justify-content: center; gap: 8px;
}
.list-hint .arrow { color: var(--jem-green); font-weight: 700; }

/* ---------- Swipe row / move card ---------- */
.swipe-row {
  position: relative; border-radius: 14px; margin-bottom: 12px; overflow: hidden;
  background: var(--jem-green);
  touch-action: pan-y;
}
.swipe-row .bg {
  position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: space-between; padding: 0 22px; color: #fff; font-weight: 700; font-size: 14px;
}
/* TWO "Confirm move" labels, one at each edge (JGMA-79): a drag in EITHER direction confirms, and
   swipe.js fades in whichever the drag uncovers. The background is the confirm green both ways —
   it is .swipe-row's own — so there is no longer a second colour for a left drag. (The amber
   .flagging rule and the .flag-side label went with the flag mode; nothing adds either.) */
.swipe-row .bg .confirm-side { display: flex; align-items: center; gap: 8px; }
.swipe-row .bg .confirm-side.end { margin-left: auto; opacity: 0; }

/* JGMA-24: a groom who may not confirm gets no swipe actions, so the row must not look as though
   it has any. The confirm green lives on .swipe-row ITSELF rather than on .bg, so hiding .bg alone
   would leave the card sliding aside over a bare green panel — the affordance minus its label. The
   grab cursor goes for the same reason: it advertises a gesture that is not there.
   `user-select: none` STAYS on .move-card — the tap path is ungated and still needs it. */
.swipe-row.no-swipe { background: transparent; }
.swipe-row.no-swipe .move-card { cursor: default; }

.move-card {
  position: relative; background: #fff; border-radius: 14px; padding: 14px 16px;
  box-shadow: 0 1px 3px rgba(15,23,42,0.1);
  will-change: transform; cursor: grab; user-select: none;
}
.move-card:active { cursor: grabbing; }
.move-card .horse-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.move-card .type-badge {
  font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px;
  padding: 2px 7px; border-radius: 999px; flex: 0 0 auto;
}
/* The five movement types, re-cut onto the brand palette — they were a blue, a cyan and
   a purple, none of which are Juddmonte colours.
   Each is a -20 tint behind the 100% colour, EXCEPT where that pairing fails contrast:
   green on green-20 and pink on pink-20 are both around 2:1 at this size, so those two
   take forest and charcoal instead. Distinguishability lives in the BACKGROUND — grey,
   teal, amber, mint, pink — which is what a groom actually scans. */
.move-card .type-badge.local { background: var(--jem-gainsboro-40); color: var(--jem-charcoal); }
.move-card .type-badge.intra { background: var(--jem-forest-20); color: var(--jem-forest); }
.move-card .type-badge.departure { background: var(--jem-warning-soft); color: var(--jem-warning); }
.move-card .type-badge.arrival { background: var(--jem-green-20); color: var(--jem-forest); }
.move-card .type-badge.bulk { background: var(--jem-pink-20); color: var(--jem-charcoal); }
/* Flexes so the date + badge keep their width and a long name ellipsizes instead of shoving them. */
.move-card .horse {
  font-size: 16px; font-weight: 700; line-height: 1.15;
  flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Tabular figures so the dates line up down the column. */
.move-card .move-date {
  flex: 0 0 auto; font-size: 11px; font-weight: 700; color: var(--ink-3);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* ---------- Overdue: scheduled before today ----------
   The list is ordered oldest-first so these sit at the top, but the card has to SAY it is late as
   well — otherwise the groom is left comparing dates to work out why this one is first.
   Two surfaces, because the card has two: the card face and the .route row nested inside it, which
   carries its own grey. Tinting only the card left that inner row a cold grey island on red. The
   route keeps a slightly deeper tint than the card, preserving the same "inset panel" relationship
   the grey pair has. Text stays dark ink — the tints are light enough to read on, and recolouring
   the horse name would trade legibility for emphasis it does not need. */
.move-card.overdue { background: var(--jem-danger-soft); }
.move-card.overdue .route { background: var(--jem-danger-soft); }
.move-card.overdue .route.more { color: var(--jem-danger); }
.move-card.overdue .move-date { color: var(--jem-danger); }
/* The badge that SAYS it (JGMA-68). The tint above is the emphasis; this is the label, and it exists
   because colour alone reaches neither a screen reader nor a greyscale screen. Sized and shaped like
   .type-badge beside it rather than inventing a second badge language on one row — the difference is
   the danger palette, which is the row's one piece of new information. */
.move-card .overdue-badge {
  flex: 0 0 auto; font-size: 10px; font-weight: 800; letter-spacing: 0.3px;
  text-transform: uppercase; white-space: nowrap;
  padding: 3px 7px; border-radius: 999px;
  background: var(--jem-danger); color: #fff;
}
.move-card .route {
  margin-top: 10px; background: var(--jem-gainsboro-20); border-radius: 8px; padding: 9px 11px;
  font-size: 13px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.move-card .route .ep { font-weight: 600; }
.move-card .route .arrow { color: var(--jem-green); font-weight: 800; }
/* A batch, whose horses take different routes — the per-horse list is in the details panel. */
.move-card .route.more { color: var(--ink-3); font-weight: 600; }

/* ---------- Done card ---------- */
.done-card {
  background: #fff; border: 1px solid var(--line); border-left: 4px solid var(--jem-green);
  border-radius: 12px; padding: 12px 14px; margin-bottom: 10px;
  display: flex; align-items: center; gap: 12px;
  animation: dropIn 280ms ease;
}
@keyframes dropIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
.done-card .check { width: 30px; height: 30px; border-radius: 50%; background: var(--jem-green); color: #fff; display: grid; place-items: center; flex: 0 0 30px; }
/* The middle column has to be allowed to shrink, or a long route pushes the time off the card. */
.done-card .dc-meta { flex: 1 1 auto; min-width: 0; }
.done-card .horse { font-weight: 700; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Now carries the same route row the to-do cards do (Components/MoveRoute.razor), so it needs that
   row's parts — the arrow and the two endpoints — at this card's smaller scale. */
.done-card .meta { font-size: 11px; color: var(--ink-3); margin-top: 1px; display: flex; align-items: center; gap: 5px; min-width: 0; }
.done-card .meta .ep { font-weight: 600; color: var(--ink-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.done-card .meta .arrow { color: var(--jem-green); font-weight: 800; flex: 0 0 auto; }
.done-card .time { margin-left: auto; font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); }
/* Tappable — it opens the detail panel, same as a to-do card. */
.done-card { cursor: pointer; -webkit-tap-highlight-color: transparent; }
@media (hover: hover) {
  .done-card:hover { background: var(--jem-gainsboro-20); }
}
.done-card:active { background: var(--jem-gainsboro-40); }
.done-card.flagged { border-left-color: var(--jem-warning); }
.done-card.flagged .check { background: var(--jem-warning); }
/* Requires Action (JGMA-70) borrows the flagged card's amber, deliberately rather than inventing a
   third colour. Both say the same thing to a groom — this one is not finished — and the two can
   never appear in the same list, because a flagged movement writes no ledger row and so can never
   qualify for Requires Action (JGMA-68 AC 4). A distinct colour would be a distinction without a
   difference, on a screen whose whole palette is three colours. */
.done-card.needs-place { border-left-color: var(--jem-warning); }
.done-card.needs-place .check { background: var(--jem-warning); }

.empty-state { text-align: center; padding: 50px 24px; color: var(--ink-3); }
.empty-state .ico { font-size: 40px; opacity: 0.4; margin-bottom: 10px; line-height: 0; }
.empty-state .big { font-size: 15px; font-weight: 600; color: var(--ink-2); }

/* ---------- Bottom sheets (shared base) ---------- */
.sheet-backdrop {
  position: absolute; inset: 0; background: rgba(15,23,42,0.5); z-index: 30;
  opacity: 0; pointer-events: none; transition: opacity 200ms;
}
.sheet-backdrop.show { opacity: 1; pointer-events: auto; }
.sheet {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 31;
  background: #fff; border-radius: 18px 18px 0 0; padding: 12px 16px 22px;
  padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 22px);
  /* VISIBILITY IS LOAD-BEARING, NOT DECORATION (JGMA-26). A transform MOVES an element; it removes
     it from neither the accessibility tree nor the tab order. Every .sheet is rendered
     unconditionally, so on the transform alone a screen reader read out the notes box, the date
     field and Cancel / Save of sheets nobody had opened, and Tab walked into them.
     `visibility 0s linear 260ms` delays the flip to hidden until the slide has finished — without
     the delay the sheet VANISHES instead of sliding away. Opening carries no delay (see .show).
     NOT display:none: swipe.attachDragDismiss measures panel.offsetHeight to size a dismiss drag,
     which reads 0 under display:none and breaks swipe-down-to-dismiss on every sheet. visibility
     preserves layout, so the measurement stays correct.
     Asserted by SheetVisibilityTests. .detail-modal, .cal-sheet and the wizard's .alert-backdrop
     carry the same technique at their OWN durations (JGMA-46) — see each rule. */
  transform: translateY(100%); visibility: hidden;
  transition: transform 260ms cubic-bezier(.2,.9,.3,1.2), visibility 0s linear 260ms;
  /* Reserve vertical drags for swipe-down-to-dismiss (from anywhere on the sheet). */
  touch-action: pinch-zoom;
}
/* The transition is RESTATED here, not duplicated by accident: this rule declared none of its own,
   so it would otherwise inherit the delayed one above and the sheet would POP IN at the end of the
   slide instead of sliding in. SheetVisibilityTests asserts it for that reason. */
.sheet.show {
  transform: translateY(0); visibility: visible;
  transition: transform 260ms cubic-bezier(.2,.9,.3,1.2), visibility 0s;
}
.sheet .grab-zone { padding: 4px 0 10px; cursor: grab; touch-action: none; }
.sheet .grab-zone:active { cursor: grabbing; }
.sheet .grab { width: 38px; height: 4px; background: var(--line-2); border-radius: 4px; margin: 0 auto; }
.sheet h3 { margin: 0 0 10px; font-size: 16px; font-weight: 700; text-align: center; }
.sheet .sub { font-size: 12px; color: var(--ink-3); margin-bottom: 12px; }

/* Confirm sheet body. The photo placeholder's rules went with it (JGMA-79): the box sat inside the
   deleted flag mode, and attachments are JGMA-80's. */
.sheet textarea { width: 100%; border: 1px solid var(--line-2); border-radius: 10px; padding: 11px; font: inherit; font-size: 16px; min-height: 80px; resize: none; outline: none; background: #fff; color: var(--ink); }
.sheet .field { margin-bottom: 12px; }
.sheet .field label { display: block; font-size: 12px; font-weight: 600; color: var(--ink-2); margin-bottom: 6px; }
.sheet .actions { display: flex; gap: 10px; margin-top: 14px; }
.sheet .actions button { flex: 1; border: 0; padding: 14px; border-radius: 12px; font-weight: 700; font-size: 14px; cursor: pointer; font-family: inherit; }
.sheet .actions .cancel { background: var(--jem-gainsboro-40); color: var(--ink-2); }
.sheet .actions .save { background: var(--jem-warning); color: #fff; }
.sheet .actions .save.confirm { background: var(--jem-green); }

/* Delete-move confirm must sit ABOVE the detail modal it is launched from. */
#del-backdrop { z-index: 70; }
#del-sheet { z-index: 71; }

/* The small outlined controls in the header — Today and Retry.

   RENAMED FROM a Reset-specific name: it started life on the "Reset demo" button alone, but Today
   and Retry are ordinary parts of the app and will outlive the demo data entirely. A class named
   after the one control that is going away is a class that has to be renamed later anyway.

   THAT RENAME PAID OFF: the Reset button has since been removed and this class did not have to
   move with it. Today and Retry are its only wearers now.

   --ink-3 rather than --ink-2 (JGMA-9): against the charcoal header the darker border read as no
   border at all, so the control did not look like one until you hovered it. Hover goes to #fff with
   the label, so the whole button brightens together instead of the text alone.

   font-weight and letter-spacing belong HERE rather than in a second rule further down: there used
   to be a stray `.mini-btn { font-weight: 700 }` a thousand lines away, left by the append-only
   habit this file has since dropped. */
.mini-btn {
  background: transparent; border: 1px solid var(--ink-3); color: var(--ink-4);
  border-radius: 6px; font-size: 10px; font-weight: 700; letter-spacing: 1px;
  padding: 3px 7px; cursor: pointer;
}
.mini-btn:hover { color: #fff; border-color: #fff; }

/* ---------- Move detail modal (mirrors S10 detail card) ---------- */
.detail-backdrop { position: absolute; inset: 0; background: rgba(15,23,42,0.5); z-index: 40; opacity: 0; pointer-events: none; transition: opacity 200ms; }
.detail-backdrop.show { opacity: 1; pointer-events: auto; }
.detail-modal {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 41;
  max-height: calc(100% - 56px);
  background: #fff; border-radius: 18px 18px 0 0; overflow: hidden;
  display: flex; flex-direction: column;
  /* Hidden from assistive technology and the tab order while closed, as .sheet is (JGMA-46): a
     transform alone left every closed detail panel readable and tabbable. The visibility flip waits
     out THIS rule's 300ms slide, not .sheet's 260ms, or the panel vanishes before it has slid away. */
  transform: translateY(100%); visibility: hidden;
  transition: transform 300ms cubic-bezier(.2,.9,.3,1.1), visibility 0s linear 300ms;
}
/* Restated, not duplicated: without its own undelayed transition this rule inherits the delayed one
   above and the panel POPS IN at the end of its slide. SheetVisibilityTests asserts both. */
.detail-modal.show {
  transform: translateY(0); visibility: visible;
  transition: transform 300ms cubic-bezier(.2,.9,.3,1.1), visibility 0s;
}
.detail-modal > header {
  padding: 10px 16px 14px; border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg,var(--jem-gainsboro-20),#fff);
  display: flex; align-items: center; gap: 10px; flex: 0 0 auto;
  touch-action: pinch-zoom;
}
.detail-modal > header h3 { margin: 0; font-size: 15px; font-weight: 700; line-height: 1.25; }
.detail-modal .dm-grab-zone { flex: 0 0 auto; padding: 10px 0 4px; cursor: grab; touch-action: none; }
.detail-modal .dm-grab-zone:active { cursor: grabbing; }
.detail-modal .grab { width: 38px; height: 4px; background: var(--line-2); border-radius: 4px; margin: 0 auto; }
.detail-body { flex: 1 1 auto; overflow-y: auto; padding-bottom: calc(env(safe-area-inset-bottom, 0px)); }
.detail-body::-webkit-scrollbar { width: 0; }

.status { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; flex: 0 0 auto; }
.status.scheduled { background: var(--jem-gainsboro-40); color: var(--jem-charcoal); }
.status.transit   { background: var(--jem-warning-soft); color: var(--jem-warning); }
.status.arrived   { background: var(--jem-green-20); color: var(--jem-forest); }

.detail-body .section { padding: 14px 16px; border-bottom: 1px solid var(--line); }
.detail-body .section h4 { margin: 0 0 8px; font-size: 11px; text-transform: uppercase; color: var(--ink-3); letter-spacing: 0.5px; font-weight: 700; }
.detail-body .kv { display: grid; grid-template-columns: 96px 1fr; gap: 6px 12px; font-size: 12px; }
.detail-body .kv .k { color: var(--ink-3); }
.detail-body .kv .v { color: var(--ink); font-weight: 500; }
/* "by" is a connector, not data — dropping the value weight is enough to sit it back; it keeps the
   row.s colour so the attribution reads as one phrase with the name it introduces. */
.detail-body .kv .v .by { font-weight: 400; }
/* Overdue: the DATE only. The label stays muted grey — "Scheduled" is not the thing that is late —
   and the class sits on a span around the date rather than the whole value, so the "by <name>"
   after it is not reddened along with it. */
.detail-body .kv .v .overdue { color: var(--jem-danger); font-weight: 700; }

/* "Moves" — the per-horse routes. Same two-column shape as .kv above, but the name column is
   `max-content`: it grows to the LONGEST name in this move and no further, so every route starts at
   the same x without a fixed width that would either truncate names or waste the row. The value
   column is minmax(0, 1fr) so a three-part route wraps inside the panel rather than widening it. */
.detail-body .kv.routes { grid-template-columns: max-content minmax(0, 1fr); gap: 7px 14px; }
.detail-body .kv.routes .k { white-space: nowrap; }
.detail-body .kv.routes .v { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.detail-body .kv.routes .ep { font-weight: 600; }
.detail-body .kv.routes .arrow { color: var(--jem-green); font-weight: 800; }

/* The two notes, read-only (JGMA-79). A note is free text a person typed, so its own line breaks are
   kept (pre-line) — D3's prefill joins several notes one per line, and flattening them would run two
   people's words together. Under Moves, a horse's own note takes a full line beneath its route. */
.detail-body .kv .v.note { white-space: pre-line; overflow-wrap: anywhere; }
.detail-body .kv.routes .horse-note {
  flex-basis: 100%; font-size: 12px; color: var(--ink-2); font-weight: 400;
  white-space: pre-line; overflow-wrap: anywhere;
}
.detail-body .kv.routes .horse-note .hn-k { color: var(--ink-3); margin-right: 4px; }

/* Detail-modal action row (Edit / Delete move) */
.detail-body .action-row { display: flex; gap: 8px; flex-wrap: wrap; }
/* inline-flex + gap so the leading icon sits on the label's optical centre. */
.detail-body .action-row .act { border: 1px solid var(--line-2); background: #fff; color: var(--ink-2); border-radius: 9px; padding: 9px 13px; font-size: 13px; font-weight: 600; cursor: pointer; font-family: inherit; display: inline-flex; align-items: center; gap: 7px; }
.detail-body .action-row .act svg { flex: 0 0 auto; }
.detail-body .action-row .act:active { background: var(--jem-gainsboro-40); }
.detail-body .action-row .act.danger { border-color: var(--jem-danger-soft); background: var(--jem-danger); color: #fff; }

/* ============================================================================
   Calendar overlay (Components/CalendarSheet.razor) — shared by screen 20's
   confirm/flag sheet and the 20c wizard's "When?" step. Sits above the sheets.
   ============================================================================ */
.cal-backdrop { position: absolute; inset: 0; background: rgba(15,23,42,0.5); z-index: 55; opacity: 0; pointer-events: none; transition: opacity 200ms; }
.cal-backdrop.show { opacity: 1; pointer-events: auto; }
.cal-sheet {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 56;
  background: #fff; border-radius: 18px 18px 0 0; padding: 5px 16px 22px;
  padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 22px);
  /* Hidden while closed, as .sheet is (JGMA-46). One pair covers all three calendars — hdr-cal-sheet,
     confirm-cal-sheet and the wizard's — since each renders through CalendarSheet; before this a closed
     one left a whole month of day buttons in the accessibility tree and the tab order, on every step
     of the wizard. Its own 300ms curve, which differs from .detail-modal's in the last term. */
  transform: translateY(100%); visibility: hidden;
  transition: transform 300ms cubic-bezier(.2,.9,.3,1), visibility 0s linear 300ms;
}
.cal-sheet.show {
  transform: translateY(0); visibility: visible;
  transition: transform 300ms cubic-bezier(.2,.9,.3,1), visibility 0s;
}
.cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.cal-head .cal-title { font-size: 15px; font-weight: 800; color: var(--ink); }
.cal-nav { width: 34px; height: 34px; border-radius: 8px; border: 1px solid var(--line-2); background: #fff; color: var(--ink-2); font-size: 16px; cursor: pointer; display: grid; place-items: center; }
.cal-nav:disabled { opacity: 0.35; cursor: not-allowed; }
.cal-dow { display: grid; grid-template-columns: repeat(7,1fr); gap: 2px; margin-bottom: 4px; }
.cal-dow span { text-align: center; font-size: 10px; font-weight: 700; color: var(--ink-4); text-transform: uppercase; }
.cal-grid { display: grid; grid-template-columns: repeat(7,1fr); gap: 4px; }
.cal-day { aspect-ratio: 1; border: 0; background: transparent; border-radius: 9px; font-size: 13px; font-weight: 600; color: var(--ink); cursor: pointer; display: grid; place-items: center; font-family: inherit; }
.cal-day:hover:not(:disabled) { background: var(--jem-green-20); }
.cal-day.empty { visibility: hidden; }
.cal-day:disabled { color: var(--line-2); cursor: not-allowed; }
.cal-day.sel { background: var(--jem-green); color: #fff; }

/* Quick-date grid + "other date" button (wizard step 3 + screen 20 sheet) */
.quick-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; margin-bottom: 14px; }
.quick-btn { border: 1px solid var(--line-2); background: #fff; border-radius: 12px; padding: 16px 10px; font-size: 14px; font-weight: 700; color: var(--ink-2); cursor: pointer; text-align: center; display: flex; flex-direction: column; gap: 4px; align-items: center; font-family: inherit; }
.quick-btn .qb-sub { font-size: 11px; font-weight: 500; color: var(--ink-3); }
.quick-btn:active { background: var(--jem-gainsboro-20); }
.quick-btn.on { border-color: var(--jem-green); background: var(--jem-green-20); color: var(--jem-forest); }
.quick-btn.on .qb-sub { color: var(--jem-forest); }
.other-btn { width: 100%; border: 1px solid var(--line-2); background: #fff; border-radius: 12px; padding: 16px; font-size: 14px; font-weight: 700; color: var(--ink-2); cursor: pointer; display: flex; align-items: center; gap: 10px; font-family: inherit; }
.other-btn .ob-meta { text-align: left; flex: 1; }
.other-btn.on { border-color: var(--jem-green); background: var(--jem-green-20); color: var(--jem-forest); }
.other-btn .ob-cal { margin-left: auto; color: var(--ink-3); display: flex; }
.other-btn.on .ob-cal { color: var(--jem-forest); }
/* Screen 20's confirm/flag sheet uses a slightly tighter date button than the wizard. */
.sheet .other-btn { padding: 14px; }

/* ============================================================================
   Screen 20c — New Movement wizard (Pages/NewMovement.razor)
   A linear 4-step flow: Move what? → To where? → When? → Review.
   ============================================================================ */

/* Nav header */
/* --top-inset is 0 only under the environment banner, which takes the inset instead (JGMA-84). */
.m-nav { background: var(--jem-charcoal); color: #fff; padding: calc(var(--top-inset, env(safe-area-inset-top, 0px)) + 10px) 12px 12px; display: flex; align-items: center; gap: 5px; flex: 0 0 auto; }
/* align-items: baseline (not the grid `place-items: center` it had) — the ‹ glyph carries a lot of
   empty space below its baseline, so centring the box left it sitting visibly high of the title. */
.m-nav .back { width: 34px; height: 34px; border-radius: 8px; flex: 0 0 34px; display: grid; align-items: center; justify-items: center; color: #fff; background: transparent; border: 0; font-size: 24px; line-height: 1; cursor: pointer; }
.m-nav .back:active { background: rgba(255, 255, 255, 0.14); }
.m-nav .title { font-size: 19px; font-weight: 700; margin: 0; }
.m-nav .logout-btn { margin-left: auto; }

/* Stepper */
.stepper { flex: 0 0 auto; background: var(--jem-charcoal); padding: 0 14px 14px; display: flex; align-items: flex-start; }
.stepper .seg { flex: 1; display: flex; flex-direction: column; align-items: center; position: relative; }
.stepper .seg .track { position: absolute; top: 13px; height: 2px; background: var(--jem-charcoal-80); left: -50%; right: 50%; z-index: 0; }
.stepper .seg:first-child .track { display: none; }
.stepper .seg.done .track, .stepper .seg.current .track { background: var(--jem-green); }
.stepper .dot { width: 26px; height: 26px; border-radius: 50%; z-index: 1; display: grid; place-items: center; font-size: 12px; font-weight: 700; background: var(--jem-charcoal-80); color: var(--ink-4); border: 0; font-family: var(--font-mono); }
.stepper .seg.done .dot { background: var(--jem-green); color: #fff; }
.stepper .seg.done { cursor: pointer; }
.stepper .seg.current .dot { background: #fff; color: var(--jem-charcoal); box-shadow: 0 0 0 3px rgba(255,255,255,0.22); }
.stepper .lbl { font-size: 9px; color: var(--ink-3); margin-top: 5px; text-align: center; line-height: 1.1; text-transform: uppercase; letter-spacing: 0.3px; font-weight: 600; }
.stepper .seg.current .lbl { color: #fff; }
.stepper .seg.done .lbl { color: var(--ink-4); }

/* Step area */
.m-form { flex: 1 1 auto; overflow-y: auto; padding: 16px 14px 22px; background: var(--bg); }
.m-form::-webkit-scrollbar { width: 0; }
.step-panel { display: none; }
.step-panel.on { display: block; animation: stepIn 220ms ease; }
@keyframes stepIn { from { opacity: 0; transform: translateX(8px); } to { opacity: 1; transform: none; } }
.step-h { margin: 0 0 4px; font-size: 20px; font-weight: 700; color: var(--ink); }
.step-lead { font-size: 12px; color: var(--ink-3); margin-bottom: 14px; }
.view { display: none; }
.view.on { display: block; }

/* Search input */
.search-input { position: relative; margin-bottom: 12px; }
.search-input input { width: 100%; border: 1px solid var(--line-2); border-radius: 10px; padding: 11px 11px 11px 34px; font: inherit; font-size: 16px; outline: none; color: var(--ink); background: #fff; }
.search-input input:focus { border-color: var(--jem-info); box-shadow: 0 0 0 3px color-mix(in srgb, var(--jem-info) 18%, transparent); }
.search-input .mag { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: var(--ink-3); display: flex; }

/* Result rows (horses / paddocks / yards) */
.res-row { border: 1px solid var(--line); border-radius: 10px; background: #fff; padding: 10px 11px; display: flex; gap: 11px; cursor: pointer; align-items: center; margin-bottom: 8px; }
.res-row:active { background: var(--jem-gainsboro-20); }
.res-row.disabled { cursor: default; opacity: 0.5; background: var(--jem-gainsboro-20); }
.res-row.disabled:active { background: var(--jem-gainsboro-20); }
.res-row.picked { cursor: default; background: var(--jem-green-20); border-color: var(--jem-green); }
.res-row.picked:active { background: var(--jem-green-20); }
.res-row .r-added { font-size: 11px; font-weight: 800; color: var(--jem-forest); flex: 0 0 auto; letter-spacing: 0.3px; display: flex; align-items: center; gap: 3px; }
.res-row .r-ic { width: 38px; height: 38px; border-radius: 8px; display: grid; place-items: center; color: #fff; font-weight: 800; flex: 0 0 38px; font-size: 14px; }
.res-row.horse .r-ic { background: linear-gradient(135deg,var(--jem-green-40),var(--jem-green)); }
.res-row.foal .r-ic { background: linear-gradient(135deg,var(--jem-pink-20),var(--jem-pink)); }
/* Icons, not emoji — Material Symbols inlined by Components/Icon.razor. They inherit the tile's
   colour, so white on the gradient, matching the white initials on the horse tiles beside them. */
.res-row.loc .r-ic { background: linear-gradient(135deg, var(--jem-forest-40), var(--jem-forest)); }
.res-row.loc .r-ic.outside { background: linear-gradient(135deg,var(--jem-warning-soft),var(--jem-warning)); }
.res-row .r-meta { min-width: 0; flex: 1; }
.res-row .r-name { font-size: 13px; font-weight: 800; color: var(--ink); display: flex; align-items: center; gap: 6px; }
.res-row .r-dis { font-size: 11px; color: var(--ink-3); margin-top: 2px; line-height: 1.35; }
.res-row .r-loc { font-size: 10.5px; color: var(--jem-forest); font-weight: 600; margin-top: 2px; display: flex; align-items: center; gap: 3px; }
.res-row .r-loc .icon { flex: 0 0 auto; margin-left: -1px; }
.res-row .chev { color: var(--line-2); font-size: 20px; flex: 0 0 auto; }

.r-tag { font-size: 9px; font-weight: 800; color: var(--ink-3); background: var(--jem-gainsboro-40); border-radius: 5px; padding: 2px 6px; letter-spacing: 0.3px; text-transform: uppercase; flex: 0 0 auto; }
.r-tag.foal { background: var(--jem-pink-20); color: var(--jem-charcoal); }
.r-tag.kind { background: var(--jem-gainsboro-40); color: var(--jem-charcoal); }

.loading { text-align: center; padding: 30px 20px; color: var(--ink-3); font-size: 13px; }

/* Selections list */
.count-lead { font-size: 11px; color: var(--ink-3); font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
.sel-list { display: grid; gap: 10px; margin-bottom: 14px; }
/* The row itself is INERT — removing is the ✕'s job. The whole row used to carry the remove
   handler with the ✕ as decoration, so a tap on the name or the location opened the remove sheet. */
.sel-row { background: #fff; border: 1px solid var(--line); border-radius: 12px; padding: 11px 12px; display: flex; align-items: center; gap: 11px; }
.sel-row .s-ic { width: 38px; height: 38px; border-radius: 8px; background: linear-gradient(135deg,var(--jem-green-40),var(--jem-green)); display: grid; place-items: center; color: #fff; font-weight: 800; flex: 0 0 38px; font-size: 14px; }
.sel-row.foal .s-ic { background: linear-gradient(135deg,var(--jem-pink-20),var(--jem-pink)); }
.sel-row .s-meta { min-width: 0; flex: 1; }
.sel-row .s-name { font-size: 13px; font-weight: 800; color: var(--ink); letter-spacing: 0.2px; display: flex; align-items: center; gap: 6px; }
.sel-row .s-dis { font-size: 11px; color: var(--ink-3); margin-top: 2px; }
/* A real button: 40px of hitbox (negative margin borrows the row's own padding rather than
   growing the row), an outlined circle so it reads as pressable, and a red hover/press state
   so what it does is legible before it is tapped. */
.sel-row .s-x {
  flex: 0 0 40px; width: 40px; height: 40px; margin: -4px -6px -4px 0;
  display: grid; place-items: center;
  border: 1px solid var(--line-2); border-radius: 9px; background: #fff;
  color: var(--ink-3); font-size: 15px; line-height: 1; font-family: inherit; cursor: pointer;
  transition: background 140ms, color 140ms, border-color 140ms, transform 100ms;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) {
  .sel-row .s-x:hover { background: var(--jem-danger-soft); border-color: var(--jem-danger); color: var(--jem-danger); }
}
.sel-row .s-x:active { background: var(--jem-danger-soft); border-color: var(--jem-danger); color: var(--jem-danger); transform: scale(0.9); }
.sel-row .s-x:focus-visible { outline: 2px solid var(--jem-danger); outline-offset: 2px; }

.add-btn { width: 100%; border: 2px dashed var(--line-2); background: #fff; color: var(--jem-forest); border-radius: 12px; padding: 14px; font-size: 14px; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; font-family: inherit; }
.add-btn:active { background: var(--jem-green-20); border-color: var(--jem-green); }

/* Movement details' note box (JGMA-79) — the confirm sheet's textarea, on a screen that is not a
   sheet. */
.m-body .md-note { width: 100%; border: 1px solid var(--line-2); border-radius: 10px; padding: 11px; font: inherit; font-size: 16px; min-height: 80px; resize: vertical; outline: none; background: #fff; color: var(--ink); }
.m-body .md-note:focus-visible { border-color: var(--jem-green); }

/* Destination chosen card (light — matches the origin card, not dark) */
.chosen-card { background: var(--jem-gainsboro-20); color: var(--ink); border: 1px solid var(--line); border-radius: 14px; padding: 8px 11px; display: flex; align-items: center; gap: 12px; cursor: pointer; }
.chosen-card:active { background: var(--jem-gainsboro-40); }
.chosen-card .cc-meta { flex: 1; min-width: 0; }
.chosen-card .cc-lbl { font-size: 10px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.6px; font-weight: 800; }
.chosen-card .cc-name { font-size: 15px; font-weight: 800; color: var(--ink); }
.chosen-card .cc-hint { font-size: 11px; color: var(--ink-3); }

/* Read-only origin card (Step 2) — compact: label + farm name only (light/outlined) */
.origin-card { display: flex; align-items: center; gap: 12px; background: var(--jem-gainsboro-20); border: 1px solid var(--line); border-radius: 14px; padding: 8px 11px; margin-bottom: 10px; }
.origin-card .oc-meta { flex: 1; min-width: 0; }
.origin-card .oc-lbl { font-size: 10px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.6px; font-weight: 800; }
.origin-card .oc-name { font-size: 15px; font-weight: 800; color: var(--ink); }

/* ---------------------------------------------------------------------------
   Per-horse destination — Paddock / Yard / Box (Step 2 on 20c, the roster on
   20d; markup in Components/HorseSpotPicker.razor). This replaced a recursive
   destination TREE, which put paddocks and yards in a parent/child relationship
   they do not have and could express only one of the two at a time.

   Three INDEPENDENT selects stacked one per line, each with its own label and
   its own "All" button. Stacked rather than side by side because the app column
   is a phone: three dropdowns across ~330px leaves each too narrow to read a
   paddock name in. Native <select> on purpose — it opens the platform's own
   picker, which beats any custom listbox on a touch device.
   --------------------------------------------------------------------------- */
.section-lbl { font-size: 10px; font-weight: 800; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.7px; margin: 16px 2px 8px; }
.section-lbl .sl-sub { text-transform: none; font-weight: 600; color: var(--ink-4); letter-spacing: 0; }

/* Step 2's horse list. The cards themselves are the shared .hd-item collapsible (see the .hd-list
   block further down) — Step 2 and the 20d roster render the same component, so there is nothing
   Step-2-specific to style here beyond the list's own spacing. The .ph-item / .ph-head / .ph-ic /
   .ph-main / .ph-name / .ph-sub rules that used to live here styled Step 2's own always-expanded
   card and went with it. */
.perhorse-list { display: grid; gap: 10px; }

.ph-spots { display: grid; gap: 8px; }
.ph-cell { display: grid; grid-template-columns: 58px minmax(0, 1fr); align-items: center; gap: 10px; }
.ph-lbl { font-size: 10px; font-weight: 800; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.5px; }
.ph-ctl { display: flex; align-items: center; gap: 8px; min-width: 0; }

.ph-sel {
  flex: 1 1 auto; min-width: 0;
  border: 1px solid var(--line-2); border-radius: 8px; padding: 9px 30px 9px 10px;
  font: inherit; font-size: 16px; color: var(--ink); background-color: #fff; outline: none;
  appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2'><polyline points='6 9 12 15 18 9'/></svg>");
  background-repeat: no-repeat; background-position: right 10px center;
}
.ph-sel:focus { border-color: var(--jem-info); box-shadow: 0 0 0 3px color-mix(in srgb, var(--jem-info) 18%, transparent); }
/* Box, until a yard is chosen — a box row cannot exist without a yard. */
.ph-sel:disabled { background-color: var(--bg-2); color: var(--ink-4); cursor: not-allowed; }

/* Disabled on an empty field: one-click mass CLEARING is not a thing anyone wants. */
.ph-aa {
  flex: 0 0 auto; min-width: 38px; height: 36px; padding: 0 9px;
  border: 1px solid var(--line-2); border-radius: 8px; background: #fff;
  color: var(--jem-forest); font-family: inherit; font-size: 10px; font-weight: 800;
  text-transform: uppercase; letter-spacing: 0.5px; cursor: pointer;
  transition: background 140ms, border-color 140ms, transform 100ms;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) {
  .ph-aa:hover:not(:disabled) { background: var(--jem-green-20); border-color: var(--jem-green); }
}
.ph-aa:active:not(:disabled) { background: var(--jem-green-20); border-color: var(--jem-green); transform: scale(0.94); }
.ph-aa:disabled { color: var(--ink-4); cursor: not-allowed; opacity: 0.55; }
.ph-aa:focus-visible { outline: 2px solid var(--jem-green); outline-offset: 2px; }

/* Local detail preference — appears only when the horse has BOTH a paddock and a yard. */
.ph-pref { margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--line); display: grid; gap: 7px; }
.ph-pref-label { font-size: 10px; font-weight: 800; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.5px; }
.ph-seg { display: flex; border: 1px solid var(--line-2); border-radius: 8px; overflow: hidden; }
.ph-seg button {
  flex: 1 1 0; padding: 8px 6px; border: 0; background: #fff; color: var(--ink-2);
  font-family: inherit; font-size: 12px; font-weight: 700; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.ph-seg button + button { border-left: 1px solid var(--line-2); }
.ph-seg button.on { background: var(--jem-green-20); color: var(--jem-forest); }
.ph-seg button:focus-visible { outline: 2px solid var(--jem-green); outline-offset: -2px; }

/* Named validation messages (Step 2 on 20c, the roster on 20d) */
.s2-err { margin: 10px 2px 14px; font-size: 12px; font-weight: 600; color: var(--jem-danger); background: var(--jem-danger-soft); border-radius: 8px; padding: 8px 10px; line-height: 1.45; }

/* Collapsible per-horse card — 20d's roster (the wizard shows every horse expanded) */
.hd-item { background: #fff; border: 1px solid var(--line); border-radius: 12px; margin-bottom: 8px; overflow: hidden; }
.hd-item.open { border-color: var(--jem-green); }
.hd-head { display: flex; align-items: center; gap: 11px; padding: 9px 11px; cursor: pointer; }
.hd-head:active { background: var(--jem-gainsboro-20); }
/* align-self: start keeps the tile level with the name when the card grows, as it does with a
   read-only attachments list under the P/Y/B line (JGMA-80), rather than centring it on the block. */
.hd-ic { width: 34px; height: 34px; border-radius: 8px; background: linear-gradient(135deg,var(--jem-green-40),var(--jem-green)); display: grid; place-items: center; color: #fff; font-weight: 800; font-size: 14px; flex: 0 0 34px; align-self: start; }
.hd-item.foal .hd-ic { background: linear-gradient(135deg,var(--jem-pink-20),var(--jem-pink)); }
.hd-main { flex: 1 1 auto; min-width: 0; }
.hd-name { font-size: 13px; font-weight: 800; color: var(--ink); letter-spacing: 0.2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hd-sel { font-size: 12px; font-weight: 600; color: var(--jem-forest); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hd-sel.none { color: var(--ink-3); font-weight: 500; }
.hd-caret { flex: 0 0 auto; width: 24px; text-align: center; color: var(--ink-3); }
.hd-caret .hc-caret { font-size: 15px; display: inline-block; transition: transform 180ms; }
.hd-item.open .hd-caret .hc-caret { transform: rotate(90deg); }
.hd-spot { display: none; border-top: 1px solid var(--line); padding: 11px; }
.hd-item.open .hd-spot { display: block; }

/* Group label between destination sections (Step 2 farm/location list) */
.s2-group-lbl { font-size: 10px; font-weight: 800; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.6px; margin: 4px 2px 8px; }
.s2-group-lbl + .s2-group-lbl, .s2-results .res-row + .s2-group-lbl { margin-top: 14px; }

/* Review */
.rv-block { margin-bottom: 16px; }
.rv-block > .rv-h { font-size: 10px; font-weight: 800; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.6px; margin: 0 0 8px; display: flex; align-items: center; gap: 8px; }
.rv-block > .rv-h .rv-edit { margin-left: auto; font-size: 10px; color: var(--jem-forest); font-weight: 800; letter-spacing: 0.3px; }
/* WRAPS, so a per-horse notice can take a full-width line of its own beneath the tile (JGMA-68).
   align-items:center still centres the tile against the name block, because both are on the first
   line; the notice is pushed to a second by flex-basis:100% and aligns with the row's own padding —
   i.e. it starts directly below the horse's image rather than indented past it. */
.rv-horse { background: #fff; border: 1px solid var(--line); border-radius: 11px; padding: 11px 12px; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 8px; cursor: pointer; }
.rv-horse:active { background: var(--jem-gainsboro-20); }
.rv-horse .rh-ic { width: 34px; height: 34px; border-radius: 8px; background: linear-gradient(135deg,var(--jem-green-40),var(--jem-green)); display: grid; place-items: center; color: #fff; font-weight: 800; flex: 0 0 34px; font-size: 13px; }
.rv-horse.foal .rh-ic { background: linear-gradient(135deg,var(--jem-pink-20),var(--jem-pink)); }
.rv-horse .rh-meta { flex: 1; min-width: 0; }
.rv-horse .rh-name { font-size: 13px; font-weight: 800; color: var(--ink); display: flex; align-items: center; gap: 7px; }
.rv-horse .rh-dis { font-size: 11px; color: var(--ink-3); margin-top: 2px; }
.rv-horse .rh-x { color: var(--ink-4); font-size: 15px; flex: 0 0 auto; }
.rv-card { background: #fff; border: 1px solid var(--line); border-radius: 12px; padding: 13px 14px; display: flex; align-items: center; cursor: pointer; }
.rv-card:active { background: var(--jem-gainsboro-20); }
.rv-card .rc-ic { width: 40px; height: 40px; border-radius: 10px; background: var(--jem-green-20); color: var(--jem-forest); display: grid; place-items: center; font-size: 19px; flex: 0 0 40px; margin-right: 12px; }
.rv-card .rc-meta { flex: 1; min-width: 0; }
.rv-card .rc-name { font-size: 15px; font-weight: 800; color: var(--ink); }
.rv-card .rc-sub { font-size: 11px; color: var(--ink-3); margin-top: 2px; }
.rv-card .chev { color: var(--line-2); font-size: 20px; }
/* Step 4 review · per-horse destinations inside the To-where card */
.rv-dest-card { flex-direction: column; align-items: stretch; }
.rv-dest-head { display: flex; align-items: center; }
.rv-dest-rows { border-top: 1px solid var(--line); margin-top: 11px; padding-top: 9px; display: grid; gap: 7px; }
.rv-dest-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 12px; }
.rv-dest-row .rdr-name { font-weight: 800; color: var(--ink); letter-spacing: 0.2px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The spot can be all three parts ("Big Dunnings, Yard 4, Box 5"), so it wraps and takes the
   right-hand half rather than being pinned to one line. */
.rv-dest-row .rdr-spot { flex: 1 1 auto; min-width: 0; text-align: right; font-weight: 600; color: var(--jem-forest); }
.rv-dest-row .rdr-none { color: var(--ink-3); font-weight: 500; }

/* ONE PER-HORSE NOTICE STYLE FOR THE WHOLE REVIEW STEP (JGMA-68), not a class per message.
   The supersession warning (JGMA-49, Q2 on JGMA-33) is the only one today; the next validation
   message that belongs to a single horse uses this same class rather than inventing a shape.

   INSIDE the .rv-horse card, on its own line. flex-basis:100% is what breaks the line — the tile,
   the name block and the ✕ share the first, and the notice takes the second on its own, starting at
   the card's content edge, directly below the image.

   NO SOFT BACKGROUND, which is the change from the original amber panel. Inside a white card the
   filled block read as a second card wedged into the first; the colour alone carries the warning and
   the card already scopes it to one horse. --jem-warning still does the work, and it warns without
   blocking: re-booking a horse is legitimate, and refusing it would leave a groom no way to correct
   a plan from here. */
.rv-note {
  flex: 1 0 100%; margin: 2px 0 0; font-size: 12px; font-weight: 600; line-height: 1.45;
  color: var(--jem-warning);
}

/* Footer nav */
.m-savebar { flex: 0 0 auto; background: #fff; border-top: 1px solid var(--line); padding: 12px 14px; padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 12px); display: flex; gap: 10px; }
.m-savebar .btn { flex: 1; border: 0; padding: 14px; border-radius: 12px; font-weight: 700; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; }
.m-savebar .ghost { background: var(--jem-gainsboro-40); color: var(--ink-2); }
.m-savebar .primary { background: var(--jem-green); color: #fff; }
.m-savebar .primary:disabled { background: var(--line-2); color: #fff; cursor: not-allowed; }

/* Generic action sheet */
.sheet-actions { display: grid; gap: 8px; }
.sheet-actions.row { grid-template-columns: 1fr 1fr; }
.sheet-actions.row button.cancel { margin-top: 0; order: 2; }
.sheet-actions button { border: 0; padding: 15px; border-radius: 12px; font-weight: 700; font-size: 15px; cursor: pointer; background: var(--jem-gainsboro-40); color: var(--ink); width: 100%; font-family: inherit; }
.sheet-actions button:active { filter: brightness(0.96); }
/* Danger red, not the brand pink. Pink is this app's ACCENT — the FAB, a foal's avatar — so a pink
   "Discard movement" read as the encouraged action rather than the destructive one. Matches the
   detail panel's own Delete move button (.act.danger), which was already red. */
.sheet-actions button.destructive { background: var(--jem-danger); color: #fff; }
.sheet-actions button.cancel { background: #fff; color: var(--ink-2); border: 1px solid var(--line); margin-top: 2px; }

/* Generic alert */
/* opacity:0 and pointer-events:none hide NOTHING from assistive technology or the tab order, so the
   closed alert used to expose its focusable OK button on every step of the wizard. visibility does
   (JGMA-46), with the flip to hidden delayed by the 160ms fade so the alert still fades out. */
.alert-backdrop {
  position: absolute; inset: 0; background: rgba(15,23,42,0.5); z-index: 60; opacity: 0; pointer-events: none;
  display: grid; place-items: center; padding: 0 34px;
  visibility: hidden;
  transition: opacity 160ms, visibility 0s linear 160ms;
}
.alert-backdrop.show {
  opacity: 1; pointer-events: auto; visibility: visible;
  transition: opacity 160ms, visibility 0s;
}
.alert-modal { background: #fff; border-radius: 16px; width: 100%; max-width: 280px; padding: 22px 20px 14px; text-align: center; transform: scale(0.92); transition: transform 160ms; }
.alert-backdrop.show .alert-modal { transform: scale(1); }
.alert-modal .a-msg { font-size: 14px; color: var(--ink); line-height: 1.45; font-weight: 500; margin-bottom: 16px; }
.alert-modal .a-ok { border: 0; border-top: 1px solid var(--line); background: transparent; color: var(--jem-forest); font-weight: 800; font-size: 15px; padding: 12px; width: calc(100% + 40px); margin: 0 -20px -14px; cursor: pointer; font-family: inherit; }

/* ============================================================================
   Screen 20d — Movement details (Pages/MovementDetails.razor)
   Edit an existing move: add / remove horses; destination + date are locked.
   ============================================================================ */
/* Same padding + 34px control height as the wizard's .m-nav, so the Cancel chevron lines up
   vertically with the wizard back arrow when moving between Movement details ↔ Select Horses. */
/* Movement details reuses the wizard's .m-nav wholesale (see its block above) — this is only the
   Save button, which the wizard has no equivalent for. A real filled button, not green text: it is
   the one committing action on the screen and needs to look pressable, which bare text does not. */
.m-nav .nav-done {
  margin-left: auto; flex: 0 0 auto;
  height: 34px; padding: 0 16px; border: 0; border-radius: 9px;
  background: var(--jem-green); color: #fff;
  font: inherit; font-size: 14px; font-weight: 700; cursor: pointer;
  display: inline-flex; align-items: center;
  transition: background 140ms, transform 100ms;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) {
  .m-nav .nav-done:hover { filter: brightness(0.92); }
}
.m-nav .nav-done:active { filter: brightness(0.88); transform: scale(0.96); }
.m-nav .nav-done:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

.m-body { flex: 1 1 auto; overflow-y: auto; padding: 18px 16px 24px; background: var(--bg); }
.m-body::-webkit-scrollbar { width: 0; }

.summary { background: var(--jem-charcoal); color: #fff; border-radius: 16px; padding: 8px 18px; margin-bottom: 20px; }
.summary .row { display: flex; align-items: baseline; gap: 12px; padding: 7px 0; }
.summary .row + .row { border-top: 1px solid rgba(255,255,255,0.08); }
.summary .lbl { font-size: 10px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--ink-4); font-weight: 700; flex: 0 0 96px; }
.summary .val { font-size: 16px; font-weight: 800; }

.sec-head { font-size: 11px; font-weight: 800; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.5px; margin: 0 2px 10px; display: flex; align-items: center; gap: 8px; }
.sec-head .n { margin-left: auto; color: var(--jem-forest); }
.ros-list { display: grid; gap: 10px; margin-bottom: 14px; }
/* 20d reuses the per-horse card + the Paddock/Yard/Box picker, but with a larger avatar, a
   separate remove (✕) button, and a header split into a clickable toggle + that button. The
   shared .hd-* / .ph-* styles live in the section above; these scoped rules override only what
   differs for the roster. */
.hd-list .hd-item { margin-bottom: 0; }
.hd-list .hd-head { gap: 6px; padding: 9px 10px 9px 11px; cursor: default; }
.hd-list .hd-head:active { background: transparent; }
.hd-list .hd-toggle { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 11px; cursor: pointer; }
.hd-list .hd-toggle:active { opacity: 0.7; }
/* A card with no spot to set (a departure) has nothing to open, so its head is not a control. */
.hd-list .hd-toggle.static, .hd-list .hd-toggle.static:active { cursor: default; opacity: 1; }
.hd-list .hd-ic { width: 40px; height: 40px; border-radius: 9px; font-size: 15px; flex: 0 0 40px; }
.hd-list .hd-name { font-size: 14px; display: flex; align-items: center; gap: 7px; overflow: visible; white-space: normal; }
.hd-list .hd-name .nm-txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hd-list .hd-sel { margin-top: 2px; white-space: normal; }
.hd-list .hd-caret { width: 22px; }
/* The roster card is narrower than the wizard's (it carries a remove button), so the spot labels
   sit above their selects rather than beside them. */
.hd-list .hd-spot .ph-cell { grid-template-columns: minmax(0, 1fr); gap: 4px; }
/* The same remove control as Step 1's .sel-row .s-x — same rounded-square shape, same red
   hover/press. It had no hover state at all and pressed PINK, so the two screens' remove buttons
   behaved and coloured differently for an identical action. */
.hd-list .rm {
  width: 34px; height: 34px; flex: 0 0 34px;
  display: grid; place-items: center;
  border: 1px solid var(--line-2); border-radius: 9px; background: #fff;
  color: var(--ink-4); font-size: 15px; line-height: 1; font-family: inherit; cursor: pointer;
  transition: background 140ms, color 140ms, border-color 140ms, transform 100ms;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) {
  .hd-list .rm:hover { background: var(--jem-danger-soft); border-color: var(--jem-danger); color: var(--jem-danger); }
}
.hd-list .rm:active { background: var(--jem-danger-soft); border-color: var(--jem-danger); color: var(--jem-danger); transform: scale(0.9); }
.hd-list .rm:focus-visible { outline: 2px solid var(--jem-danger); outline-offset: 2px; }

.empty-roster { text-align: center; padding: 26px 20px; color: var(--ink-3); font-size: 13px; background: #fff; border: 1px dashed var(--line-2); border-radius: 14px; margin-bottom: 14px; line-height: 1.5; }
.empty-roster .big { font-size: 14px; font-weight: 700; color: var(--ink-2); margin-bottom: 3px; }

/* ============================================================================
   Blazor framework UI (preserved from the template): boot loader + error bar.
   ============================================================================ */
#blazor-error-ui {
  color-scheme: light only;
  background: lightyellow;
  bottom: 0;
  box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
  box-sizing: border-box;
  display: none;
  left: 0;
  padding: 0.6rem 1.25rem 0.7rem 1.25rem;
  position: fixed;
  width: 100%;
  z-index: 1000;
}
#blazor-error-ui .dismiss {
  cursor: pointer;
  position: absolute;
  right: 0.75rem;
  top: 0.5rem;
}

.blazor-error-boundary {
  background: var(--jem-danger);
  padding: 1rem 1rem 1rem 1rem;
  color: white;
}
.blazor-error-boundary::after {
  content: "An error has occurred."
}

.loading-progress {
  position: absolute;
  display: block;
  width: 8rem;
  height: 8rem;
  inset: 20vh 0 auto 0;
  margin: 0 auto 0 auto;
}
.loading-progress circle {
  fill: none;
  stroke: var(--ink-3);
  stroke-width: 0.6rem;
  transform-origin: 50% 50%;
  transform: rotate(-90deg);
}
.loading-progress circle:last-child {
  stroke: var(--jem-green);
  stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
  transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
  position: absolute;
  text-align: center;
  font-weight: bold;
  color: var(--line-2);
  inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
  content: var(--blazor-load-percentage-text, "Loading");
}

/* Brief message shown by AuthorizeRouteView's <Authorizing> while MSAL resolves the
   sign-in state on first load (before the redirect to Entra ID takes over). */
.auth-status {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Fills .layout below the banner (JGMA-84); it was min-height: 100vh, which the banner would push
     past the bottom of the screen. */
  flex: 1 1 auto;
  padding: 24px;
  color: var(--jem-info-soft);
  font-family: var(--font);
  font-size: 15px;
  text-align: center;
}

/* Signed-out screen (RemoteAuthenticatorView LogOutSucceeded) — a working way back in. */
.auth-status .signed-out { display: flex; flex-direction: column; align-items: center; gap: 18px; }
.auth-status .so-msg { color: #fff; font-size: 17px; font-weight: 600; }
.auth-status .so-signin { background: var(--jem-green); color: #fff; border: 0; border-radius: 12px; padding: 13px 30px; font-size: 15px; font-weight: 700; cursor: pointer; font-family: inherit; }
.auth-status .so-signin:active { filter: brightness(0.88); }

/* ---------------------------------------------------------------------------
   JGMA-8 — each horse's sex and year of birth on the per-horse card, and a
   foal drawn under its dam.
   --------------------------------------------------------------------------- */

/* The horse itself: "Mare · 2017". A third text line in .hd-main, under the name
   and above the destination summary. Muted, because it is context rather than the
   thing the card is for — the destination stays the loudest line. Wraps like
   .hd-sel does inside .hd-list rather than ellipsising, so a long value makes the
   card taller instead of hiding half of itself. */
.hd-det { font-size: 12px; font-weight: 500; color: var(--ink-3); margin-top: 2px; }
.hd-list .hd-det { white-space: normal; }

/* A foal shown directly beneath its dam, indented so the pairing reads without
   opening anything. Keyed on .under-dam and NOT on .foal: a foal whose dam is not
   in the same movement keeps its own place in the list and must not be indented,
   or the indent would claim a parentage that is not there.
   Scoped to .ros-list (Movement details) — the wizard's Step 2 keeps its name
   order and is never passed the flag.
   16px: enough to read as subordinate, small enough to leave the remove ✕ and the
   caret reachable at 360px, where this row is the tightest in the list.
   The accent is an inset shadow rather than a border-left so it cannot fight
   .hd-item.open's border-color; var(--jem-pink) is the pink already in the foal icon
   gradient. */
.ros-list .hd-item.under-dam { margin-left: 16px; box-shadow: inset 3px 0 0 var(--jem-pink); }

/* ---------------------------------------------------------------------------
   JGMA-5 — Shared UI states: busy, loading and failure

   Three patterns every later ticket inherits. The skeleton sits in the list like
   any other content; the veil and the dialog are narrowed to the same 480px column
   as .app-shell, but anchored to the VIEWPORT rather than to the shell — see the
   rule immediately below for why that is not the obvious choice.

   The z-index ladder inside the shell, for reference:
     FAB 20 · sheet 30/31 · detail 40/41 · calendar 55/56 · wizard alert 60 ·
     delete sheet 70/71 · [busy veil 80] · [failure dialog 90] · error UI 1000
   The veil has to clear 71: a confirm or a flag is saved from inside an open
   sheet, so a veil beneath one would leave its buttons live.

   The failure dialog COPIES the .alert-* values rather than reusing those
   classes. They carry z-index 60 and belong to the wizard's validation pop-up,
   which that ticket must not touch, so sharing them would have meant editing
   rules another screen depends on.
   (This used to cite the append-only rule as the reason. That rule was withdrawn
   on 14 Sep 2026 — see the top of this file's neighbours — so the reason is now
   simply blast radius, not a prohibition. The copy itself still stands.)
   --------------------------------------------------------------------------- */

/* Both overlays are FIXED to the viewport, then narrowed and centred so they cover
   exactly the same 480px column as .app-shell (max-width: 480px; margin: 0 auto).

   Not `position: absolute; inset: 0` inside the shell, which is the obvious thing and
   is wrong: the shell is `overflow: hidden`, and an overflow-hidden box is still a
   SCROLL CONTAINER — the browser scrolls it to keep a focused element in view. Its
   scrollHeight already exceeds its height, because the hidden calendar sheet sits
   below the fold on a translateY(100%). So tapping a button low in a sheet scrolls the
   shell by a few hundred pixels and drags every absolutely-positioned child up with
   it. The existing sheets and the detail modal are shifted by that too; an overlay
   that must be centred cannot afford to be.

   Fixed also escapes the shell's clipping, which is what we want here and only here.
   z-index still compares against the sheets: .app-shell is `z-index: auto`, so it
   creates no stacking context. */
.busy-veil,
.fail-backdrop {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  margin: 0 auto;
  max-width: 480px;
}

/* ---------- Busy veil (AC 1) ---------- */
.busy-veil {
  z-index: 80;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  background: rgba(15, 23, 42, 0.45);
  /* NO pointer-events: none. Swallowing pointer events is what stops both @onclick
     and swipe.js's pointerdown handlers, which is how AC 1's "does not accept
     further interaction" is actually met. */
}

.busy-spinner {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 3px solid rgba(255, 255, 255, 0.28);
  border-top-color: #fff;
  animation: busy-spin 700ms linear infinite;
}

.busy-label {
  color: #fff;
  font-family: var(--font);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.01em;
}

@keyframes busy-spin {
  to { transform: rotate(360deg); }
}

/* Respect a groom who has asked for less motion: the veil still says "busy", it
   just pulses instead of spinning. */
@media (prefers-reduced-motion: reduce) {
  .busy-spinner {
    animation: busy-pulse 1.4s ease-in-out infinite;
  }

  @keyframes busy-pulse {
    0%, 100% { opacity: 0.35; }
    50% { opacity: 1; }
  }
}

/* ---------- Loading placeholder (AC 2, AC 3) ---------- */
.skel-list { padding: 2px 0; }

.skel-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  padding: 13px 14px;
  margin-bottom: 10px;
}

.skel-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}

.skel-line {
  height: 11px;
  border-radius: 5px;
  /* A highlight swept across a bar — one declaration, no extra element.
     THE RAMP WAS --bg-2 -> --line -> --bg-2, AND THAT IS #eef1f5 AGAINST #e2e8f0: twelve levels of
     grey apart, on an 11px bar, on a white card. The animation ran the whole time and essentially
     nobody could see it. The bar now sits at --line and a --card highlight passes THROUGH it, which
     is the usual shimmer and about thirty levels of contrast instead. */
  background: linear-gradient(90deg, var(--line) 25%, var(--card) 37%, var(--line) 63%);
  background-size: 400% 100%;
  animation: skel-sweep 1.3s ease-in-out infinite;
}

.skel-line.horse { flex: 1 1 auto; max-width: 150px; }
.skel-line.date { width: 54px; flex: 0 0 auto; height: 10px; }
.skel-line.route { width: 78%; height: 9px; }

@keyframes skel-sweep {
  from { background-position: 100% 0; }
  to { background-position: 0 0; }
}

/* Still the skeleton, just not moving — and it must stay VISIBLE, since a motionless bar is the only
   thing distinguishing "not loaded yet" from an empty card here. Matches the swept bar's base rather
   than the old --bg-2, which would now read as a different, fainter component. */
@media (prefers-reduced-motion: reduce) {
  .skel-line {
    animation: none;
    background: var(--line);
  }
}

/* ---------- Failure dialog (AC 4 to AC 10) ---------- */
/* Unlike the sheets, this one is CREATED when a failure is raised and removed when it
   is dismissed, rather than sitting in the DOM waiting for a .show class. So it
   animates on entry with a keyframe: an opacity TRANSITION would never fire, because
   the element never exists at the starting value long enough to transition from it. */
.fail-backdrop {
  z-index: 90;
  display: grid;
  place-items: center;
  /* 20px, not the alert's 34px: at 360px the four-case wording needs the width. */
  padding: 0 20px;
  background: rgba(15, 23, 42, 0.5);
  animation: fail-fade 160ms ease-out;
}

.fail-modal {
  width: 100%;
  max-width: 300px;
  padding: 22px 20px 16px;
  border-radius: 16px;
  background: #fff;
  text-align: center;
  animation: fail-pop 160ms ease-out;
}

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

@keyframes fail-pop {
  from { transform: scale(0.92); }
  to { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .fail-backdrop,
  .fail-modal { animation: none; }
}

.fail-modal .f-msg {
  margin-bottom: 18px;
  color: var(--ink);
  font-family: var(--font);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.45;
}

/* WHAT failed, beneath the message (JGMA-80) - a file name. Pulled up into the message's bottom
   margin so the two read as one block above the buttons. break-word, because a phone camera's
   name is one long unbroken token and must not widen the dialog past 360px. */
.fail-modal .f-detail {
  margin: -12px 0 18px;
  color: var(--ink-3);
  font-family: var(--font);
  font-size: 13px;
  line-height: 1.4;
  overflow-wrap: anywhere;
}

/* A column, not a row: "Something went wrong and your change wasn't saved…"
   sits above two buttons that must stay tappable at 360px. */
.fail-modal .f-actions { display: grid; gap: 8px; }

.fail-modal .f-btn {
  width: 100%;
  padding: 13px;
  border: 0;
  border-radius: 12px;
  font-family: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}

.fail-modal .f-btn:active { filter: brightness(0.96); }

/* The way forward — retrying, or signing back in. */
.fail-modal .f-btn.primary { background: var(--jem-green); color: #fff; }

/* Acknowledging it. Bordered rather than filled, so it never competes. */
.fail-modal .f-btn.quiet {
  background: #fff;
  color: var(--ink-2);
  border: 1px solid var(--line);
}

.fail-modal .f-btn:disabled {
  /* Only ever the tail of the minimum-visible hold — the dialog is legible immediately, but
     inert until the veil lifts. Kept subtle: for that fraction of a second a greyed-out
     button would read as broken rather than as busy. */
  opacity: 0.55;
  cursor: default;
}

/* ---------- JGMA-6 · per-horse box assignment in the confirm/flag sheet ---------- */

/* The sheet now carries a per-horse roster, so its body has to scroll and the actions row has to
   stay put. This is the .detail-modal pattern already in this file (max-height, hidden overflow,
   flex column, one scrolling child), applied to #sheet ONLY.

   #sheet, never .sheet: three panels share that class — #loc-sheet (which JGMA-9 rewrites),
   #sheet and #del-sheet — and restructuring it into a flex column would restructure all three.
   An id outranks a class, so this block simply wins and nothing above needs editing. */
#sheet {
  max-height: calc(100% - 56px);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* Relaxed from .sheet's `pinch-zoom`, and this is the whole reason the scroll works.
     touch-action is INTERSECTED with every ancestor, so a descendant cannot re-enable a behaviour
     disallowed above it: `pan-y` on .sh-body under a `pinch-zoom` panel is inert, and the roster
     would not scroll under a finger at all — while scrolling perfectly with a mouse wheel, so a
     desktop browser never shows the bug. .detail-modal declares no touch-action for exactly this
     reason and restricts its fixed children instead; the two rules below do the same.

     Consequence, accepted on this ticket: swipe-down-to-dismiss now lives on the head and the
     actions row rather than "anywhere on the sheet". That is how the detail modal already behaves. */
  touch-action: auto;
}
#sheet .sh-head { flex: 0 0 auto; touch-action: pinch-zoom; }
#sheet .actions { flex: 0 0 auto; touch-action: pinch-zoom; }
/* Everything inside here stays at the inherited `auto`. Restricting the date field, the messages or
   the flag-mode note would make them dead: they could not scroll (panning forbidden) and could not
   dismiss either, because attachDragDismiss bails out for a press inside the scroll region. */
#sheet .sh-body { flex: 1 1 auto; overflow-y: auto; }
#sheet .sh-body::-webkit-scrollbar { width: 0; }

/* The farm switcher, bounded and scrollable (JGMA-58) — the same structure as #sheet above, and for
   the same reason it is written against the ID.

   WITHOUT THIS THE LIST HAS NO CEILING. .sheet is `bottom: 0` with no max-height, so the panel grows
   UPWARDS out of the top of the app as farms are added: at eight farms it already reaches 540px, and
   a groom on more than about a dozen loses the top rows off-screen with no way to reach them. Capped
   here at 73% of the shell, with the list itself taking the overflow. */
#loc-sheet {
  max-height: 73%;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* Relaxed from .sheet's `pinch-zoom`, exactly as #sheet is, and it is what makes the scroll work
     ON A TOUCH DEVICE. touch-action is INTERSECTED with every ancestor, so `pan-y` on .loc-list
     under a `pinch-zoom` panel is inert — the list would scroll perfectly under a mouse wheel and
     not move at all under a finger, which no desktop browser reveals.

     Consequence, the same one #sheet accepts: swipe-down-to-dismiss now lives on the grab zone
     rather than anywhere on the panel. */
  touch-action: auto;
}
/* Outside the scroller, so restricting it is safe — and it is what drag-dismiss now has to grab. */
#loc-sheet .grab-zone { flex: 0 0 auto; }
/* min-height: 0 is load-bearing. A flex item's default `min-height: auto` refuses to shrink below
   its content, so without it the list wins the argument, the panel grows past its max-height and
   nothing scrolls at all. */
#loc-sheet .loc-list { flex: 1 1 auto; overflow-y: auto; min-height: 0; }
#loc-sheet .loc-list::-webkit-scrollbar { width: 0; }
/* The roster's own controls came from the wizard, where they sit on a page rather than in a sheet. */
#sheet .section-lbl { margin-top: 14px; }
#sheet .ros-list { margin-bottom: 10px; }

/* Two horses in one box. A background tint, deliberately NOT a border-color and NOT an inset
   box-shadow: border-color would outrank .hd-item.open on specificity and cost an open clashing
   card its green edge, and an inset shadow would fight .under-dam's pink one (which JGMA-8 chose
   precisely so it could not fight .hd-item.open). A tint stacks with both. */
#sheet .hd-item.clash { background: var(--jem-warning-soft); }

/* The clash note. Shaped like .s2-err but amber, because it warns without blocking — two horses may
   legitimately share a box on some yards — and it persists rather than clearing on a timer, so it is
   still on screen when the groom reaches Save. */
#sheet .clash-note {
  margin: 2px 2px 12px; font-size: 12px; font-weight: 600; line-height: 1.45;
  color: var(--jem-warning); background: var(--jem-warning-soft); border-radius: 8px; padding: 8px 10px;
}

/* ---------------------------------------------------------------- JGMA-11 */
/* Regular Movements — the read-only list of standing yard-box to paddock
   pairings, and the row on Today's moves that reaches it. */

/* The explanatory line, verbatim from the legacy screen. Sits above the list and
   outside its state switch, so it reads as a description of the screen rather
   than of whatever the list happens to be showing. */
.rm-explain {
  padding: 12px 14px 4px; font-size: 13px; line-height: 1.5; color: var(--ink-3);
}

/* A row states one pair and nothing else. Shaped like a move card so the screen
   reads as the same family, but flatter: there is no date, no badge and no
   swipe, so it does not need a card's weight. A button, not a div, because it is
   the tap target — keyboard focus and Enter come free. */
.rm-row {
  width: 100%; text-align: left; font: inherit; cursor: pointer;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-bottom: 8px; padding: 13px 12px;
  background: var(--card); border: 1px solid var(--line); border-radius: 10px;
}
.rm-row:active { background: var(--jem-gainsboro-20); }
.rm-row .ep { font-weight: 600; color: var(--ink-2); font-size: 14px; }
/* One-way, matching the move cards' route arrow. The legacy screen asked for a
   two-way icon and rendered arrow-right regardless (ListItem.js:131). */
.rm-row .arrow { color: var(--jem-green); font-weight: 800; }
.rm-row .chev { margin-left: auto; color: var(--ink-3); font-size: 20px; line-height: 1; }

/* The entry point at the bottom of the To-do list. Deliberately NOT a card: it is
   navigation, not a movement, and sitting among the move cards it has to say so.
   The top border separates it from the last card without boxing it. */
.reg-moves-link {
  width: 100%; text-align: left; font: inherit; cursor: pointer;
  display: flex; align-items: center; gap: 8px;
  margin-top: 4px; padding: 15px 12px;
  background: transparent; border: 0; border-top: 1px solid var(--line);
  font-size: 14px; font-weight: 600; color: var(--ink-2);
}
.reg-moves-link:active { background: var(--jem-gainsboro-40); }
.reg-moves-link .chev { margin-left: auto; color: var(--ink-3); font-size: 20px; line-height: 1; }
/* ---------------------------------------------------------------------------
   JGMA-13 — a horse's photograph in the per-horse card's tile.

   WAS scoped to .hd-ic and nothing else, with a note that the four other classes
   drawing the same kind of initial tile were out of scope. JGMA-68 brought three
   of them in — .r-ic (step 1's results), .s-ic (step 1's selections) and .rh-ic
   (step 4's review) — so the wizard shows the same horse the same way the roster
   does, and JGMA-93 brought in .mb-ic, the search page's mini-bio, which the client
   asked to show photographs too. Search's result rows are .r-ic, so they were
   already covered once they rendered a HorseTile. .loc-ic is still out, and
   correctly: it is a PLACE, and a paddock has no photograph.

   Everything here keys on .has-photo, which HorseTile sets only while an img is
   actually showing. A tile with no photograph, or one whose photograph failed,
   matches none of it and renders exactly as it did before.
   --------------------------------------------------------------------------- */

/* The tile becomes a FRAME: a white mount with a thin grey edge (JGMA-93). overflow:hidden is
   what clips the photograph to the existing border-radius, so the img needs no radius of its own
   and the two can never disagree. Each tile keeps its own size and radius — those are set where the
   tile is — and gains only the mount, the edge and the clip.

   NO GRADIENT BEHIND A PHOTOGRAPH, loading or loaded (client feedback 24 Sep 2026: "a little
   ugly"). It used to be left underneath deliberately, to show in the instant before the image
   paints; a plain white mount does that job more quietly. The background is set at ROW specificity
   because the gradients are (.res-row.horse .r-ic, .hd-item.foal .hd-ic, …), and this rule comes
   later, so it wins on source order rather than by an override block. A tile with no photograph —
   the initial — keeps its gradient, untouched.

   THE FOAL EDGE STAYS PINK, and is required rather than decorative: the pink-vs-green gradient IS
   how a foal's tile is told apart at a glance, and a photograph covers it completely — leaving the
   FOAL chip as the only per-card signal. So a foal's frame is pink where every other is grey.
   box-sizing: border-box is already global, so no tile's outer size changes and nothing in the row
   shifts. Starting point only: the client will refine it in testing. */
.hd-item .hd-ic.has-photo,
.res-row .r-ic.has-photo,
.sel-row .s-ic.has-photo,
.rv-horse .rh-ic.has-photo,
.mini-bio .mb-ic.has-photo {
  background: #fff; border: 1px solid var(--line-2); overflow: hidden;
}
.hd-item.foal .hd-ic.has-photo,
.res-row.foal .r-ic.has-photo,
.sel-row.foal .s-ic.has-photo,
.rv-horse.foal .rh-ic.has-photo,
.mini-bio.foal .mb-ic.has-photo { border: 2px solid var(--jem-pink); }

/* cover, not contain: a head shot is not square and the tile is, so letterboxing
   it would leave two bands of white and read as a mistake. Cropping the sides
   of a head shot is the lesser loss.

   width/height at 100% of a FIXED box is also the real no-reflow guarantee — the
   img's own width/height attributes only supply an intrinsic ratio; the row's
   geometry never depended on the image at all. */
.hd-ic.has-photo img,
.r-ic.has-photo img,
.s-ic.has-photo img,
.rh-ic.has-photo img,
.mb-ic.has-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The photograph's loading spinner (JGMA-80), laid over the tile until the img's load or error
   fires. Keyed on .loading, which HorseTile sets only then, so every tile above gains it without a
   per-tile rule. GREY on the white mount since JGMA-93 — it was white on the gradient, and white on
   white is no spinner at all. It reuses busy-spin and, under reduced motion, pulses as
   .busy-spinner does. */
.has-photo.loading { position: relative; }
.ht-spin {
  position: absolute; inset: 0; margin: auto; width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid var(--line-2); border-top-color: var(--ink-3);
  animation: busy-spin 700ms linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .ht-spin { animation: busy-pulse 1.4s ease-in-out infinite; }
}

/* JGMA-29 — Regular Movements: the failed-load state.
   The list region can fail now that it is backed by a query rather than a static table. Styled on
   .empty-state rather than as a dialog: it is a LIST STATE, not a FailureKind, and the two are
   deliberately different mechanisms. */
.rm-retry {
    margin-top: 12px;
    padding: 8px 20px;
    font: inherit;
    color: var(--ink, #1a1a1a);
    background: #fff;
    border: 1px solid var(--line, #d8d4cc);
    border-radius: 999px;
    cursor: pointer;
}

.rm-retry:active {
    background: var(--wash, #f4f1ec);
}


/* ------------------------------------------------------------------ JGMA-9
   Today's moves header: the day strip, its calendar sheet, and the search slot.

   Only the rules this ticket INTRODUCES live here. The three it CHANGED — .m-header's padding,
   .m-tabs' margin-top and .m-header .who — are edited in place above rather than shadowed from
   down here, which is what the withdrawn append-only rule used to force.
   ------------------------------------------------------------------------- */

/* ---------- The day strip ---------- */

/* The name-and-date line lives in here now, so it takes this row's treatment
   rather than its own. `space-between` is what puts the calendar button at the
   far end. */
.dpick-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  font-size: 11px; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-4);
}

/* No padding, so this aligns vertically with the log-out button it sits near —
   the two were visibly off by a couple of pixels while both had their own. */
/* The two controls at the end of the row, kept together — .dpick-head is space-between, which would
   otherwise spread the name, Today and the calendar evenly across it. */
.dp-actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }

/* The Today button is a .mini-btn, minus that class's uppercase inheritance from .dpick-head —
   "TODAY" shouted next to a date that is already uppercase was one emphasis too many. It KEEPS the
   1px letter-spacing, so it sits at the same rhythm as the other .mini-btn controls. (It read
   "as Reset demo a row above it" until that button was removed; the rhythm is unchanged.) */
.dp-today { text-transform: none; line-height: 1.4; display: inline-flex; align-items: center; gap: 5px; }
/* Overdue work is pinned to today, so the button that returns there is the one that should say so.
   Only the resting state goes red — the hover treatment is shared with every other .mini-btn, and a
   control that changed colour scheme on hover would read as a different button. */
.dp-today.late { color: var(--jem-late); border-color: var(--jem-late); }

/* Behaves exactly like .logout-btn, because it sits a few pixels away and anything less made the
   two look like different kinds of control. Same 30x30 target, same hover/active wash, same focus
   ring — only the icon differs. */
.dp-cal {
  width: 30px; height: 30px; border-radius: 8px; flex: 0 0 30px;
  display: grid; place-items: center; padding: 0;
  color: var(--ink-4); background: transparent; border: 0; cursor: pointer;
  transition: background 140ms, color 140ms;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) {
  .dp-cal:hover { background: rgba(255, 255, 255, 0.14); color: #fff; }
}
.dp-cal:active { background: rgba(255, 255, 255, 0.14); color: #fff; }
.dp-cal:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

.dpick-row { display: flex; align-items: center; margin-top: 2px; }

/* THE ARROWS GROW INWARDS, AND THE STRIP NO LONGER TAKES THE WHOLE ROW (JGMA-75).

   These two rules are one change and have to be read together. The strip used to
   be `flex: 1 1 auto` against two 24px arrows, so on a wide screen it was nine
   cells across — and paint() fades a cell by distance, `1 - d * 0.30`, so
   everything from four days out was at opacity ZERO and still tappable. A groom
   aiming at the previous-day arrow hit an invisible date and jumped several days.

   So the strip is capped at five cells (5 x 46px = 230px, the range REACH admits
   in js/datepicker.js) and the arrows absorb everything left over. On a 360px
   phone that makes each arrow ~55px instead of 24; in the 480px shell, ~115px.
   Both are a far better target than the thin glyph, which is the other half of
   what was asked for — and the glyph stays centred in its box, so visually the
   arrow simply sits further out rather than looking different.

   `flex-shrink: 1` with `min-width: 0` on the view, so a viewport narrower than
   230 + 48 still degrades gracefully rather than overflowing the header. */
.dp-arrow {
  flex: 1 1 24px; min-width: 24px; height: 46px; background: transparent; border: 0;
  color: var(--ink-4); font-size: 19px; line-height: 1; cursor: pointer;
  border-radius: 8px; font-family: inherit; padding: 0;
}
.dp-arrow:active { color: #fff; background: rgba(255, 255, 255, 0.12); }
.dp-arrow:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/* touch-action: pan-y so a vertical scroll of the list still works with a finger
   that starts on the strip; the strip only claims the horizontal axis.

   230px must stay 5 x .dp-cell's width, and REACH in js/datepicker.js must stay
   2: the script clamps a tap to REACH days, and this is the geometry that makes
   the clamp agree with what is on screen. Widen one without the other and either
   invisible cells become reachable again, or visible ones stop responding. */
.dp-view { flex: 0 1 230px; min-width: 0; height: 46px; overflow: hidden; position: relative; touch-action: pan-y; cursor: grab; }
.dp-view:active { cursor: grabbing; }
.dp-track { position: absolute; inset: 0 auto 0 0; width: 0; will-change: transform; }

/* 46px must match CELL in js/datepicker.js — the script positions every cell by
   absolute left, so the two cannot drift apart silently. It is also the unit
   .dp-view's 230px is five of; see the note there. */
.dp-cell {
  position: absolute; top: 0; width: 46px; height: 46px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; user-select: none; cursor: pointer;
}
.dp-cell .dw { font-size: 9px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-4); }
.dp-cell .dn { width: 28px; height: 28px; display: grid; place-items: center; border-radius: 50%; font-size: 15px; font-weight: 700; color: #fff; }
.dp-cell.on .dn { background: var(--jem-green); }
.dp-cell.on .dw { color: #cbd5e1; }
/* A DAY CARRYING OVERDUE WORK, marked whether or not it is the selected one — the whole point is to
   show where the lateness is without the groom visiting each day to find out. Declared AFTER .on so
   it wins at equal specificity on a day that is both: selection is still legible from the strip's
   own centring, scale and opacity, whereas lateness has nothing else to say it. */
.dp-cell.late .dn { background: var(--jem-late); }

/* ---------- The calendar sheet's grab affordance ---------- */

/* Everything else the sheet needs — .cal-backdrop, .cal-sheet, .cal-head, .cal-dow, .cal-grid and
   .cal-day — ALREADY EXISTS above, and CalendarSheet.razor already renders it. Only the grab zone
   is new, and it deliberately carries the identical values to `.detail-modal .dm-grab-zone` rather
   than a near-copy, so the two cannot drift. */
.cal-sheet .dm-grab-zone { flex: 0 0 auto; padding: 10px 0 4px; cursor: grab; touch-action: none; }
.cal-sheet .dm-grab-zone:active { cursor: grabbing; }
.cal-sheet .grab { width: 38px; height: 4px; background: #cbd5e1; border-radius: 4px; margin: 0 auto; }

/* ------------------------------------------------------------------ JGMA-12
   Global search: the header magnifier, the horse mini-bio and the paddock /
   yard expansion.

   Everything else this screen draws already exists — .search-input, .res-row
   and its modifiers, .s2-group-lbl, .s2-results, .empty-state, the sheet. The
   screen wraps its results in .s2-results ON PURPOSE: the inter-group spacing
   rule above is `.s2-results .res-row + .s2-group-lbl`, scoped to that
   ancestor, so without the wrapper every group after the first loses its 14px.
   ------------------------------------------------------------------------- */

/* The magnifier, in the 32x32 slot JGMA-9 reserved.

   RE-DECLARED AT THE SAME SPECIFICITY as the slot rule further up, not appended
   at a lower one. That rule is `.m-header .top .search-slot` — (0,3,0) — and
   sets `display:block`; a bare `.search-btn` here would lose to it and the SVG
   would lay out as a block child instead of centring.

   The reset is the one .logout-btn carries a few rules below the slot, because
   this button sits beside it and anything less makes a UA-styled border and an
   off-centre glyph inside the 32px box. Width, height and flex are inherited
   from the slot rule and deliberately not repeated. */
.m-header .top .search-slot {
  display: grid; place-items: center;
  padding: 0; background: transparent; border: 0; border-radius: 8px;
  color: var(--ink-4); cursor: pointer;
  transition: background 140ms, color 140ms;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) {
  .m-header .top .search-slot:hover { background: rgba(255, 255, 255, 0.14); color: #fff; }
}
.m-header .top .search-slot:active { background: rgba(255, 255, 255, 0.14); color: #fff; }
.m-header .top .search-slot:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/* "Back to results" — a text control above the mini-bio and the location list.
   Not the .m-nav back chevron: that dismisses the whole screen, and these two
   must not look like the same affordance. */
.sr-back {
  display: inline-flex; align-items: center; gap: 4px;
  margin: 0 0 12px; padding: 0;
  background: none; border: 0; cursor: pointer;
  font: inherit; font-size: 12px; font-weight: 700; color: var(--jem-info);
  -webkit-tap-highlight-color: transparent;
}
.sr-back:focus-visible { outline: 2px solid var(--jem-info); outline-offset: 3px; border-radius: 4px; }

/* The mini-bio. A CARD rather than a row, because it is a destination and not a
   list item — the initial tile is the same gradient .res-row.horse uses, scaled
   up, so a horse looks like itself in both places. Its photograph, when it has one,
   is in the photo-tile block above (.mb-ic.has-photo, JGMA-93). */
.mini-bio {
  border: 1px solid var(--line); border-radius: 12px; background: #fff;
  padding: 18px; text-align: center;
}
.mini-bio .mb-ic {
  width: 64px; height: 64px; border-radius: 14px; margin: 0 auto 12px;
  display: grid; place-items: center;
  background: linear-gradient(135deg, var(--jem-green-40), var(--jem-green));
  color: #fff; font-weight: 800; font-size: 24px;
}
.mini-bio .mb-name {
  font-size: 17px; font-weight: 800; color: var(--ink);
  display: flex; align-items: center; justify-content: center; gap: 6px;
}
.mini-bio .mb-dis { font-size: 13px; color: var(--ink-3); margin-top: 3px; }
.mini-bio .mb-loc {
  font-size: 12px; font-weight: 600; color: var(--jem-forest);
  margin-top: 6px; display: flex; align-items: center; justify-content: center; gap: 4px;
}
.mini-bio .mb-move {
  margin-top: 16px; width: 100%;
  padding: 12px; border: 0; border-radius: 10px;
  background: var(--jem-green); color: #fff;
  font: inherit; font-size: 14px; font-weight: 800; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.mini-bio .mb-move:active { background: var(--jem-forest); }
.mini-bio .mb-move:focus-visible { outline: 2px solid var(--jem-forest); outline-offset: 2px; }

/* "Move all N horses", at the head of an expanded paddock or yard (JGMA-77).

   THE SAME BUTTON AS .mb-move, because it is the same action reached from the
   other kind of result — a groom who has learnt what the green full-width
   button on a horse does should not have to learn a second control for the
   location. Declared separately rather than by widening .mb-move's selector:
   that rule is scoped to .mini-bio, which this is not inside, and unscoping it
   would put the treatment on anything that ever borrows the class.

   The margin differs on purpose. .mb-move sits INSIDE the mini-bio card, under
   its own content, so its 16px is internal spacing; this sits between the group
   label and the first row, where the list's own rhythm applies. */
.s2-results .loc-move-all {
  width: 100%; margin: 0 0 10px;
  padding: 12px; border: 0; border-radius: 10px;
  background: var(--jem-green); color: #fff;
  font: inherit; font-size: 14px; font-weight: 800; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.s2-results .loc-move-all:active { background: var(--jem-forest); }
.s2-results .loc-move-all:focus-visible { outline: 2px solid var(--jem-forest); outline-offset: 2px; }

/* ============================================================================
   Per-horse attachments (JGMA-80) - Components/HorseAttachments.razor
   Shared by the confirm sheet and Movement Details. NEW selectors only; nothing
   here restyles a rule that exists elsewhere in this file.

   A file is its NAME, with no file-type icon or thumbnail. The name wraps
   anywhere: phone cameras produce long unbroken names, and one must not push
   the row past a 360px screen.

   Each horse's attachments live in its HorseSpotCard (placed there 23 Sep
   2026): the editor in the open body, a count on the P/Y/B line of a closed
   card, and - for a card that does not open - a read-only list under that line
   with upload_file as each name's bullet.
   ============================================================================ */
.att { margin: 0 0 12px; }
.att + .att { border-top: 1px solid var(--line); padding-top: 10px; }
.att-horse { font-size: 13px; font-weight: 700; color: var(--ink); margin-bottom: 6px; }
/* The editor's heading inside an open card, after the picker. Shaped like the picker's own small
   labels rather than a section label, since it is one part of one card. */
.hd-spot .att { margin: 12px 0 2px; }
.att-title { font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-3); margin-bottom: 6px; }

/* The P/Y/B line with the count at its far end ("space between on the last row"). The label keeps
   its own wrapping; the count never shrinks or wraps. */
.hd-sel-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; }
.hd-sel-row .hd-sel { flex: 1 1 auto; min-width: 0; }
.hd-att {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 2px;
  font-size: 12px; font-weight: 700; color: var(--ink-3); line-height: 1;
}

/* The read-only list under a card that does not open: a bullet, the name, View. Compact — it
   sits inside the card head, where the rows above it are 12px text. */
.att-ro { list-style: none; margin: 6px 0 0; padding: 0; display: grid; gap: 2px; }
.att-ro-file { display: flex; align-items: center; gap: 6px; color: var(--ink-3); }
.att-ro-file .att-fname { font-size: 12px; }
.att-ro-file .att-view { min-height: 32px; padding: 0 4px; }
.att-ro-file .att-noview { font-size: 11px; }

.att-list { list-style: none; margin: 0 0 8px; padding: 0; display: grid; gap: 6px; }
.att-file {
  display: flex; align-items: center; gap: 8px;
  border: 1px solid var(--line-2); border-radius: var(--radius-lg);
  background: var(--card); padding: 8px 10px; min-height: 44px;
}
.att-file.staged { border-style: dashed; }
.att-fname { flex: 1; min-width: 0; font-size: 13px; color: var(--ink-2); overflow-wrap: anywhere; }

/* VIEW, NOT DOWNLOAD (JGMA-93): the client does not want files on devices. A button now, not a
   link, because nothing here navigates — it opens the in-app viewer. */
.att-view, .att-rm {
  display: inline-flex; align-items: center; gap: 4px;
  min-height: 36px; padding: 0 8px; border-radius: var(--radius);
  font: inherit; font-size: 12px; font-weight: 700; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.att-view { border: 0; background: transparent; color: var(--jem-forest); }
/* What a file the app cannot draw says in View's place — a statement, not a control. */
.att-noview { flex: 0 0 auto; font-size: 12px; color: var(--ink-3); padding: 0 4px; }
.att-rm { border: 0; background: transparent; color: var(--ink-3); min-width: 36px; justify-content: center; }
.att-rm:active { color: var(--jem-danger); }
.att-view:focus-visible, .att-rm:focus-visible, .att-btn:focus-within { outline: 2px solid var(--jem-forest); outline-offset: 2px; }

.att-add { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
/* The input sits INSIDE its label and is visually hidden, so tapping the label opens the picker
   on every mobile browser without a script click - which iOS Safari refuses for a camera capture. */
.att-btn {
  position: relative; display: flex; align-items: center; justify-content: center; gap: 6px;
  min-height: 44px; border: 1px solid var(--line-2); border-radius: var(--radius-lg);
  background: #fff; color: var(--ink-2); font-size: 13px; font-weight: 700; cursor: pointer;
}
.att-input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
/* overflow-wrap because the message quotes a file name, and a camera's is one unbroken token that
   otherwise runs out of the box at 360px. */
.att-err { margin-top: 8px; overflow-wrap: anywhere; }

/* ---------------------------------------------------------------------------
   JGMA-93 — the in-app attachment viewer (Components/AttachmentViewer.razor).

   CENTRED OVER THE SCREEN, and FIXED to the viewport in the shell's 480px column
   for the reason .busy-veil and .fail-backdrop are: the shell is a scroll
   container, and an absolutely-positioned overlay inside it is dragged up when a
   focused element scrolls it. z-index 75 — above the delete sheet (70/71), below
   the busy veil (80) and the failure dialog (90).

   CREATED WHEN OPENED, so it needs no hidden state and no visibility transition:
   the fade in is a keyframe, as the failure dialog's is.

   The body SCROLLS a PDF of any length. It is not inside a .sheet, so the
   touch-action trap there does not apply: nothing here declares one, and the
   global `manipulation` lets it pan.
   --------------------------------------------------------------------------- */
.av-backdrop {
  position: fixed; top: 0; bottom: 0; left: 0; right: 0; margin: 0 auto; max-width: 480px;
  z-index: 75; display: grid; place-items: center; padding: 16px;
  background: rgba(15, 23, 42, 0.6);
  animation: fail-fade 160ms ease-out;
}
.av-dialog {
  width: 100%; max-height: calc(100dvh - 32px); display: flex; flex-direction: column;
  border-radius: 16px; background: #fff; overflow: hidden; outline: none;
  animation: fail-pop 160ms ease-out;
}
.av-head {
  flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
  padding: 8px 8px 8px 16px; border-bottom: 1px solid var(--line);
}
.av-name { flex: 1; min-width: 0; font-size: 14px; font-weight: 700; color: var(--ink); overflow-wrap: anywhere; }
.av-close {
  flex: 0 0 auto; width: 40px; height: 40px; border: 0; border-radius: 50%;
  background: transparent; color: var(--ink-3); font-size: 18px; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.av-close:active { background: var(--line); }
.av-close:focus-visible { outline: 2px solid var(--jem-forest); outline-offset: 2px; }
.av-body {
  position: relative; flex: 1 1 auto; min-height: 120px; overflow-y: auto;
  -webkit-overflow-scrolling: touch; background: var(--line);
}
/* The photograph whole, never cropped — this is the one place a groom looks at it properly. No
   long-press "Save to Photos" on iOS; nothing on the web can stop a screenshot. */
.av-img {
  display: block; width: 100%; height: auto; max-height: calc(100dvh - 90px); object-fit: contain;
  -webkit-touch-callout: none; user-select: none;
}
.av-pdf { display: grid; gap: 8px; padding: 8px; }
.av-pdf .av-page { display: block; max-width: 100%; height: auto; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.15); }
.av-msg { padding: 32px 20px; text-align: center; font-size: 14px; color: var(--ink-2); }
.av-spin {
  position: absolute; top: 50%; left: 50%; width: 28px; height: 28px; margin: -14px 0 0 -14px;
  border-radius: 50%; border: 3px solid var(--line-2); border-top-color: var(--ink-3);
  animation: busy-spin 700ms linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .av-backdrop, .av-dialog { animation: none; }
  .av-spin { animation: busy-pulse 1.4s ease-in-out infinite; }
}
