/* ============================================================================
   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);
  /* CLIP, with hidden as the fallback for Safari before 16 (JGMA-95, item 15). A hidden box is still a
     scroll container, and the browser scrolled this one to reveal a focused control or anything
     scrolled into view — moving the New move button and every absolutely positioned child up with it.
     A clipped box is not a scroll container, so nothing can. The WINDOW's share of the same problem,
     after the iPhone keyboard, is wwwroot/js/viewport.js. */
  overflow: hidden;
  overflow: clip;
}

/* ============================================================================
   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;
  /* Hovers over the list: a soft shadow below, which paints over .m-list because the header's
     z-index (2) is above the list's (1) (JGMA-95 feedback). */
  box-shadow: 0 3px 6px rgba(15,23,42,0.3);
  /* 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. */
/* A flex COLUMN so the Regular Movements link can sit at the foot of a short list (margin-top: auto,
   below); its children must not shrink, so a long list scrolls rather than squashing its cards to
   fit. */
.m-list {
  padding: 12px; flex: 1 1 auto; overflow-y: auto; -webkit-overflow-scrolling: touch;
  /* Stated, not left to compute from overflow-y (it would be auto): a card dragged past its row's
     edge (.swipe-row) must be clipped at the screen's edge, never scroll the list sideways. */
  overflow-x: hidden;
  display: flex; flex-direction: column;
  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; }
.m-list > * { flex-shrink: 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 was overflow:hidden and so
   still a scroll container whose scrollHeight exceeds its height — anything positioned against the
   SHELL was dragged out of place when the browser scrolled it to reveal a focused control (the reason
   .busy-veil is fixed rather than absolute; see its block below). The shell is overflow:clip since
   JGMA-95, so that no longer happens where clip is supported; in flow is still the safe place for
   Safari before 16, which falls back to hidden. 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 { display: inline-flex; line-height: 1; }

.list-hint {
  text-align: center; font-size: 11px; color: var(--ink-3); padding: 0 0 12px;
  display: flex; align-items: center; justify-content: center; gap: 8px;
}
.list-hint .arrow { color: var(--jem-green); display: inline-flex; align-items: center; }
/* The foot of the card list (JGMA-95): "pull to refresh", and beneath it the swipe hint's arrow at the
   same size, turned to point down — Material Symbols has no downward arrow in the same style. */
.pull-hint {
  font-size: 11px; color: var(--ink-3); padding: 2px 0 12px;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
}
.pull-hint .arrow { color: var(--jem-green); display: inline-flex; transform: rotate(90deg); }

/* ---------- Swipe row / move card ----------
   The row does NOT clip its card (JGMA-95): a dragged card slides past the row's edge and off the
   screen, rather than disappearing into the row. VISIBLE on both axes — visible on one axis only
   computes to auto when the other is not visible, and the row would still clip. .m-list clips
   sideways instead, at the screen's edge. */
.swipe-row {
  position: relative; border-radius: 14px; margin-bottom: 12px; overflow: visible;
  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; }

/* The INSET BEVEL (JGMA-95 ruling 19): lit from the top left, so the top and left edges are light and
   the bottom and right edges dark, over the card's drop shadow. Two insets each way — a sharp 2px
   edge and a softer 4px falloff — so it reads as a raised face rather than a line. The face is a
   shade off white, because a white highlight on a white card cannot show. The bottom
   padding is 5px, because the card's last line — the route or the horse list — carries its own. */
.move-card {
  position: relative; background: #f6f8fa; border-radius: 14px; padding: 14px 16px 5px;
  box-shadow:
    inset 2px 2px 0 rgba(255,255,255,1),
    inset 4px 4px 4px rgba(255,255,255,0.7),
    inset -2px -2px 0 rgba(15,23,42,0.14),
    inset -4px -4px 4px rgba(15,23,42,0.05),
    0 2px 6px rgba(15,23,42,0.14);
  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; }
/* Flexes so the date keeps its width and a long name ellipsizes instead of shoving it. */
.move-card .horse {
  font-size: 14px; font-weight: 700; line-height: 1.15;
  flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A card of several horses is titled with its route, "Paddock · Yard → Paddock · Yard" (JGMA-95), and
   the yards are what the card groups on — so the title wraps rather than losing one to an ellipsis at
   360px. */
.move-card .horse.group { white-space: normal; }
.move-card .horse .arrow { display: inline-flex; vertical-align: middle; margin: 0 4px 1px; }
/* Every "to" arrow on a card is the green arrow_right_alt, and every one sits 1px up: the glyph's
   centre is a pixel low against the text's line height. Cards only — the panel header and Regular
   Movements draw the same icon without it. */
.move-card .arrow { color: var(--jem-green); margin-bottom: 1px; }
/* A pedigree line — and a grouped card's count, which sits where a single horse's pedigree does. JEM's
   pedigree strings run long, so they are small. */
.move-card .pedigree { margin-top: 2px; font-size: 10px; color: var(--ink-3); line-height: 1.3; }

/* Words for a screen reader only — the "to" beside an arrow drawn as an icon (ruling 19), which
   Icon marks aria-hidden. The usual visually-hidden pattern: in the tree, out of sight. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* Top right, where the move-type badge was (ruling 19). Tabular figures so the dates line up down
   the column. */
.move-card .move-date {
  flex: 0 0 auto; font-size: 11px; font-weight: 400; color: var(--ink-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* Several horses: each as "<name> → <box>", its pedigree beneath (JGMA-95). The box is left out, arrow
   and all, for a horse going to none. */
.move-card .card-horses { list-style: none; margin: 6px 0 0; padding: 0 0 9px; display: grid; gap: 5px; }
.move-card .card-horses .ch-line { display: flex; flex-wrap: wrap; align-items: center; column-gap: 4px; font-size: 13px; }
.move-card .card-horses .ch-name { font-weight: 600; }
.move-card .card-horses .ch-box { display: inline-flex; align-items: center; gap: 4px; color: var(--ink-2); }
.move-card .card-horses .ch-box .arrow { display: inline-flex; }
.move-card .card-horses .pedigree { margin-top: 1px; }

/* ---------- 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.
   One surface: the route row has no background of its own since ruling 19, so the card's tint is
   all there is. Text stays dark ink — the tint is light enough to read on, and recolouring the
   horse name would trade legibility for emphasis it does not need. */
/* The red left edge is the Done card's green one in the danger colour. The bevel is re-lit for the
   tint: a full-white highlight on the soft red stands out far more than it does on the near-white
   face, so the light edges are fainter here and the dark ones lean red. */
.move-card.overdue {
  background: var(--jem-danger-soft);
  border-left: 4px solid var(--jem-danger);
  box-shadow:
    inset 2px 2px 0 rgba(255,255,255,0.45),
    inset 4px 4px 4px rgba(255,255,255,0.25),
    inset -2px -2px 0 rgba(127,29,29,0.14),
    inset -4px -4px 4px rgba(127,29,29,0.05),
    0 2px 6px rgba(15,23,42,0.14);
}
.move-card.overdue .route.more { color: var(--jem-danger); }
.move-card.overdue .move-date { color: var(--jem-danger); }
/* Plain since ruling 19: no background, no left padding, 5px above. */
.move-card .route {
  margin-top: 5px; padding: 0 11px 9px 0;
  font-size: 13px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.move-card .route .ep { font-weight: 600; }
/* The arrow is the arrow_right_alt icon (ruling 19), so it centres as a box. */
.move-card .route .arrow { display: inline-flex; align-items: center; }
/* A card whose destination is missing (Requires Action) — the panel says what is. */
.move-card .route.more { color: var(--ink-3); font-weight: 600; }

/* ---------- Done card ----------
   A .move-card too (JGMA-95, 28/09 feedback): the same content (Components/MoveCardContent.razor),
   with the time it was confirmed where a To-do card has its date. No tick: the green left edge is
   what says "done", as it did before the cards were unified. Requires Action keeps its amber flag
   beside the content, and the amber edge that says it is unfinished. What differs
   is only what a finished card is not: it does not swipe, so it has no grab cursor and no swipe row
   around it, and it keeps its own spacing and entrance. */
.done-card {
  display: flex; align-items: flex-start; gap: 12px; margin-bottom: 12px;
  border-left: 4px solid var(--jem-green);
  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 body has to be allowed to shrink, or a long route pushes the time off the card. */
.done-card .dc-body { flex: 1 1 auto; min-width: 0; }
/* 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); cursor: pointer; }
/* Requires Action (JGMA-70) is the amber card: this one is not finished. It shared the colour with
   the flagged card until JGMA-95 stopped showing flagged movements, so it is now the only card that
   uses it — the screen's palette is still 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; }
/* The title's arrow is the arrow_right_alt icon (ruling 19), sat on the text's middle. */
.detail-modal > header h3 .icon { vertical-align: middle; color: var(--jem-green); margin: 0 3px; }
.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; }
/* Scheduled is amber and Overdue red (JGMA-95): a move still to do, and one that is late. */
.status.scheduled { background: var(--jem-warning-soft); color: var(--jem-warning); }
.status.overdue   { background: var(--jem-danger-soft); color: var(--jem-danger); }
.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); }
/* Plain black and unbolded (JGMA-95 ruling 20) — the label column's grey is what separates them. */
.detail-body .kv .v { color: #000; font-weight: 400; }
/* Booked on: "by <name>" is on its own line (ruling 20), and a long email address breaks anywhere
   rather than pushing the panel wider. */
.detail-body .kv .v.booked { overflow-wrap: anywhere; }
/* Overdue: the DATE only. The label stays muted grey — "Scheduled for" is not the thing that is late
   — and the class sits on a span around the date rather than the whole value. */
.detail-body .kv .v .overdue { color: var(--jem-danger); font-weight: 700; }

/* 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. Where the horses' notes differ, each "Name: note" is a line of its own
   (ruling 20 moved them here when it removed the Moves section). */
.detail-body .kv .v.note { white-space: pre-line; overflow-wrap: anywhere; }
.detail-body .kv .v.note .note-line { display: block; }

/* 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; }
/* Today, beneath the grid (JGMA-95): the neutral white button — .sheet-actions button.cancel's look.
   Not .dp-today, which is Home's header chip. */
.cal-today {
  display: block; width: 100%; margin-top: 12px; padding: 12px; border-radius: 12px;
  background: #fff; color: var(--ink-2); border: 1px solid var(--line);
  font-family: inherit; font-weight: 700; font-size: 14px; cursor: pointer;
}
.cal-today:active { filter: brightness(0.96); }

/* 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; }
/* A pedigree line is 10px wherever it is listed (JGMA-95): JEM's strings run long. */
.res-row .r-dis.ped { font-size: 10px; }
.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); }
/* The Movement note heading on Movement details is spaced from the roster above it as sections are —
   .summary's 20px (JGMA-95). On the HEADING rather than the Add horse button, because a completed
   movement's Edit has the note and no Add horse; the margin collapses with whatever ends the roster,
   so both modes get the same 20px. */
.m-body .sec-head.note-head { margin-top: 20px; }

/* 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 fade so the alert still animates out. */
/* A SLIDE-UP SHEET, not a centred box (JGMA-95, ruling 15): no modal in this app sits in the middle
   of the screen. The backdrop now fades over the panel's 260ms slide rather than its old 160ms pop,
   so its delayed flip to hidden waits out the slide and the panel slides away rather than vanishing. */
.alert-backdrop {
  position: absolute; inset: 0; background: rgba(15,23,42,0.5); z-index: 60; opacity: 0; pointer-events: none;
  display: flex; flex-direction: column; justify-content: flex-end;
  visibility: hidden;
  transition: opacity 260ms, visibility 0s linear 260ms;
}
.alert-backdrop.show {
  opacity: 1; pointer-events: auto; visibility: visible;
  transition: opacity 260ms, visibility 0s;
}
.alert-modal {
  background: #fff; border-radius: 18px 18px 0 0; width: 100%;
  padding: 22px 16px 22px; padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 22px);
  text-align: center;
  transform: translateY(100%); transition: transform 260ms cubic-bezier(.2,.9,.3,1.2);
}
.alert-backdrop.show .alert-modal { transform: translateY(0); }
.alert-modal .a-msg { font-size: 14px; color: var(--ink); line-height: 1.45; font-weight: 500; margin-bottom: 16px; }
/* The sheets' quiet acknowledging button (.fail-modal .f-btn.quiet), not the old full-bleed text
   button, which only made sense along the bottom edge of a floating box. */
.alert-modal .a-ok { width: 100%; border: 1px solid var(--line); border-radius: 12px; background: #fff; color: var(--ink-2); font-weight: 700; font-size: 15px; padding: 13px; cursor: pointer; font-family: inherit; }
.alert-modal .a-ok:active { filter: brightness(0.96); }

/* ============================================================================
   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
   was wrong: the shell was `overflow: hidden`, and an overflow-hidden box is still a
   SCROLL CONTAINER — the browser scrolls it to keep a focused element in view. Since
   JGMA-95 the shell is `overflow: clip`, which is not one; FIXED stays, for Safari
   before 16 (which falls back to hidden) and because it escapes the clipping below. 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 stay exactly where it is — the veil over everything, the failure sheet on
   the bottom edge — 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. */
/* A SLIDE-UP SHEET on the bottom edge, as every other panel in the app is (JGMA-95, ruling
   15) — it was a box in the middle of the screen. The rise is .sheet's own 260ms curve, run
   as a keyframe for the reason above. */
.fail-backdrop {
  z-index: 90;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  background: rgba(15, 23, 42, 0.5);
  animation: fail-fade 160ms ease-out;
}

.fail-modal {
  width: 100%;
  padding: 22px 16px 22px;
  padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 22px);
  border-radius: 18px 18px 0 0;
  background: #fff;
  text-align: center;
  animation: sheet-rise 260ms cubic-bezier(.2,.9,.3,1.2);
}

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

/* The entry of a panel that is CREATED open — the failure sheet, the attachment viewer — as
   .sheet.show's transition is for the panels that sit in the DOM waiting. */
@keyframes sheet-rise {
  from { transform: translateY(100%); }
  to { transform: translateY(0); }
}

@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. pre-line, so a
   detail naming several things — the horses that moved, one per line (JGMA-95) — keeps its
   line breaks. */
.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;
  white-space: pre-line;
}

/* 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
   the legacy screen carried above the list went with JGMA-95 ruling 22. */

/* 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); display: inline-flex; align-items: center; }
.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.
   At the FOOT of the list whatever its length (JGMA-95): margin-top auto takes up a short list's spare
   height in .m-list's column, and a long one leaves none, so it follows the last card. The 50px below
   clears the New move button in both cases. */
.reg-moves-link {
  width: 100%; text-align: left; font: inherit; cursor: pointer;
  display: flex; align-items: center; gap: 8px;
  margin-top: auto; margin-bottom: 50px; 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-dis.ped { font-size: 10px; }
.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; }
/* Movement details' whole move, "origin → destination" (JGMA-95 ruling 21): the origin in the
   quieter ink and sized to itself, so the destination keeps the rest of the row. */
.hd-sel-row .hd-sel.from { flex: 0 1 auto; color: var(--ink-2); }
.hd-sel-row .hd-arrow { flex: 0 0 auto; align-self: center; display: inline-flex; color: var(--jem-green); }
.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).

   A SLIDE-UP SHEET on the bottom edge (JGMA-95, ruling 15 — it was centred over the
   screen), 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 and the rise are keyframes, as the failure sheet's are.

   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: flex; flex-direction: column; justify-content: flex-end; padding-top: 16px;
  background: rgba(15, 23, 42, 0.6);
  animation: fail-fade 160ms ease-out;
}
.av-dialog {
  width: 100%; max-height: calc(100dvh - 16px); display: flex; flex-direction: column;
  border-radius: 18px 18px 0 0; background: #fff; overflow: hidden; outline: none;
  padding-bottom: env(safe-area-inset-bottom, 0px);
  animation: sheet-rise 260ms cubic-bezier(.2,.9,.3,1.2);
}
.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;
}
/* A video (JGMA-95) sits as the photograph does: whole, black behind it, and no long-press menu. */
.av-video {
  display: block; width: 100%; height: auto; max-height: calc(100dvh - 90px); background: #000;
  -webkit-touch-callout: 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; }
}
