/* =========================================================================
   Phone frame and app screens for the App_Carousel marketing section.

   Lifted verbatim from prototypes/n3-app-what-it-will-do/scl3v2.css (the
   Switchgrass sales demo) so the two builds cannot drift. Only the demo-only
   chrome was dropped: the picker rows, the full-screen stage grid, the sales
   caption panel and its landscape-iPad media queries. Everything from the
   device frame inwards is unchanged, including the two extra tokens the heat
   prototype declares inline.

   Section shell, tiles and caption live in app-carousel.css instead.
   ========================================================================= */

/* Two tokens the heat prototype declares inline and tokens.css does not carry.
   Values copied from heat-detection.html, not invented: the real "In heat" and
   warning badges are SOLID fills with white text, not soft tints. */
:root {
  --nf-heat-badge: #F55380;
  --nf-badge-warning-bg: #F78F08;
  /* the heat prototype's own chart and chip tokens, copied not invented */
  --nf-heat-tint: #FDE7EE;
  --nf-heat-tint-border: #F8A6BE;
  --nf-chart-rumination: #009E73;
  --nf-chart-activity: #0072B2;
  --nf-bg-blue-soft: #F0F9FF;
  --nf-border-blue: #B9E6FE;
  --nf-text-blue: #026AA2;
  --nf-border-warning: #FEDF89;
}

/* --------------------------------------------------------------- the phone

   The real device frame from the prototypes, to the pixel: 390x844, 44px
   radius, and the bezel drawn as two spread shadows rather than a border so it
   does not eat into the 390. */

.phone {
  position: relative;
  width: 390px;
  height: 844px;
  flex: none;
  overflow: hidden;
  border-radius: 44px;
  background: var(--nf-bg-primary);
  box-shadow:
    0 30px 60px rgba(0, 0, 0, 0.2),
    0 0 0 10px var(--nf-phone-bezel-inner),
    0 0 0 11px var(--nf-phone-bezel-outer);
  transform-origin: center center;
  isolation: isolate;
}

.phone-screen {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--nf-bg-primary);
  color: var(--nf-text-primary);
  /* the app's own type stack, not the brand face: this is product UI */
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  display: flex;
  flex-direction: column;
}

/* Dynamic island and home indicator, over everything. */
.notch {
  position: absolute;
  top: 11px;
  left: 50%;
  transform: translateX(-50%);
  width: 120px;
  height: 32px;
  border-radius: 20px;
  background: var(--nf-phone-bezel-inner);
  z-index: 60;
  pointer-events: none;
}

.home-indicator {
  position: absolute;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  width: 134px;
  height: 5px;
  border-radius: 100px;
  background: var(--nf-text-primary);
  z-index: 60;
  pointer-events: none;
}

.home-indicator.on-dark { background: var(--nf-text-white); opacity: 0.75; }

/* A beat cuts to its screen. No cross-fade: v1 taught us that a transition
   between two states of the same screen reads as a glitch, not a move. */
/* ------------------------------------------------------------------ motion

   One vocabulary for every move in the build, so the transitions agree with each
   other rather than each being timed by hand. Two eases: `--m-out` for anything
   arriving, `--m-sheet` for anything sliding from an edge, which is the curve
   iOS uses on its own sheets. Four durations, and camera work is the only thing
   allowed to be slow.

   Every animation here starts on its own: a screen is display:none until its
   beat, so it cannot run early, and it restarts on each arrival without any
   JavaScript telling it to. Two beats on the same screen leave it alone, which
   is why a scroll beat does not replay the entrance (James, 2026-08-05). */
:root {
  --m-out: cubic-bezier(0.22, 0.61, 0.36, 1);
  --m-sheet: cubic-bezier(0.32, 0.72, 0, 1);
  --m-leave: 0.12s;
  --m-fast: 0.2s;
  --m-base: 0.34s;
  --m-slow: 0.5s;
  --m-camera: 1.9s;
}

/* Absolute and stacked, not a flex child taking turns, so the outgoing and
   incoming screen can sit in the same rect at once: that is what a crossfade
   needs. is-on sits above is-leaving so the arriving screen visibly covers
   the departing one rather than the departing one covering the arrival. */
.screen { display: none; position: absolute; inset: 0; flex-direction: column; }
.screen.is-on { display: flex; z-index: 2; animation: screen-in var(--m-fast) ease both; }
.screen.is-leaving { display: flex; z-index: 1; animation: screen-out var(--m-leave) ease both; }

/* A beat can continue the same screen a previous beat already showed (the
   rotation runs three beats on rg-move alone) — skip the entrance animation
   so the phone doesn't replay a screen cut nothing actually left. */
.screen.is-cont.is-on { animation: none; }

/* opacity only. A screen that also moves fights the animations inside it, and
   the phone is already under a scale transform from fitPhone. */
@keyframes screen-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes screen-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* A map with no camera flight of its own still settles rather than cutting: the
   imagery eases down the last few per cent as the beat opens. */
.screen.is-on .map--settle .map-img {
  animation: map-settle var(--m-slow) var(--m-out) both;
}

@keyframes map-settle {
  from { opacity: 0; transform: scale(1.05); }
  to   { opacity: 1; transform: scale(1); }
}

/* Rows arrive in sequence rather than all at once, which is both what a real
   list does as it loads and what makes the eye read down it. */
.screen.is-on .list > .card,
.screen.is-on .list > .ncard,
.screen.is-on .ev-row {
  animation: row-in var(--m-base) var(--m-out) both;
}

.screen.is-on .list > :nth-child(1),
.screen.is-on .ev-row:nth-child(1) { animation-delay: 0.06s; }
.screen.is-on .list > :nth-child(2),
.screen.is-on .ev-row:nth-child(2) { animation-delay: 0.12s; }
.screen.is-on .list > :nth-child(3),
.screen.is-on .ev-row:nth-child(3) { animation-delay: 0.18s; }
.screen.is-on .list > :nth-child(4),
.screen.is-on .ev-row:nth-child(4) { animation-delay: 0.24s; }
.screen.is-on .list > :nth-child(5),
.screen.is-on .ev-row:nth-child(5) { animation-delay: 0.3s; }
.screen.is-on .list > :nth-child(6),
.screen.is-on .ev-row:nth-child(6) { animation-delay: 0.36s; }
.screen.is-on .list > :nth-child(7),
.screen.is-on .ev-row:nth-child(7) { animation-delay: 0.42s; }
.screen.is-on .list > :nth-child(8),
.screen.is-on .ev-row:nth-child(8) { animation-delay: 0.48s; }
.screen.is-on .list > :nth-child(n+9),
.screen.is-on .ev-row:nth-child(n+9) { animation-delay: 0.54s; }

@keyframes row-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .screen.is-on,
  .screen.is-leaving,
  .screen.is-on .map--settle .map-img,
  .screen.is-on .list > .card,
  .screen.is-on .list > .ncard,
  .screen.is-on .ev-row { animation: none; }
}

/* ------------------------------------------------------------- status bar

   47px of safe area, 15/600, and the real signal, wifi and battery glyphs.
   Turns white over a map, exactly as the prototype's .statusbar.over-map does. */

.sb {
  flex: none;
  height: 47px;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  padding: 0 30px 6px;
  font-size: 15px;
  font-weight: 600;
  color: var(--nf-text-primary);
  z-index: 50;
}

.sb-right { display: flex; align-items: center; gap: 6px; }
.sb-right svg { display: block; fill: currentColor; }

.sb.on-dark, .sb.on-dark svg { color: var(--nf-text-white); }

/* ----------------------------------------------------------- screen header */

.hdr {
  flex: none;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 16px 10px;
}

.hdr-back { margin-left: -8px; }

.hdr-titles { min-width: 0; flex: 1; }

/* 28/700 with -0.02em, the app's screen title */
.hdr-title {
  font-size: 28px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.15;
  color: var(--nf-text-primary);
}

/* the detail variant: smaller title with a subtitle under it */
.hdr--detail .hdr-title { font-size: 20px; letter-spacing: -0.01em; }

.hdr-title .tag-no {
  font-weight: 400;
  font-size: 13px;
  color: var(--nf-text-tertiary);
}

.hdr-sub {
  margin-top: 2px;
  font-size: 13px;
  color: var(--nf-text-tertiary);
}

.hdr-actions { display: flex; align-items: center; gap: 6px; flex: none; }

/* 40x40, radius 8 */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: var(--nf-radius-md);
  border: 1px solid var(--nf-border-secondary);
  background: var(--nf-bg-primary);
  color: var(--nf-text-primary);
}

.icon-btn--plain { border-color: transparent; background: transparent; }

.icon-btn svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
}

/* --------------------------------------------------------- secondary nav

   Rounded rectangles, not pills: padding 8x16, radius-md, 15/500. Active is
   brand-soft with brand text, which is how the real Reproduction tab reads. */

.snav {
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 2px 16px 12px;
  /* the prototype's own view-tabs scroll horizontally rather than wrap, and
     the player scrolls the active tab into view the way the app does */
  overflow-x: auto;
  scrollbar-width: none;
}

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

.snav-tab {
  padding: 8px 16px;
  flex: none;
  border: 1px solid var(--nf-border-secondary);
  border-radius: var(--nf-radius-md);
  background: var(--nf-bg-primary);
  font-size: 15px;
  font-weight: 500;
  color: var(--nf-text-primary);
  white-space: nowrap;
}

.snav-tab.is-on {
  background: var(--nf-bg-brand-soft);
  border-color: var(--nf-bg-brand-soft);
  color: var(--nf-text-brand-link);
  font-weight: 600;
}

/* --------------------------------------------------------------- the body */

.body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.body--pad { padding: 0 16px var(--nf-safe-bottom, 34px); gap: 12px; }

/* A page that scrolls, for the board's "top of the page / scroll / bottom of
   the page" telling on the heat detail. The walkthrough drives the scrolling;
   the scrollbar is hidden the way the prototypes hide theirs. */
.body--scroll {
  overflow-y: auto;
  scrollbar-width: none;
}

.body--scroll::-webkit-scrollbar { display: none; }

.body--scroll .chart { flex: none; }
.body--scroll .chart-plot > svg { height: 260px; }


/* ------------------------------------------------------------ group header

   bg-section, 18/500, count badge on the right. */

.group {
  flex: none;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 16px;
  background: var(--nf-bg-section);
  font-size: 18px;
  font-weight: 500;
  color: var(--nf-text-strong);
}

.group em {
  font-style: normal;
  font-size: 13px;
  font-weight: 400;
  color: var(--nf-text-tertiary);
}

.group-count {
  margin-left: auto;
  padding: 2px 8px;
  border-radius: 6px;
  background: var(--nf-bg-secondary);
  font-size: 14px;
  font-weight: 500;
  text-align: center;
  color: var(--nf-text-strong);
}

/* the accordion's 12px status dot, brand-filled when the group is live */
.group-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  flex: none;
  background: var(--nf-bg-brand);
}

.group-dot--empty { background: transparent; border: 1.5px solid var(--nf-text-tertiary); }

.group-chev {
  width: 20px;
  height: 20px;
  flex: none;
  fill: none;
  stroke: var(--nf-text-tertiary);
  stroke-width: 1.5;
  transform: rotate(-90deg);  /* ic-chevron points right; an expanded group points up */
}

.group svg {
  width: 20px;
  height: 20px;
  flex: none;
  fill: none;
  stroke: var(--nf-text-tertiary);
  stroke-width: 1.5;
}

/* ------------------------------------------------------------ the list row

   The real .collar-card: 48px avatar, 1fr of identity, and a right-hand column.
   Kept at three rows per screen instead of nine. */

.list { flex: none; display: flex; flex-direction: column; }

.card {
  display: grid;
  grid-template-columns: 56px 1fr auto;
  align-items: center;
  gap: 14px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--nf-border-secondary);
}

.card:last-child { border-bottom: none; }

.card-av {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--nf-bg-section);
  color: var(--nf-text-primary);
  overflow: hidden;
}

.card-av svg { width: 38px; height: 38px; fill: none; stroke: currentColor; }

/* the cow head is drawn on a 44x43 grid facing left, so in the avatar it needs
   the same centring and flip as it does on the map */
.card-av--beast svg { width: 34px; height: 34px; transform: scaleX(-1); }
.card-av img { width: 100%; height: 100%; object-fit: cover; }

.card-name {
  font-size: 26px;
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--nf-text-primary);
}

.card-meta {
  display: flex;
  align-items: center;
  gap: 5px;
  margin-top: 3px;
  font-size: 13px;
  color: var(--nf-text-tertiary);
}

.card-meta b { font-weight: 600; color: var(--nf-text-secondary); }

.card-meta svg { width: 15px; height: 15px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.5; }

.card-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex: none; }

/* the boxed number on the right of a real row: 20/700 over an 11px caption */
.stat-box {
  min-width: 62px;
  padding: 7px 10px;
  border-radius: var(--nf-radius-sm);
  background: var(--nf-bg-section);
  text-align: center;
}

.stat-box b {
  display: block;
  font-size: 20px;
  font-weight: 700;
  line-height: 1.05;
  color: var(--nf-text-primary);
}

.stat-box span { font-size: 11px; color: var(--nf-text-tertiary); }

/* Ghost and alert card states, for the activity list: the herd that is fine
   recedes to placeholder grey, and the one animal that is not sits at full
   contrast on the warning-subtle wash. The point of the screen is the
   difference between the two. */
.card--ghost .card-name,
.card--ghost .card-name span,
.card--ghost .card-meta,
.card--ghost .card-meta b { color: var(--nf-text-placeholder); }
.card--ghost .card-av { background: var(--nf-bg-card); color: var(--nf-text-placeholder); }
/* The flagged card is a plain card: the badge carries the alert on its own.
   A full amber wash over the row read as "everything is orange" rather than
   "one thing is flagged" (James, 2026-08-05). */

/* Group headers on the activity list carry the same ghost treatment as the
   cards: the copy recedes to placeholder, the status dots keep their colour,
   so the alert card is the only type on the screen at full weight. */
.group--ghost { color: var(--nf-text-placeholder); }


.group--ghost em { color: var(--nf-text-placeholder); }
.group--ghost .group-count { background: var(--nf-bg-section); color: var(--nf-text-placeholder); }
.group--ghost .group-chev { stroke: var(--nf-text-placeholder); }

/* the twin's indicator-module: 13/600, the percentage in primary and the
   battery glyph 22x14 in success green (warning when low) */
.batt {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  font-weight: 600;
  color: var(--nf-text-tertiary);
}

.batt span { color: var(--nf-text-primary); }
.batt svg { width: 22px; height: 14px; fill: none; stroke: var(--nf-bg-success-solid); }

.card--ghost .batt,
.card--ghost .batt span { color: var(--nf-text-placeholder); }
.card--ghost .batt svg { stroke: var(--nf-text-placeholder); }

/* --------------------------------------------------------------- badges */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 12px;
  border-radius: 999px;
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
  border: 1px solid transparent;
}

.badge svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  vector-effect: non-scaling-stroke;
}

/* the sprites disagree on stroke width, so it is forced on the glyph's own paths */
.badge svg * { stroke-width: 1.7; fill: none; }

/* Per the twin library's animal-card (the prototype pack, which arbitrates
   where the prototypes disagree): in-heat is the soft heat pill with its
   border, warnings are outline pills on white. */
.badge--heat { background: var(--nf-bg-heat-soft); color: var(--nf-text-heat); border-color: var(--nf-border-heat); }
.badge--warn { background: var(--nf-bg-warning-soft); color: var(--nf-text-warning); border-color: var(--nf-bg-warning-soft); }

/* The one flagged badge on a ghosted list arrives rather than sitting there: a
   short pulse on the warning ring, twice, then still. It draws the eye on the
   cut and then stops, because a demo that keeps blinking is a demo nobody can
   talk over (James, 2026-08-05). */
.screen.is-on .badge--warn {
  animation: badge-attn 1.1s 0.45s var(--m-out) 2 both;
}

@keyframes badge-attn {
  0%   { box-shadow: 0 0 0 0 var(--nf-bg-warning-solid); }
  35%  { box-shadow: 0 0 0 7px rgba(247, 144, 9, 0); }
  100% { box-shadow: 0 0 0 0 rgba(247, 144, 9, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .screen.is-on .badge--warn { animation: none; }
}
.badge--ok { background: var(--nf-bg-brand-soft); color: var(--nf-text-brand-primary); }
.badge--info { background: var(--nf-bg-info-soft); color: var(--nf-text-info); }

/* ----------------------------------------------------------------- cards

   Two per screen, never six. bg-section, radius-sm, 20/700 number. */

.stats {
  flex: none;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.stat {
  padding: 16px;
  border-radius: var(--nf-radius-lg);
  border: 1px solid var(--nf-border-secondary);
  background: var(--nf-bg-primary);
}

/* the grazing report's own tile (sgd-stat in the spine): grey, radius 6, no
   border. Used on the paddock detail, which is that report's screen. */
.stats--soft .stat {
  border: 0;
  border-radius: 6px;
  background: var(--nf-bg-section);
}

.stat-l {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  font-weight: 500;
  color: var(--nf-text-tertiary);
}

.stat-l svg { width: 16px; height: 16px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.5; }

.stat-v {
  margin-top: 8px;
  font-size: 28px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--nf-text-primary);
}

.stat-v small { margin-left: 5px; font-size: 13px; font-weight: 500; color: var(--nf-text-tertiary); }

.stat--accent { background: var(--nf-bg-warning-subtle); border-color: var(--nf-bg-warning-soft); }
.stat--accent .stat-l, .stat--accent .stat-v { color: var(--nf-text-warning); }
.stat--accent .stat-v small { color: var(--nf-text-warning-strong); }

/* ---------------------------------------------------------------- the map

   Real satellite imagery, copied out of the prototypes into img/maps/ so it
   still works with no connection, with the pasture drawn over it off the map
   overlay tokens exactly as the app does. */

.map {
  position: relative;
  flex: 1;
  min-height: 0;
  overflow: hidden;
  background: var(--nf-bg-section);
}

.map--boxed { flex: none; height: 300px; border-radius: var(--nf-radius-md); }

.body--under { position: relative; }

.body--under .map--full {
  position: absolute;
  inset: 0;
  z-index: 0;
}

.body--under .sheet { position: relative; }

.map-img {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
}

.map-img--status { background-image: url('img/maps/map-status.jpg'); }
.map-img--detail { background-image: url('img/maps/map-collar-detail.jpg'); }
.map-img--edit { background-image: url('img/maps/map-collar-aerial-field.jpg'); }
.map-img--field { background-image: url('img/maps/map-collar-aerial-field.jpg'); }

.map svg { position: absolute; inset: 0; width: 100%; height: 100%; }

/* Pasture geometry, off the tokens the app uses for exactly this. */
.pad {
  fill: var(--nf-map-overlay-fill);
  fill-opacity: var(--nf-map-overlay-fill-opacity);
  stroke: var(--nf-map-overlay-stroke);
  stroke-opacity: var(--nf-map-overlay-stroke-opacity);
  stroke-width: 2;
}

.pad--live {
  fill: var(--nf-map-overlay-fill-active);
  fill-opacity: var(--nf-map-overlay-fill-active-opacity);
  stroke: var(--nf-map-overlay-stroke-active);
}

.pad--rest { fill: var(--nf-bg-dark); fill-opacity: 0.34; stroke: var(--nf-bg-dark); stroke-opacity: 0.5; }
.pad--none { fill: none; stroke: var(--nf-pasture-boundary); stroke-opacity: 0.9; }

/* An inactive pasture is drawn dashed and the live one solid, so which ground is
   in use reads before any label does (James, 2026-08-05). */
.pad--idle {
  fill: var(--nf-map-overlay-fill);
  fill-opacity: 0.1;
  stroke: var(--nf-pasture-boundary);
  stroke-opacity: 0.85;
  stroke-width: 2;
  stroke-dasharray: 7 5;
}

/* the moving pasture in the rotational-grazing hop animation: its boundary is
   the white pasture edge, same as the live square carries elsewhere, a touch
   thicker so the fence reads while it is moving */
.pad-band {
  stroke: var(--nf-text-white);
  stroke-opacity: 1;
  stroke-width: 3.5;
}

/* the hop's direction marks: hidden until their hop draws them, SMIL owns all
   the timing, this is only what they look like */
.rot-arc {
  fill: none;
  stroke: var(--nf-text-white);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-dasharray: 240;
  stroke-dashoffset: 240;
  opacity: 0.92;
  filter: drop-shadow(0 1px 2px rgba(10, 13, 18, 0.55));
}
.rot-head {
  fill: none;
  stroke: var(--nf-text-white);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0;
  filter: drop-shadow(0 1px 2px rgba(10, 13, 18, 0.55));
}

/* an exclusion zone: ground inside the pasture the animals are kept off.
   Striped, not filled, so it stays legible over the pasture fill. A zone on
   live fenced ground carries the same solid white line as the fence around
   it; the dashed orange is for zones on inactive ground. */
.pad--excl {
  fill: url(#pat-exclusion);
  stroke: var(--nf-pasture-exclusion);
  stroke-width: 2;
  stroke-dasharray: 5 4;
}
.pad--excl.in-active {
  stroke: var(--nf-text-white);
  stroke-width: 2.5;
  stroke-dasharray: none;
}

/* The grazing heat map is James's own alpha PNG (Heat Map Alpha_00000.png,
   3840px square, downsampled to 900 and palette-quantised to 159KB), laid over
   the satellite imagery and clipped to the pasture. The drawn blobs were a
   stand-in for exactly this, so the stand-in is gone. */
.map-heat { opacity: 0.88; }

/* the wash fades up when its beat opens, so the reveal is the beat's content
   rather than something already on screen */
.screen.is-on .map-heat--in {
  animation: heat-in 0.8s 0.15s ease both;
}

@keyframes heat-in {
  from { opacity: 0; }
  to   { opacity: 0.88; }
}

@media (prefers-reduced-motion: reduce) {
  .screen.is-on .map-heat--in { animation: none; opacity: 0.88; }
}

/* The pasture frame, from the twin library's pasture-polygon: a soft
   map-overlay fill under a 2px dashed map-overlay stroke. The heat map is
   clipped inside it, so the wash reads as belonging to this field rather than
   washing across the whole screen (James, 2026-08-05). */
.pasture-fill {
  fill: var(--nf-map-overlay-fill);
  fill-opacity: 0.16;
}

.pasture-stroke {
  fill: none;
  stroke: var(--nf-map-overlay-stroke);
  stroke-width: 2;
  stroke-dasharray: 6 4;
  opacity: var(--nf-map-overlay-stroke-opacity);
}

/* the boundary sits over the wash, so it stays legible where the map runs hot */
.pasture-edge {
  fill: none;
  stroke: var(--nf-pasture-boundary);
  stroke-width: 2.5;
  stroke-opacity: 0.95;
}

.pad-line { fill: none; stroke: var(--nf-pasture-boundary); stroke-width: 2; stroke-opacity: 0.92; }

.pad-t {
  font: 600 13px -apple-system, sans-serif;
  fill: var(--nf-text-white);
  paint-order: stroke;
  stroke: rgba(0, 0, 0, 0.35);
  stroke-width: 3px;
  stroke-linejoin: round;
}

/* no fill here: each digit sets its own, since a CSS fill would outrank the
   element's own fill attribute and force every number to one colour */
.pad-n { font: 700 11px -apple-system, sans-serif; }

/* Sits above .map-tilt, itself untilted: markers are flat icons floating over
   the 3D ground, not decals printed on it, so they stay upright and facing the
   reader while the terrain underneath tilts away. */
.map-markers { position: absolute; inset: 0; z-index: 3; }

/* the base-station pin: anchors at its point, not its centre, like a map pin */
.pin-marker {
  position: absolute;
  transform: translate(-50%, -100%);
  filter: drop-shadow(0 2px 3px rgba(10, 13, 18, 0.35));
}
.pin-marker svg { display: block; width: 28px; height: 28px; }

/* A small ambient wander, so the herd reads as out there grazing rather than
   pinned in place. Three paths so neighbouring collars don't drift in lockstep,
   staggered delays so the cluster doesn't breathe as one. Runs only while the
   screen is actually on screen. */
@keyframes collar-wander-a {
  0%, 100% { transform: translate(-50%, -50%) translate(0, 0); }
  25%      { transform: translate(-50%, -50%) translate(3px, -2px); }
  50%      { transform: translate(-50%, -50%) translate(-1px, 2px); }
  75%      { transform: translate(-50%, -50%) translate(-3px, -1px); }
}
@keyframes collar-wander-b {
  0%, 100% { transform: translate(-50%, -50%) translate(0, 0); }
  30%      { transform: translate(-50%, -50%) translate(-3px, 2px); }
  60%      { transform: translate(-50%, -50%) translate(2px, 3px); }
  85%      { transform: translate(-50%, -50%) translate(2px, -2px); }
}
@keyframes collar-wander-c {
  0%, 100% { transform: translate(-50%, -50%) translate(0, 0); }
  40%      { transform: translate(-50%, -50%) translate(2px, 2px); }
  70%      { transform: translate(-50%, -50%) translate(-3px, -2px); }
}

.screen.is-on .map-markers .collar-marker:nth-of-type(3n+1) { animation: collar-wander-a 7s ease-in-out infinite; }
.screen.is-on .map-markers .collar-marker:nth-of-type(3n+2) { animation: collar-wander-b 8.5s ease-in-out infinite; animation-delay: -2.5s; }
.screen.is-on .map-markers .collar-marker:nth-of-type(3n)   { animation: collar-wander-c 6.5s ease-in-out infinite; animation-delay: -4s; }

@media (prefers-reduced-motion: reduce) {
  .screen.is-on .map-markers .collar-marker { animation: none; }
}

/* The collar marker, copied from the satellite prototype (.collar-marker in
   its css/proto2.css): a 32px white disc with a 2px white ring, drop shadow,
   and the collar glyph inside. Positioned in percent over the map, exactly as
   that build positions its own. The selected variant is its 40px brand-ring
   pin. */
.collar-marker {
  position: absolute;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--nf-bg-primary);
  border: 2px solid var(--nf-text-white);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--nf-text-primary);
  transform: translate(-50%, -50%);
  z-index: 4;
}

/* position:static, not the inherited absolute/inset:0 from the generic ".map
   svg" rule two rules up: that rule is for the full-bleed paddock overlay, and
   pins any svg it catches to the marker's top-left corner instead of letting
   the flex centering on .collar-marker place it. */
.collar-marker svg { position: static; width: 22px; height: 22px; fill: none; stroke: currentColor; }

/* Cow marker: the real Badge/Livestock asset, circle and icon baked into one
   file, so there is no separate glyph to centre inside a disc. */
.beast-badge {
  position: absolute;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
  transform: translate(-50%, -50%);
  z-index: 4;
  overflow: hidden;
}

.beast-badge svg { position: static; display: block; width: 100%; height: 100%; }

/* the head count, on the marker rather than beside it */
.marker-badge { fill: var(--nf-bg-brand); stroke: var(--nf-text-white); stroke-width: 1.5; }
.marker-count {
  font: 700 11px -apple-system, sans-serif;
  fill: var(--nf-text-white);
  text-anchor: middle;
}

/* the same collar marker drawn inside an SVG map, for the paddock screens
   where the geometry is in viewBox units */
.marker-disc { fill: var(--nf-bg-primary); stroke: var(--nf-text-white); stroke-width: 2; }
.marker-glyph { fill: currentColor; stroke: none; color: var(--nf-text-primary); }

.ic--solid,
.tab svg.ic--solid,
.card-av svg.ic--solid,
.collar-marker svg.ic--solid,
.beast-marker svg.ic--solid,
.push-mark svg.ic--solid,
.row-ic svg.ic--solid { fill: currentColor; stroke: none; }

.collar-marker--selected {
  width: 40px;
  height: 40px;
  background: var(--nf-bg-brand);
  border-color: var(--nf-text-white);
  color: var(--nf-text-white);
  z-index: 5;
}

.collar-marker--selected svg { width: 26px; height: 26px; }

/* The cow head faces left in its own grid, so it is flipped here. Centred
   against the disc rather than by flex: flex was leaving it 3.7px off, and a
   hand-tuned nudge only ever hid part of that. */
.beast-marker svg {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 20px;
  height: 20px;
  transform: translate(-50%, -50%) scaleX(-1);
}

.collar-marker--selected.beast-marker svg { width: 24px; height: 24px; }

/* The zoom-in, ported from the satellite build: the map flies toward the centre
   and the focused collar expands with its ripple, so arriving on the animal is a
   move rather than a cut. The two ripple rings are that build's own
   collarMarkerRipple, 2.4s on the same cubic-bezier with a 1.2s offset. */
.map-zoom { position: absolute; inset: 0; transform-origin: 50% 50%; }

.screen.is-on .map-zoom {
  animation: map-fly var(--m-camera) var(--m-out) both;
}

/* A separate inner layer for the tilted-3D grazing map: kept off .map-zoom
   itself so this static perspective doesn't fight the scale keyframes above,
   which already own that element's transform. */
.map-tilt {
  position: absolute;
  inset: 0;
  perspective: 700px;
  perspective-origin: 50% 100%;
}

.map-tilt .map-img,
.map-tilt svg {
  transform: rotateX(32deg) scale(1.85) translateY(4%);
  transform-origin: 62% 100%;
}

/* step 3 is step 2 zoomed: same geometry, flown in on the paddock being grazed,
   so the two beats read as one camera move (James, 2026-08-05) */
.screen.is-on .map-zoom--paddock {
  animation: map-fly-paddock var(--m-camera) var(--m-out) both;
}

@keyframes map-fly-paddock {
  from { transform: scale(1); }
  to   { transform: scale(1.85); }
}

.map-zoom--paddock { transform-origin: 33% 63%; }

@media (prefers-reduced-motion: reduce) {
  .screen.is-on .map-zoom--paddock { animation: none; transform: scale(1.6); }
}

@keyframes map-fly {
  from { transform: scale(1); }
  to   { transform: scale(1.45); }
}

.collar-marker--live::before,
.collar-marker--live::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid var(--nf-bg-brand);
  opacity: 0;
  pointer-events: none;
}

.screen.is-on .collar-marker--live::before,
.screen.is-on .collar-marker--live::after {
  animation: collar-ripple 2.4s var(--m-out) infinite;
}

.screen.is-on .collar-marker--live::after { animation-delay: 1.2s; }

@keyframes collar-ripple {
  0%   { transform: scale(1); opacity: 0.55; }
  100% { transform: scale(2.75); opacity: 0; }
}

/* and the marker itself arrives, rather than being there all along */
.screen.is-on .collar-marker--selected {
  animation: collar-pop 0.5s 1.05s cubic-bezier(0.34, 1.4, 0.5, 1) both;
}

@keyframes collar-pop {
  from { transform: translate(-50%, -50%) scale(0.4); opacity: 0; }
  to   { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .screen.is-on .map-zoom,
  .screen.is-on .collar-marker--live::before,
  .screen.is-on .collar-marker--live::after,
  .screen.is-on .collar-marker--selected { animation: none; }
  .screen.is-on .map-zoom { transform: scale(1.2); }
}

/* The same collar marker drawn inside an SVG map, for the paddock screens where
   the geometry is in viewBox units: percentage-positioned HTML cannot align to a
   `meet`-fitted viewBox, so those markers live in the drawing. */
.marker-ring { fill: none; stroke: var(--nf-bg-dark); stroke-width: 1; opacity: 0.25; }

/* a floating control over the map, like the app's layer and locate buttons */
.map-ctrls {
  position: absolute;
  top: 12px;
  right: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 5;
}

.map-ctrl {
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--nf-radius-md);
  background: var(--nf-bg-primary);
  box-shadow: var(--nf-shadow-sm);
  color: var(--nf-text-primary);
}

.map-ctrl svg { width: 22px; height: 22px; fill: none; stroke: currentColor; }

/* ------------------------------------------------------ notification card

   The twin library's ncard: a 4px status bar in the left gutter, a 40px icon
   circle, 15/600 title over a 13px subtitle. The heat variant extends the
   twin's set with the heat text token, flagged rather than invented: heat is
   not one of the twin's four bars yet. */

.ncard {
  display: grid;
  grid-template-columns: 4px 52px 1fr auto;
  gap: 0 14px;
  padding: 20px 16px 20px 0;
  background: var(--nf-bg-primary);
  border-bottom: 1px solid var(--nf-border-secondary);
  align-items: center;
  min-height: 96px;
}

.ncard .nbar {
  width: 4px;
  align-self: stretch;
  border-radius: 0 2px 2px 0;
  background: var(--nf-text-tertiary);
}

.ncard--warning .nbar { background: var(--nf-bg-warning-solid); }
.ncard--heat .nbar { background: var(--nf-text-heat); }

.ncard .nic {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--nf-bg-section);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--nf-text-tertiary);
}

.ncard--warning .nic { color: var(--nf-bg-warning-solid); }
.ncard--heat .nic { color: var(--nf-text-heat); }
.ncard .nic svg { width: 24px; height: 24px; fill: none; stroke: currentColor; }
.ncard .nic svg * { stroke-width: 1.6; fill: none; }

.ncard .nt { font-size: 17px; font-weight: 600; color: var(--nf-text-primary); line-height: 1.25; }
.ncard .ns { margin-top: 3px; font-size: 13px; color: var(--nf-text-secondary); line-height: 1.3; }

.ncard .nwhen {
  font-size: 12px;
  color: var(--nf-text-tertiary);
  text-align: right;
  white-space: nowrap;
}

/* ------------------------------------------------------------- the banner

   The satellite proto2's status banner (.conn-status in its css/proto2.css):
   40px icon circle on bg-primary, 16/700 title, 13px secondary sub, on a soft
   wash at radius-lg. Variants swap the wash and icon colour. */

.banner {
  flex: none;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  border-radius: var(--nf-radius-lg);
  background: var(--nf-bg-brand-subtle);
  border: 1px solid var(--nf-border-brand-subtle);
}

.banner--warn {
  background: var(--nf-bg-warning-subtle);
  border-color: var(--nf-bg-warning-soft);
}

.banner--warn .banner-t { color: var(--nf-text-warning); }
.banner--warn .banner-s { color: var(--nf-text-warning-strong); }
.banner--heat { background: var(--nf-bg-heat-soft); border-color: var(--nf-bg-heat-soft); }

.banner-ic {
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--nf-bg-primary);
  display: flex;
  align-items: center;
  justify-content: center;
}

.banner-ic svg { width: 22px; height: 22px; fill: none; stroke: var(--nf-text-brand-primary); }
.banner--warn .banner-ic svg { stroke: var(--nf-text-warning); }
.banner--heat .banner-ic svg { stroke: var(--nf-text-heat); }

.banner-t { font-size: 16px; font-weight: 700; color: var(--nf-text-primary); }
.banner-s { font-size: 13px; color: var(--nf-text-secondary); margin-top: 1px; }

/* --------------------------------------------------------------- buttons */

.btns { flex: none; display: flex; gap: 8px; }

.stats + .btns { margin-top: 10px; }

.btn {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 48px;
  padding: 0 20px;
  border-radius: var(--nf-radius-md);
  border: 1px solid var(--nf-border-primary);
  background: var(--nf-bg-primary);
  font-size: 15px;
  font-weight: 600;
  color: var(--nf-text-secondary);
  text-align: center;
}

/* green is the normal, non-destructive action, per the standing rule */
.btn--brand {
  background: var(--nf-bg-brand);
  border-color: var(--nf-bg-brand);
  color: var(--nf-text-white);
}

/* the "Move herd" button presses itself in step with each hop the loop fires,
   so the phone reads as reacting rather than an animation on a screen with no
   input behind it */
@keyframes btn-press {
  0%, 100% { background: var(--nf-bg-brand); transform: scale(1); }
  45%      { background: var(--nf-bg-brand-hover); transform: scale(0.97); }
}
.btn--brand.is-pressing { animation: btn-press 0.5s var(--m-out) 0.15s both; }

@media (prefers-reduced-motion: reduce) {
  .btn--brand.is-pressing { animation: none; }
}

.btn--dark {
  background: var(--nf-bg-dark);
  border-color: var(--nf-bg-dark);
  color: var(--nf-text-white);
}

/* ---------------------------------------------------------------- a graph

   The heat prototype's chart card (`.chart-card` in the Animal heat event
   build), measured off it: a white bordered card at radius-lg with 16px
   padding, a 15/700 title with a help affordance, a subhead of day-pill plus
   "Next heat expected", the pinch-to-zoom hint, High/Low on the y axis, the
   cycle-gap markers above the plot, the centred zoom control with the
   full-screen button pinned right, and the six-item legend at 10px.

   The satellite proto2's full-bleed band was the wrong reference for this
   screen: that build's activity scroller and this build's heat card are
   different components. Activity keeps the band, heat gets the card.
   ------------------------------------------------------------------------- */

.chart {
  flex: none;
  display: flex;
  flex-direction: column;
  padding: 16px;
  border-radius: var(--nf-radius-lg);
  border: 1px solid var(--nf-border-secondary);
  background: var(--nf-bg-primary);
}

.chart-h {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 12px;
  font-size: 18px;
  font-weight: 700;
  color: var(--nf-text-primary);
}

.chart-help { flex: none; display: inline-flex; color: var(--nf-text-tertiary); }
.chart-help svg { width: 20px; height: 20px; fill: none; stroke: currentColor; }

/* day pill on the left, next-heat on the right */
.chart-sub {
  flex: none;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin: 2px 0 12px;
}

.chart-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 9px;
  border-radius: var(--nf-radius-sm);
  background: var(--nf-bg-section);
  color: var(--nf-text-tertiary);
  font-size: 11px;
  font-weight: 600;
  white-space: nowrap;
}

.chart-meta { font-size: 12px; color: var(--nf-text-tertiary); white-space: nowrap; }
.chart-meta strong { font-weight: 700; color: var(--nf-text-secondary); }

.chart-hint {
  flex: none;
  margin: 0 0 8px;
  font-size: 11px;
  color: var(--nf-text-placeholder);
  text-align: center;
}

/* the cycle-gap markers that sit above the plot: flame chips joined by a solid
   line for measured gaps and a dashed line with an arrow for the projection */
.chart-marks { position: relative; height: 30px; flex: none; }

.gap-line {
  position: absolute;
  bottom: 16px;
  height: 0;
  border-top: 1.5px solid var(--nf-heat-tint-border);
}

.gap-line--dashed { border-top-style: dashed; }

.gap-label {
  position: absolute;
  bottom: 10px;
  transform: translateX(-50%);
  font-size: 10px;
  font-weight: 700;
  color: var(--nf-text-secondary);
  white-space: nowrap;
  text-shadow: -1px -1px 0 var(--nf-bg-primary), 1px -1px 0 var(--nf-bg-primary),
               -1px 1px 0 var(--nf-bg-primary), 1px 1px 0 var(--nf-bg-primary);
}

.gap-arrow {
  position: absolute;
  bottom: 12.75px;
  transform: translateX(-100%);
  width: 0;
  height: 0;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  border-left: 6px solid var(--nf-heat-tint-border);
}

.gap-mark {
  position: absolute;
  bottom: 6px;
  transform: translateX(-50%);
  width: 22px;
  height: 22px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  background: var(--nf-heat-tint);
  border: 1px solid var(--nf-heat-tint-border);
  color: var(--nf-heat-badge);
}

.gap-mark svg { width: 13px; height: 13px; fill: none; stroke: currentColor; }

/* High / Low down the left of the plot */
.chart-area { display: flex; align-items: stretch; flex: none; }

.chart-y {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 4px 6px 22px 0;
  font-size: 10px;
  color: var(--nf-text-placeholder);
  text-align: right;
  flex: none;
}

.chart-plot { flex: 1; min-width: 0; }
.chart-plot > svg { display: block; width: 100%; height: 210px; }

.chart-x { display: flex; justify-content: space-between; margin-top: 6px; }
.chart-x span { font-size: 10px; color: var(--nf-text-placeholder); }

/* zoom centred, full screen pinned right */
.chart-tools {
  position: relative;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 10px;
  min-height: 30px;
}

.zoom-ctl { display: flex; align-items: center; gap: 4px; }

.zoom-btn {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid var(--nf-border-secondary);
  background: var(--nf-bg-primary);
  color: var(--nf-text-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
}

.zoom-btn svg { width: 15px; height: 15px; fill: none; stroke: currentColor; }
.zoom-label { font-size: 12px; color: var(--nf-text-tertiary); }

.chart-fs {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 30px;
  height: 30px;
  border-radius: var(--nf-radius-sm);
  border: 1px solid var(--nf-border-secondary);
  background: var(--nf-bg-primary);
  color: var(--nf-text-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
}

.chart-fs svg { width: 17px; height: 17px; fill: none; stroke: currentColor; }

/* the six-item legend */
.chart-legend {
  flex: none;
  display: flex;
  column-gap: 12px;
  row-gap: 6px;
  margin-top: 12px;
  flex-wrap: wrap;
}

.leg { display: flex; align-items: center; gap: 4px; font-size: 10px; color: var(--nf-text-tertiary); white-space: nowrap; }
.leg i { width: 14px; height: 2px; border-radius: 1px; flex: none; }
.leg i.dash { height: 0; background: none; border-top: 2px dashed currentColor; }
.leg i.band { height: 10px; border-radius: 2px; background: var(--nf-heat-tint); border-bottom: 2px solid var(--nf-heat-badge); }
.leg svg { width: 13px; height: 13px; flex: none; fill: none; stroke: var(--nf-heat-badge); }

/* plot vocabulary: the prototype's four series */
.g-grid { stroke: var(--nf-bg-secondary); stroke-width: 1; }
.g-thresh { stroke: var(--nf-text-error); stroke-width: 1.5; stroke-dasharray: 5 4; fill: none; }
.g-rum { fill: none; stroke: var(--nf-chart-rumination); stroke-width: 1.5; stroke-dasharray: 4 3; }
.g-act { fill: none; stroke: var(--nf-chart-activity); stroke-width: 1.5; stroke-dasharray: 4 3; }
.g-line { fill: none; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.g-line--heat { stroke: var(--nf-heat-badge); }
.g-line--warn { stroke: var(--nf-bg-warning-solid); }
.g-herd { fill: none; stroke: var(--nf-text-tertiary); stroke-width: 1.5; stroke-dasharray: 5 4; }
.g-band { fill: var(--nf-heat-tint); }
.g-band-edge { stroke: var(--nf-heat-tint-border); stroke-width: 1; stroke-dasharray: 3 3; }
.g-area { opacity: 0.12; }
.g-area--warn { fill: var(--nf-bg-warning-solid); }
.g-tick { font: 500 10px -apple-system, sans-serif; fill: var(--nf-text-placeholder); }

/* the activity scene keeps the satellite build's full-bleed band, which is that
   build's own component and the right reference for that screen */
.chart--band {
  margin: 0 -16px;
  padding: 12px 0;
  border-radius: 0;
  border-left: 0;
  border-right: 0;
  border-top: 1px solid var(--nf-border-secondary);
  border-bottom: 1px solid var(--nf-border-secondary);
  background: var(--nf-bg-section);
}

.chart--band .chart-h { padding: 0 16px; margin-bottom: 6px; font-size: 17px; color: var(--nf-text-brand-primary); }
.chart--band { flex: 1 1 auto; min-height: 230px; max-height: 320px;
  display: flex; flex-direction: column; }
.chart--band .chart-area { flex: 1; min-height: 0; display: flex; }
.chart--band .chart-plot { margin: 0; display: flex; flex: 1; min-height: 0; }
.chart--band .chart-plot > svg { width: 100%; height: 100%; min-height: 210px; }
.chart--band .chart-tools { padding: 0 16px; }

/* "graph animates in with orange" is the storyboard's own note on the activity
   row, and it is the only motion inside a screen in v2. Everything else cuts. */
.screen.is-on .g-draw {
  stroke-dasharray: 1100;
  animation: g-draw 0.9s 0.1s ease forwards;
}

@keyframes g-draw {
  from { stroke-dashoffset: 1100; }
  to   { stroke-dashoffset: 0; }
}

.screen.is-on .g-fade { animation: g-fade 0.55s 0.45s ease both; }

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

.screen.is-on .g-fade-area { animation: g-fade-area 0.55s 0.45s ease both; }

@keyframes g-fade-area {
  from { opacity: 0; }
  to   { opacity: 0.12; }
}

@media (prefers-reduced-motion: reduce) {
  .screen.is-on .g-draw,
  .screen.is-on .g-fade,
  .screen.is-on .g-fade-area { animation: none; }
  .screen.is-on .g-draw { stroke-dasharray: none; }
}

/* ------------------------------------------------------- heat events card

   The prototype's prev-events-card: a bordered card whose header sits above a
   divider, and rows of date-left, italic event name, source chips, chevron. */

.ev-card {
  flex: none;
  margin-top: 12px;
  border-radius: var(--nf-radius-lg);
  border: 1px solid var(--nf-border-secondary);
  background: var(--nf-bg-primary);
  overflow: hidden;
}

.ev-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px 10px;
  font-size: 18px;
  font-weight: 700;
  color: var(--nf-text-primary);
  border-bottom: 1px solid var(--nf-border-secondary);
}

.ev-row {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 12px 16px;
  border-bottom: 1px solid var(--nf-border-secondary);
}

.ev-row:last-child { border-bottom: none; }

.ev-date {
  width: 50px;
  flex: none;
  padding-top: 1px;
  font-size: 13px;
  font-weight: 600;
  color: var(--nf-text-secondary);
}

.ev-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }

.ev-name {
  font-size: 13px;
  font-style: italic;
  color: var(--nf-text-secondary);
  line-height: 1.4;
}

.ev-chips { display: inline-flex; flex-wrap: wrap; gap: 6px; }

.src-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px 3px 7px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
}

.src-chip svg { width: 13px; height: 13px; flex: none; fill: none; stroke: currentColor; }

.src-chip--prov { background: var(--nf-bg-blue-soft); color: var(--nf-text-blue); border: 1px solid var(--nf-border-blue); }
.src-chip--pending { background: var(--nf-bg-warning-soft); color: var(--nf-text-warning); border: 1px solid var(--nf-border-warning); }
.src-chip--confirmed { background: var(--nf-heat-tint); color: var(--nf-heat-badge); border: 1px solid var(--nf-heat-tint-border); }

.ev-trail { display: flex; align-items: center; align-self: center; flex: none; color: var(--nf-text-tertiary); }
.ev-trail svg { width: 18px; height: 18px; fill: none; stroke: currentColor; }

/* a notebook entry that stands for "an entry", not any particular one: the
   payoff row keeps its words, the rest carry a bar */
.ph-line {
  display: block;
  width: 130px;
  max-width: 60%;
  height: 12px;
  margin-top: 2px;
  border-radius: 3px;
  background: var(--nf-bg-secondary);
}

/* the notebook breathes: taller rows than the heat record, because these
   entries are the story here rather than a dense log */
.ev-card--roomy .ev-row { padding: 22px 16px; align-items: center; }
.ev-card--roomy .ev-date { font-size: 15px; }
.ev-card--roomy .ph-line { height: 14px; width: 170px; margin-top: 0; }
.ev-card--roomy .ev-row--calved { padding: 20px 16px 22px; align-items: flex-start; }

/* ============================================== the calving lands, on time ===
   The last scene of calving detection: the Today entry drops into the
   notebook itself, "Calved successfully", wearing the green calf disc.
   Springy on purpose: this is the good news of the whole feature, and the
   timeline is where it belongs. */
.ev-row--calved { background: var(--nf-bg-brand-subtle); }
/* the title leads: top-left, headline-sized, upright. The date drops into the
   chip line so the good news gets the whole first line to itself. */
.ev-row--calved .ev-body { flex: 1; min-width: 0; }
.ev-row--calved .ev-name {
  font-size: 19px;
  font-style: normal;
  font-weight: 700;
  color: var(--nf-text-strong);
  line-height: 1.25;
}
.ev-row--calved .ev-chips { margin-top: 6px; align-items: center; }
/* the date sits in the top-left corner, small, over the headline */
.ev-row--calved .ev-date { width: auto; padding-top: 0; margin-bottom: 2px; }
.screen.is-on .ev-row--calved { animation: calf-pop 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 0.9s both; }
@keyframes calf-pop {
  from { opacity: 0; transform: scale(0.6); }
  to   { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .screen.is-on .ev-row--calved { animation: none; }
}

/* the sticky Record heat footer */
.sticky-cta {
  flex: none;
  padding: 12px 16px calc(var(--nf-safe-bottom, 22px) + 12px);
  background: var(--nf-bg-primary);
  border-top: 1px solid var(--nf-border-secondary);
}

/* ------------------------------------------------------------- lock screen

   Two of the six rows open on a notification, because that is how a farmer
   actually meets the feature: it comes to them. Real wallpaper would be another
   1.6MB, so this is the brand dark with a wash over it. */

/* The night gradient lives on the whole screen, not just the panel: with it on
   .lock the status-bar strip above stayed white, and the white time on it was
   invisible. */
.screen--lock {
  background:
    radial-gradient(120% 70% at 50% 0%,
      color-mix(in srgb, var(--scl3-brand-dark) 84%, white) 0%, transparent 60%),
    linear-gradient(180deg,
      var(--scl3-brand-dark) 0%,
      color-mix(in srgb, var(--scl3-brand-dark) 82%, black) 100%);
}

.lock {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0 14px 34px;
  color: var(--nf-text-white);
}

.lock-time {
  margin-top: 82px;
  font-size: 74px;
  font-weight: 250;
  letter-spacing: -0.04em;
  line-height: 1;
}

.lock-date { margin-top: 4px; font-size: 15px; opacity: 0.66; }

.push {
  width: 100%;
  margin-top: 34px;
  padding: 13px 15px;
  border-radius: 18px;
  background: rgba(255, 255, 255, 0.15);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.22);
}

.screen.is-on .push { animation: push-in 0.44s var(--m-sheet) both; }

@keyframes push-in {
  from { opacity: 0; transform: translateY(-16px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .screen.is-on .push { animation: none; }
}

/* iOS anatomy: the app icon sits in its own column on the left, spanning the
   text stack, with the app name and "now" on the first row beside it. */
.push {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 11px;
  align-items: start;
}

.push-mark {
  grid-row: 1 / span 3;
  width: 38px;
  height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  align-self: center;
  border-radius: 9px;
  background: var(--nf-bg-brand);
  color: var(--nf-text-white);
}

.push-mark svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.8; }

/* the mark takes the alert's colour: a green badge on a warning is a mixed
   signal at a glance */
.push-mark--warn { background: var(--nf-bg-warning-solid); }

.push-top {
  display: flex;
  align-items: center;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  opacity: 0.8;
}

.push-top span { margin-left: auto; font-weight: 500; opacity: 0.8; }

.push-t { margin-top: 2px; font-size: 15px; font-weight: 650; letter-spacing: -0.01em; }
.push-b { margin-top: 2px; font-size: 14px; line-height: 1.4; opacity: 0.84; }

/* ------------------------------------------------------------- bottom nav

   86px including 22px of safe area, four items, 24px icons, 12px labels. */

.tabs {
  flex: none;
  height: calc(var(--nf-bottom-nav, 64px) + var(--nf-safe-bottom, 22px));
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  padding: 10px 0 calc(var(--nf-safe-bottom, 22px) + 2px);
  border-top: 1px solid var(--nf-border-secondary);
  background: var(--nf-bg-primary);
}

.tab {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  font-size: 10px;
  font-weight: 500;
  color: var(--nf-text-tertiary);
}

.tab svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.5; }
.tab.is-on { color: var(--nf-bg-brand); font-weight: 600; }

/* ------------------------------------------------------------ bottom sheet */

.sheet {
  flex: none;
  margin-top: auto;
  padding: 8px 16px 16px;
  border-top: 1px solid var(--nf-border-secondary);
  border-radius: var(--nf-radius-xl) var(--nf-radius-xl) 0 0;
  background: var(--nf-bg-primary);
  box-shadow: 0 -10px 30px rgba(24, 29, 39, 0.1);
  z-index: 20;
}

/* The sheet peeks on the first beat and expands on the second, which is how the
   app's own sheets move: peek, mid, expanded. Because the two beats are two
   screens, the expansion is animated on arrival so it reads as one gesture
   rather than a cut (James, 2026-08-05). */
.sheet--peek {
  max-height: 30px;
  padding-bottom: 0;
  overflow: hidden;
}

.screen.is-on .sheet--expand {
  animation: sheet-up 0.46s var(--m-sheet) both;
}

/* the contents come up a beat behind the panel, so the sheet reads as one
   object arriving rather than a block of text sliding */
.screen.is-on .sheet--expand > * {
  animation: sheet-fill var(--m-base) 0.14s var(--m-out) both;
}

@keyframes sheet-fill {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes sheet-up {
  from { transform: translateY(calc(100% - 30px)); }
  to   { transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .screen.is-on .sheet--expand,
  .screen.is-on .sheet--expand > * { animation: none; }
}

.sheet-handle {
  width: 48px;
  height: 4px;
  margin: 0 auto 12px;
  border-radius: 2px;
  background: var(--nf-border-primary);
}

.sheet-t { font-size: 18px; font-weight: 650; letter-spacing: -0.01em; }
.sheet-b { margin-top: 5px; font-size: 14px; line-height: 1.45; color: var(--nf-text-tertiary); }
.sheet .btns { margin-top: 14px; }
.sheet .stats { margin-top: 12px; }


/* ------------------------------------------------- paddocks building up

   The rotation is told by the paddocks appearing one at a time rather than by a
   divider being dragged: four squares, each arriving in its grazing order, which
   is the same information with no invented gesture (James, 2026-08-05). */

.screen.is-on .pad-in {
  animation: pad-in var(--m-slow) cubic-bezier(0.34, 1.3, 0.5, 1) both;
}

.screen.is-on .pad-in--1 { animation-delay: 0.2s; }
.screen.is-on .pad-in--2 { animation-delay: 0.42s; }
.screen.is-on .pad-in--3 { animation-delay: 0.64s; }
.screen.is-on .pad-in--4 { animation-delay: 0.86s; }

@keyframes pad-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .screen.is-on .pad-in { animation: none; }
}

/* =========================================================== report entry ===

   The grazing-report entry flow (James, 2026-08-11). The components here are
   lifted from the published prototypes rather than approximated, so the demo
   shows the app's own controls:

     .fab            v1 app.html "TWIN: fab"           dark, 3px brand ring, 72px
     .pcard          v1 app.html "TWIN: pasture-card"  80px thumb, heads, grazed
     .accordion      v1 app.html "TWIN: accordion"     Active / Resting groups
     .apo-*          v1 app.html add-pasture-overlay   the real bottom sheet
     .list-link      v1 app.html "TWIN: list-link"     the Dates row
     .bottom-buttons v1 app.html "TWIN: bottom-buttons" the primary CTA

   Two things here are NOT in the twin library and are flagged as new controls
   rather than passed off as design system: the multi-select .chip rows for
   pastures and collars, and the .veil export state. Both need a real component
   decision before this ships anywhere near production.
   ========================================================================= */

/* The reference declares `position: relative` here, because in that build a
   .screen is a flex child (flex: 1) and needs a containing block of its own for
   the FAB, the scrim, the drawer and the veil. In this build .screen is already
   `position: absolute; inset: 0` so the screens can overlap for the crossfade,
   which makes it that containing block anyway; re-declaring relative drops the
   screen out of the stack and leaves it short of the phone. Re-affirmed rather
   than removed so the class still means what the reference means. */
.screen--stack { position: absolute; }

/* ------------------------------------------------------ TWIN: fab (app.html)
   Dark fill with a brand ring, not a green disc: that is what the app ships.
   The glyph rotates into a close, the button itself does not. */
.fab {
  position: absolute;
  right: 16px;
  bottom: calc(var(--nf-bottom-nav, 64px) + var(--nf-safe-bottom, 22px) + 16px);
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--nf-bg-dark);
  color: var(--nf-text-white);
  border: 3px solid var(--nf-bg-brand);
  box-shadow: var(--nf-shadow-fab);
}
.fab.retracted { width: 72px; height: 72px; padding: 0; border-radius: 50%; }
.fab svg { width: 24px; height: 24px; flex: none; fill: currentColor; stroke: none; }
/* The stack sits at z-index 104, the FAB at 105, so the FAB stays bright over
   the scrim and reads as the way out. This is the app's own arrangement. */
.fab.is-close { z-index: 105; }
.fab.is-close svg { transform: rotate(45deg); }

/* ------------------------------------------- TWIN: accordion + pasture-card */
.accordion { flex: none; display: flex; flex-direction: column; }
.accordion-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  background: var(--nf-bg-section);
  color: var(--nf-text-strong);
  font-size: 18px;
  font-weight: 500;
}
.accordion-header .indicator { width: 12px; height: 12px; border-radius: 50%; flex: none; }
.accordion-header .indicator.on { background: var(--nf-bg-brand); }
.accordion-header .indicator.empty { border: 1.5px solid var(--nf-text-tertiary); }
.accordion-header .title { flex: 1; min-width: 0; }
.accordion-header .badge {
  flex: none;
  padding: 2px 8px;
  border-radius: 6px;
  background: var(--nf-bg-secondary);
  color: var(--nf-text-strong);
  font-size: 14px;
  font-weight: 500;
}
.chev-ac {
  width: 20px;
  height: 20px;
  flex: none;
  fill: none;
  stroke: var(--nf-icon-muted);
  stroke-width: 1.5;
  transform: rotate(90deg);   /* expanded, so it points down */
}
.accordion-content { padding: 4px 0 12px; }

.pcard {
  display: grid;
  grid-template-columns: 80px 1fr auto;
  align-items: start;
  gap: 14px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--nf-border-secondary);
}
.pcard:last-child { border-bottom: none; }
.pcard .thumb {
  width: 80px;
  height: 80px;
  flex: none;
  overflow: hidden;
  border-radius: var(--nf-radius-sm);
  background: var(--nf-bg-section);
}
.pcard .thumb-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pcard .thumb--muted { opacity: 0.7; }
.pbody { min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.ptitle { font-size: 18px; font-weight: 600; line-height: 1.2; color: var(--nf-text-primary); }
.pmeta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  font-size: 14px;
  font-weight: 500;
  color: var(--nf-text-tertiary);
}
.meta-heads {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: none;
  font-weight: 700;
  color: var(--nf-text-primary);
}
.meta-heads svg { width: 14px; height: 14px; flex: none; fill: currentColor; stroke: none; }
.meta-graze { display: inline-flex; align-items: center; gap: 6px; flex: none; }
.meta-graze-pct {
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  color: var(--nf-text-brand-link);
}
.meta-graze-spark { width: 52px; height: 14px; flex: none; display: block; }
.meta-graze-area { fill: var(--nf-bg-brand); opacity: 0.18; stroke: none; }
.meta-graze-line { fill: none; stroke: var(--nf-bg-brand); stroke-width: 1.5; }

/* -------------------------------------- the real bottom sheet (apo-*) ----- */
.apo-scrim { position: absolute; inset: 0; z-index: 25; background: var(--nf-bg-dark); opacity: 0.4; }
.apo-drawer {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 30;
  max-height: 86%;
  display: flex;
  flex-direction: column;
  padding-bottom: 32px;
  border-radius: var(--nf-radius-xl) var(--nf-radius-xl) 0 0;
  background: var(--nf-bg-primary);
  box-shadow: var(--nf-shadow-fab);
}
.screen.is-on .apo-drawer { animation: apo-up 0.42s var(--m-sheet) both; }
@keyframes apo-up {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}
.apo-handle { flex: none; display: flex; justify-content: center; padding: 12px 0 8px; }
.apo-handle-bar { width: 36px; height: 4px; border-radius: 999px; background: var(--nf-border-secondary); }
.apo-header {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 16px 16px;
}
.apo-title { font-size: 17px; font-weight: 600; line-height: 1.3; color: var(--nf-text-primary); }
.apo-close {
  width: 32px;
  height: 32px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  color: var(--nf-text-tertiary);
}
.apo-close svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.5; }
/* ------------------------------------------ TWIN: action-picker (app.html)
   The FAB's own menu: dark pills stacked above it, the FAB staying put and
   turning into the close. Twin: library/patterns/action-picker,
   Figma UX Arena 191:16479. Not a bottom sheet: the sheet is the
   add-pasture-overlay pattern, and this is the quick-action one. */
.action-stack {
  position: absolute;
  right: 16px;
  /* clears the 72px FAB that stays in place below it */
  bottom: calc(var(--nf-bottom-nav, 64px) + var(--nf-safe-bottom, 22px) + 16px + 84px);
  z-index: 104;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 12px;
}
.action-pill {
  padding: 12px 20px;
  border-radius: 999px;
  background: var(--nf-bg-dark);
  color: var(--nf-text-white);
  font-size: 15px;
  font-weight: 600;
  white-space: nowrap;
  box-shadow: var(--nf-shadow-fab);
}
/* the one the rep chooses. It arrives unselected like its siblings, takes the
   press once the stack has settled, and holds it (James, 2026-08-11). The real
   pill has no chosen state because in the app the press navigates away at
   once; the demo has to show the choosing and then talk over it. */
/* the stack rises as it appears, one after another, from nearest the FAB up */
.screen.is-on .action-pill { animation: pill-in 0.22s var(--m-out) both; }
.screen.is-on .action-pill:nth-last-child(1) { animation-delay: 0.06s; }
.screen.is-on .action-pill:nth-last-child(2) { animation-delay: 0.14s; }
.screen.is-on .action-pill:nth-last-child(3) { animation-delay: 0.22s; }
/* after the stagger rules so its two delays survive them: the chosen pill
   rises with the stack (0.06s), then takes the press once it settles (1.05s) */
.screen.is-on .action-pill.is-pressed {
  animation: pill-in 0.22s var(--m-out) both, pill-select 0.45s var(--m-out) both;
  animation-delay: 0.06s, 1.05s;
}
@keyframes pill-select {
  0%   { background: var(--nf-bg-dark); transform: scale(1); }
  45%  { background: var(--nf-bg-brand); transform: scale(0.96); }
  100% { background: var(--nf-bg-brand); transform: scale(1); }
}
@keyframes pill-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

.apo-body {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding-bottom: 12px;
}

/* --------------------------------------------------- TWIN: list-link ------ */
.list-link {
  display: grid;
  grid-template-columns: 36px 1fr auto;
  align-items: center;
  gap: 14px;
  padding: 12px 16px;
  border-top: 1px solid var(--nf-border-secondary);
  border-bottom: 1px solid var(--nf-border-secondary);
}
.ll-icon {
  width: 28px;
  height: 28px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--nf-text-primary);
}
.ll-icon svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 1.5; }
.ll-body { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ll-title { font-size: 16px; font-weight: 600; line-height: 1.2; color: var(--nf-text-primary); }
.ll-sub { font-size: 13px; font-weight: 400; line-height: 1.3; color: var(--nf-text-tertiary); }
.ll-trail { display: flex; align-items: center; flex: none; color: var(--nf-text-tertiary); }
.ll-trail svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.5; }

/* --------------------------------------------- TWIN: bottom-buttons ------- */
.bottom-buttons {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 0 16px;
  background: var(--nf-bg-primary);
}
.bottom-buttons .button-row { display: flex; align-items: stretch; gap: 12px; }
.bb-btn {
  flex: 1;
  min-width: 0;
  height: 56px;
  padding: 0 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: var(--nf-radius-md);
  font-size: 17px;
  font-weight: 600;
  white-space: nowrap;
}
.bb-btn.primary {
  background: var(--nf-bg-brand);
  border-color: var(--nf-bg-brand);
  color: var(--nf-text-white);
}
.bb-btn.primary.is-pressed { background: var(--nf-bg-brand-hover); border-color: var(--nf-bg-brand-hover); }

/* ============================================== NEW CONTROL: filter chips ===
   Not in the twin library. Multi-select for pastures and collars, using the
   same shape for both so the two questions read as one idea. Needs a real
   design system decision before production. */
.fgroup { display: flex; flex-direction: column; gap: 10px; padding: 0 16px; }
.flabel {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--nf-text-tertiary);
}
.fcount { font-size: 13px; font-weight: 500; letter-spacing: 0; text-transform: none; }

.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  padding: 9px 14px;
  border: 1px solid var(--nf-border-primary);
  border-radius: 100px;
  background: var(--nf-bg-primary);
  font-size: 15px;
  font-weight: 500;
  color: var(--nf-text-secondary);
}
.chip.is-on {
  border-color: var(--nf-bg-brand);
  background: var(--nf-bg-brand-subtle);
  color: var(--nf-text-brand-primary);
  font-weight: 600;
}

/* ------------------------------------------------- the sheet filling itself

   The sheet arrives with nothing answered, then the answers land one at a
   time. Delays only, no JS: same approach as .pad-in, and the settled screens
   (graze-form-go, graze-export) never carry --filling so they never replay it. */
.apo-drawer--filling .ll-sub--v,
.apo-drawer--filling .fcount--is { opacity: 0; }
.apo-drawer--filling .ll-sub--ph,
.apo-drawer--filling .fcount--was { opacity: 1; }

/* the value and the placeholder occupy the same line, so nothing reflows when
   one replaces the other */
.apo-drawer--filling .ll-body { position: relative; }
.apo-drawer--filling .ll-sub--v { position: absolute; left: 0; bottom: 0; }
.apo-drawer--filling .flabel { position: relative; }
.apo-drawer--filling .fcount--was,
.apo-drawer--filling .fcount--is { position: absolute; right: 0; bottom: 0; }

/* The pacing is deliberate: the sheet settles first (0.42s), and only then do
   the answers land, one control at a time with air between them, so a watcher
   sees each selection HAPPEN rather than a form that arrives filled in
   (James, 2026-08-11: "can we not pre select these options but have them
   animated selected during the step"). Everything before 1s reads as arrived,
   not chosen. */
/* Each control announces itself before its data changes: an emanating glow
   pulls the eye to it, THEN the selection lands (James, 2026-08-11, and the
   standing rule that highlights are emanating, never outlined). The glow leads
   its control's change by ~0.35s all the way down the form, so the sequence
   reads as a cursor moving through it. */
@keyframes glow-ring {
  from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--nf-bg-brand) 45%, transparent); }
  to   { box-shadow: 0 0 0 14px transparent; }
}

/* the date row: glow, the app's pressed-row flash, then the value lands */
.screen.is-on .apo-drawer--filling .list-link {
  animation: glow-ring 0.55s ease both, row-tap 0.4s ease both;
  animation-delay: 0.55s, 0.75s;
}
@keyframes row-tap {
  0%, 100% { background: var(--nf-bg-primary); }
  40%      { background: var(--nf-bg-section); }
}

.screen.is-on .apo-drawer--filling .ll-sub--ph { animation: v-out 0.25s ease 1.0s both; }
.screen.is-on .apo-drawer--filling .ll-sub--v  { animation: v-in  0.25s ease 1.1s both; }

.screen.is-on .apo-drawer--filling .fcount--was { animation: v-out 0.25s ease 3.7s both; }
.screen.is-on .apo-drawer--filling .fcount--is  { animation: v-in  0.25s ease 3.8s both; }
.screen.is-on .apo-drawer--filling .fgroup:last-of-type .fcount--was { animation-delay: 4.9s; }
.screen.is-on .apo-drawer--filling .fgroup:last-of-type .fcount--is  { animation-delay: 5.0s; }

@keyframes v-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes v-in  { from { opacity: 0; } to { opacity: 1; } }

/* the picked chips land in order. fill-mode both holds the unselected look
   through the delay, so the sheet really does start with nothing chosen. */
.apo-drawer--filling .chip.is-on {
  border-color: var(--nf-border-primary);
  background: var(--nf-bg-primary);
  color: var(--nf-text-secondary);
  font-weight: 500;
}
/* strictly one after another (James, 2026-08-11): each chip's tap finishes
   (0.6s) before the next chip's glow begins, so the sequence is three separate
   events, not one wash of colour */
.screen.is-on .apo-drawer--filling .chip.is-on { animation: glow-ring 0.55s ease both, chip-on 0.6s var(--m-out) both; }
/* The delay rules MUST match the shorthand's specificity (five classes), or
   the shorthand's implicit delay: 0 wins and every chip fires at once, which
   is exactly the bug James kept seeing while the measurements sampled too
   coarsely to catch it (2026-08-11). Hence .chip.pick--N, not .pick--N. */
.screen.is-on .apo-drawer--filling .chip.pick--1 { animation-delay: 1.65s, 2.0s; }
.screen.is-on .apo-drawer--filling .chip.pick--2 { animation-delay: 2.7s, 3.05s; }
.screen.is-on .apo-drawer--filling .chip.pick--3 { animation-delay: 3.9s, 4.25s; }

/* Selection is a tap, not a style swap (James, 2026-08-11): the chip goes
   down under the finger still unselected, and comes back up selected. The
   press is the interaction, the colour flip is the styling, in that order. */
@keyframes chip-on {
  0% {
    border-color: var(--nf-border-primary);
    background: var(--nf-bg-primary);
    color: var(--nf-text-secondary);
    font-weight: 500;
    transform: scale(1);
  }
  35% {
    border-color: var(--nf-border-primary);
    background: var(--nf-bg-section);
    color: var(--nf-text-secondary);
    font-weight: 500;
    transform: scale(0.9);
  }
  75% {
    border-color: var(--nf-bg-brand);
    background: var(--nf-bg-brand-subtle);
    color: var(--nf-text-brand-primary);
    font-weight: 600;
    transform: scale(1.03);
  }
  100% {
    border-color: var(--nf-bg-brand);
    background: var(--nf-bg-brand-subtle);
    color: var(--nf-text-brand-primary);
    font-weight: 600;
    transform: scale(1);
  }
}

/* ============================================ NEW CONTROL: export veil =====
   Also not in the twin library. The whole screen goes soft behind it, so the
   rep can say "and it goes off and builds it" without a half-built report on
   screen. color-mix keeps the tint on the token rather than a literal. */
.veil {
  position: absolute;
  inset: 0;
  z-index: 110;
  display: flex;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(14px) saturate(0.9);
  -webkit-backdrop-filter: blur(14px) saturate(0.9);
  background: color-mix(in srgb, var(--nf-bg-primary) 58%, transparent);
}
.veil-box { display: flex; flex-direction: column; align-items: center; gap: 16px; }

/* the disc works, then lands. Spinner and tick share one box so the swap is a
   cross-fade in place rather than the label jumping when the glyph changes. */
.veil-ic {
  position: relative;
  width: 68px;
  height: 68px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--nf-bg-brand);
  color: var(--nf-text-white);
  box-shadow: var(--nf-shadow-fab);
}
.veil-ic svg {
  position: absolute;
  width: 32px;
  height: 32px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
}
.veil-ic-done { stroke-width: 2.2; }

.veil-t { position: relative; font-size: 18px; font-weight: 600; color: var(--nf-text-strong); }
.veil-t-done { position: absolute; left: 50%; top: 0; transform: translateX(-50%); white-space: nowrap; }

/* it works for 1.5s, then completes. Delays only, same as everywhere else. */
.screen.is-on .veil-ic-work  { animation: veil-spin 1.15s linear infinite, v-out 0.2s ease 1.5s both; }
.screen.is-on .veil-ic-done  { animation: v-in 0.28s var(--m-out) 1.56s both; }
.screen.is-on .veil-t-work   { animation: v-out 0.2s ease 1.5s both; }
.screen.is-on .veil-t-done   { animation: v-in 0.28s ease 1.56s both; }
/* the disc gives one pulse as it lands, so completion registers without sound */
.screen.is-on .veil-ic { animation: veil-land 0.42s var(--m-out) 1.5s both; }

@keyframes veil-spin { to { transform: rotate(360deg); } }
@keyframes veil-land {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.09); }
  100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .screen.is-on .apo-drawer,
  .screen.is-on .apo-drawer--filling .chip.is-on,
  .screen.is-on .apo-drawer--filling .ll-sub--ph,
  .screen.is-on .apo-drawer--filling .ll-sub--v,
  .screen.is-on .apo-drawer--filling .fcount--was,
  .screen.is-on .apo-drawer--filling .fcount--is,
  .screen.is-on .veil-ic,
  .screen.is-on .veil-ic-work,
  .screen.is-on .action-pill { animation: none; }
  /* the tick still has to arrive, it just arrives without the spin or the pulse */
  .screen.is-on .veil-ic-work { opacity: 0; }
  .screen.is-on .veil-t-work { opacity: 0; }
}

/* the merged form beat ends with the CTA pressing itself, once the last chip
   has landed (chips land at 0.95 / 1.2 / 1.8s): one beat shows the form
   filling in and going off (James, 2026-08-11) */
.screen.is-on .apo-drawer--filling .bb-btn.primary {
  animation: glow-ring 0.55s ease both, btn-press 0.5s var(--m-out) both;
  animation-delay: 5.3s, 5.65s;
}
@media (prefers-reduced-motion: reduce) {
  .screen.is-on .apo-drawer--filling .bb-btn.primary,
  .screen.is-on .apo-drawer--filling .list-link { animation: none; }
}
