/* css/core.css : the styling for the classes core/offline.js and core/ui.js emit, and the one
   z-index ladder every app in this foundation stacks against.

   NO BRAND COLOURS. Every colour here is a neutral placed behind a variable, so an app sets
   its own palette once (on :root) and never edits this file. Every var() carries a neutral
   fallback, so a surface that sets nothing still looks deliberate rather than unstyled.

   Load this BEFORE the app's own stylesheet, and cache-bust the link (?v=<build>): Pages
   caches CSS for four hours.

   Classes in here, for the integrator:
     body.is-offline     set by offline.syncNet(), the one signal flag
     .offpill            ui.pagehead(), visible only under body.is-offline
     .needsig            ui.needsSignal(), visible only under body.is-offline
     .offnote            the honest line over an empty shell (offline.NOTE)
     .stalebar  .stalebar__msg  .stalebar__retry    a refresh failed behind cached data
     .is-busy            a button mid-write (ui.latch)
     html.no-scroll, body.no-scroll                 the page behind an overlay
     .pagehead  .pagehead__eyebrow  .pagehead__title
   Utilities:
     .minw0              a grid or flex child that may shrink
     .above-tabbar       a column that ends above the fixed tabbar
     .sticky-under-head  a second sticky bar, under the first
*/

:root {
  /* THE ONE Z-INDEX LADDER. These numbers are core.ui.Z, character for character. Register
     #99: a third-party booking overlay covered the error modal. Register #50: the number pad
     was buried under the modal it types into. Nothing in an app writes a bare z-index. */
  --z-base: 0;
  --z-sticky: 20;
  --z-tabbar: 40;
  --z-sheet: 60;
  --z-overlay: 80;
  --z-modal: 100;
  --z-confirm: 120;
  --z-error: 140;
  --z-keypad: 160;
  --z-toast: 180;

  /* Measured at runtime by core.ui.stickyOffset (#161: two sticky bars both at top 0). The
     fallbacks keep a first paint honest before the measurement lands. */
  --topnav-h: 0px;
  --tabbar-h: 64px;
  --safe-b: 0px;

  --core-gap: 10px;
  --core-radius: 10px;
}

@supports (padding: env(safe-area-inset-bottom)) {
  :root { --safe-b: env(safe-area-inset-bottom); }
}

/* ---------- the offline state ---------- */

/* Register #181: on a real phone nobody noticed the app was offline. Disabled buttons with no
   words read as broken. These two are always IN the markup and only ever shown or hidden, so a
   change of signal is a class flip (offline.syncNet) and never a repaint. */
.offpill,
.needsig { display: none; }

body.is-offline .offpill,
body.is-offline .needsig { display: inline-flex; }

.offpill {
  align-items: center;
  gap: 6px;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--core-line, rgba(127, 127, 127, 0.35));
  background: var(--core-surface-2, rgba(127, 127, 127, 0.12));
  color: var(--core-text-2, #5c5c66);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1.4;
  white-space: nowrap;
}
.offpill::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--core-warn, #b4802a);
}

.needsig {
  align-items: center;
  margin-top: 6px;
  color: var(--core-text-3, #77777f);
  font-size: 0.75rem;
  line-height: 1.4;
}

/* The one line over a shell painted with no cache and no signal. It never says which pages
   work without a connection (the owner's rule, 12 Sep): it says this one fills in. */
.offnote {
  margin: var(--core-gap) 0;
  padding: 10px 12px;
  border-radius: var(--core-radius);
  border: 1px solid var(--core-line, rgba(127, 127, 127, 0.35));
  background: var(--core-surface-2, rgba(127, 127, 127, 0.08));
  color: var(--core-text-2, #5c5c66);
  font-size: 0.875rem;
  line-height: 1.45;
}

/* Register #81: a refresh failing behind cached data was console-only forever, so a tab showed
   last week's numbers and said nothing at all. */
.stalebar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--core-gap);
  margin: 0 0 var(--core-gap);
  padding: 8px 10px;
  border-radius: var(--core-radius);
  border: 1px solid var(--core-line, rgba(127, 127, 127, 0.35));
  background: var(--core-surface-2, rgba(127, 127, 127, 0.08));
  color: var(--core-text-2, #5c5c66);
  font-size: 0.8125rem;
  line-height: 1.4;
}
.stalebar__msg { min-width: 0; }
.stalebar__retry {
  flex: 0 0 auto;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--core-line-2, rgba(127, 127, 127, 0.5));
  background: transparent;
  color: inherit;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  /* 44px is the smallest target a thumb hits reliably on a phone. */
  min-height: 44px;
}

/* ---------- a button mid-write ---------- */

/* Register #102 and #183: four taps in two seconds made four rows, and the cell looked
   untouched until the refetch landed. core.ui.latch disables the button AND relabels it; this
   is the part the eye catches. Never opacity alone: a faded button with no words reads as
   broken (#181, the same finding on the offline pass). */
.is-busy,
button[aria-busy="true"] {
  cursor: progress;
  opacity: 0.72;
}
.is-busy::after,
button[aria-busy="true"]::after {
  content: "";
  display: inline-block;
  width: 0.75em;
  height: 0.75em;
  margin-left: 0.5em;
  vertical-align: -0.05em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: core-spin 0.7s linear infinite;
}
@keyframes core-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .is-busy::after,
  button[aria-busy="true"]::after { animation-duration: 2.4s; }
}

/* ---------- the page behind an overlay ---------- */

/* Ethan, 24 Aug: "the only thing that should be moving is the box that's up on the screen."
   Set by core.ui.overlays, which watches for an overlay being present rather than trusting a
   dozen call sites to remember (#163/#164). position:fixed on the body would lose the scroll
   position, so this is overflow only. */
html.no-scroll,
body.no-scroll {
  overflow: hidden;
  overscroll-behavior: none;
  touch-action: pan-x pan-y;
}

/* ---------- page head ---------- */

.pagehead {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--core-gap);
  margin-bottom: var(--core-gap);
}
.pagehead__eyebrow {
  flex: 1 0 100%;
  margin: 0;
  color: var(--core-text-3, #77777f);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.pagehead__title {
  /* min-width:0, same reason as .minw0 below: a long title must be allowed to wrap rather
     than push the pill off the row. */
  min-width: 0;
  margin: 0;
  color: var(--core-text-1, #17171b);
  font-size: 1.5rem;
  line-height: 1.2;
}

/* ---------- utilities ---------- */

/* A grid and flex child's default is min-width:auto, which is "never smaller than my content".
   A number input or a long unbroken word then pushes the track wider than its 1fr share, the
   row overflows, and the page grows a phantom horizontal scrollbar that nobody can trace back
   to the input that caused it (the 1 Sep food form hunt). Any child of a grid or flex row that
   is allowed to shrink gets this. */
.minw0 { min-width: 0; }
.minw0 > * { min-width: 0; }

/* Register #57: a button rendered UNDER the fixed tabbar, which swallowed its taps, and
   nothing errored. The column has to END above the bar, and the bar's height is not a guess:
   it is --tabbar-h plus the home-indicator inset. Every screen that keeps the tabbar visible
   takes this, and core.ui.clearOfFixed proves it on a real viewport. */
.above-tabbar { padding-bottom: calc(var(--tabbar-h) + var(--safe-b) + 20px); }

/* Register #161: the second sticky bar and the header were both at top 0, so the bar slid
   under the header the moment the page scrolled. --topnav-h is measured by ui.stickyOffset. */
.sticky-under-head {
  position: sticky;
  top: var(--topnav-h, 0px);
  z-index: var(--z-sticky);
}

/* Keyboard people can always see where they are, and a thumb never sees this ring. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--core-ring, #4b7bd1);
  outline-offset: 2px;
  border-radius: inherit;
}
/* The ring is the ONLY thing that may remove the default outline, and it puts one back. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus:not(:focus-visible) {
  outline: none;
}
