/* =========================================================================
 * MidnightGrid UI foundation -- the one place the look is decided.
 * =========================================================================
 *
 * SOURCE OF TRUTH. Do not edit the copies in extracted/ or landing/assets/;
 * edit this file and run `python tools/sync_design_tokens.py`. A test fails
 * the build if the copies drift.
 *
 * Why a --mg- prefix: four web surfaces and a desktop app already declare
 * --bg, --ink, --line and friends with slightly different values, and 27 app
 * pages are fully self-contained. Introducing this layer under its own prefix
 * is additive -- nothing it touches can break a page that has not migrated
 * yet. Surfaces move onto it one at a time, and the prefix disappears from
 * nobody's way in the meantime.
 *
 * Three layers, in order: primitive (raw values, never used directly in a
 * component) -> semantic (what a thing is FOR) -> component (one widget).
 * Only ever reference the semantic or component layer from real CSS.
 */

/* The display face ships beside every copy of this file (fonts/, synced from
   design/fonts/ with its SIL Open Font License), so the booking page, portal,
   technician app, account pages and the offline desktop app all set headings
   in it -- not only the pages that could reach Google Fonts. */
@font-face { font-family: "Barlow Condensed"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("fonts/barlow-condensed-latin-600-normal.woff2") format("woff2"); }
@font-face { font-family: "Barlow Condensed"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("fonts/barlow-condensed-latin-700-normal.woff2") format("woff2"); }
@font-face { font-family: "Barlow Condensed"; font-style: normal; font-weight: 800; font-display: swap;
  src: url("fonts/barlow-condensed-latin-800-normal.woff2") format("woff2"); }

/* ---- 1. PRIMITIVE ------------------------------------------------------
 * Raw ramps. These have no meaning on their own and must not appear in a
 * rule that styles an actual element.
 * ---------------------------------------------------------------------- */
:root {
  /* Night ramp -- the product's existing dark identity, extended into a
     usable scale. 900 is the page, 800 the panel, 700 the raised surface. */
  --mg-night-900: #070b14;
  --mg-night-800: #0c111b;
  --mg-night-700: #101a2b;
  --mg-night-600: #152239;
  --mg-night-500: #1b2c49;
  --mg-night-400: #223049;
  --mg-night-300: #273753;

  /* Day ramp -- the light pairs already shipping in admin/tech. */
  --mg-day-050: #ffffff;
  --mg-day-100: #f8fafc;
  --mg-day-200: #f0f2f5;
  --mg-day-300: #e8ebf0;
  --mg-day-400: #d9e0ea;
  --mg-day-500: #c8d0dc;

  /* Text ramps, per theme. */
  --mg-ink-dark: #e9eff9;     /* on night */
  --mg-ink-soft-dark: #93a5c0;
  --mg-ink-light: #18202f;    /* on day */
  --mg-ink-soft-light: #5f6a7d;  /* 4.9:1 on the page, 4.6:1 on sunken; #667085 was 4.4 and 4.2 */

  /* Gold -- the one accent. 500 is the brand value; 400 lifts on hover,
     600 is the light-theme variant that holds contrast on white. */
  --mg-gold-400: #ffd070;
  --mg-gold-500: #ffb62e;
  --mg-gold-600: #d99214;
  --mg-gold-700: #8c5c00;   /* gold as TEXT on a light ground: 5.8:1 on white, 5.1:1 on the page, 4.8:1 sunken.
                              #d99214 reads as gold but is only 2.6:1 there. */
  --mg-gold-ink: #171006;     /* text ON gold; 10.8:1 against gold-500 */

  /* Status. Semantic colour is deliberately separate from the accent, so a
     warning never has to borrow the brand's voice to be read as a warning. */
  --mg-green-500: #40dc85;
  --mg-green-600: #1f9d57;
  --mg-green-700: #146c3a;  /* green as TEXT on a light ground: 6.5:1 on white, 5.8:1 on the page.
                              #1f9d57 was 3.5:1 and 3.1:1 -- a "Paid" that could not be read. */
  --mg-amber-500: #f0a52a;
  --mg-red-500: #ff6b5e;
  --mg-red-600: #b42318;
  --mg-blue-500: #4d9dff;

  /* Type. Three faces, three jobs -- not three opinions. Barlow Condensed
     carries the trade-industrial character in display sizes where its width
     is an asset; Inter runs the dense operational UI where it is simply the
     more legible face at 13-15px; IBM Plex Mono is for values that must line
     up in a column. Each surface already had one of these; this assigns the
     role rather than forcing every surface onto one face. */
  --mg-font-display: "Barlow Condensed", "Arial Narrow", sans-serif;
  --mg-font-ui: Inter, ui-sans-serif, system-ui, "Segoe UI", sans-serif;
  --mg-font-mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;

  /* Type scale, fluid. 1.200 minor third -- tight enough for dense job
     tables, wide enough that a marketing hero still lands. */
  --mg-text-xs:  0.78rem;
  --mg-text-sm:  0.875rem;
  --mg-text-base: 1rem;
  --mg-text-lg:  1.125rem;
  --mg-text-xl:  clamp(1.25rem, 0.6vw + 1.1rem, 1.4rem);
  --mg-text-2xl: clamp(1.5rem, 1.2vw + 1.2rem, 1.9rem);
  --mg-text-3xl: clamp(2rem, 2.6vw + 1.3rem, 3rem);
  --mg-text-4xl: clamp(2.6rem, 5vw + 1.2rem, 4.6rem);

  /* Space -- a 4px base, so a 44px touch target is a whole number of steps. */
  --mg-space-1: 0.25rem;
  --mg-space-2: 0.5rem;
  --mg-space-3: 0.75rem;
  --mg-space-4: 1rem;
  --mg-space-5: 1.5rem;
  --mg-space-6: 2rem;
  --mg-space-7: 3rem;
  --mg-space-8: 4rem;

  --mg-radius-sm: 0.4rem;
  --mg-radius: 0.65rem;
  --mg-radius-lg: 1rem;
  --mg-radius-pill: 999px;

  /* Motion. Three durations, two curves. Anything that needs a fourth is
     usually a layout problem wearing an animation. */
  --mg-dur-fast: 120ms;   /* state echo: press, hover, check */
  --mg-dur-base: 220ms;   /* the default: reveal, fade, collapse */
  --mg-dur-slow: 420ms;   /* entrances that carry meaning */
  --mg-ease-out: cubic-bezier(.22, .9, .3, 1);
  --mg-ease-spring: cubic-bezier(.34, 1.4, .5, 1);

  /* Minimum comfortable hit area. Referenced, not retyped, so it can never
     drift below the 44px floor on one surface and not another. */
  --mg-tap: 44px;
}

/* ---- 2. SEMANTIC -------------------------------------------------------
 * What a colour is FOR. Dark is the default: this product is used at 6am in
 * a van and at night in a shop, and every surface already ships dark-first.
 * ---------------------------------------------------------------------- */
:root,
:root[data-mg-theme="dark"] {
  --mg-bg: var(--mg-night-900);
  --mg-bg-raised: var(--mg-night-800);
  --mg-surface: var(--mg-night-700);
  --mg-surface-raised: var(--mg-night-600);
  --mg-surface-sunken: var(--mg-night-800);
  --mg-line: var(--mg-night-400);
  --mg-line-strong: var(--mg-night-300);

  --mg-ink: var(--mg-ink-dark);
  --mg-ink-muted: var(--mg-ink-soft-dark);

  --mg-accent: var(--mg-gold-500);
  --mg-accent-hover: var(--mg-gold-400);
  --mg-on-accent: var(--mg-gold-ink);
  --mg-accent-text: var(--mg-gold-500);   /* 9.9:1 on --mg-surface */

  --mg-ok: var(--mg-green-500);
  --mg-warn: var(--mg-amber-500);
  --mg-danger: var(--mg-red-500);
  --mg-info: var(--mg-blue-500);
  --mg-on-ok: #06210f;        /* a mark set ON the ok colour: 9.6:1 */

  /* Status tones -- pills, chips and badges (Scheduled, Paid, Cancelled...).
     Each is an ink measured against its own tint, not against the page, so a
     pill reads the same on a card, a table row or the page. Opaque, so the
     pair is exactly what tools/test_status_and_theme_contrast.py measures. */
  --mg-status-neutral-ink: #aebdd6;  --mg-status-neutral-bg: #202c3f;
  --mg-status-info-ink: #9cc0ff;     --mg-status-info-bg: #1a2b49;
  --mg-status-pending-ink: #ffd070;  --mg-status-pending-bg: #2d2d2b;
  --mg-status-ok-ink: #7ce8a9;       --mg-status-ok-bg: #153134;
  --mg-status-callback-ink: #f8a8cf; --mg-status-callback-bg: #2f203a;
  --mg-status-progress-ink: #c4b5fd; --mg-status-progress-bg: #212347;
  --mg-status-waiting-ink: #fdba74;  --mg-status-waiting-bg: #312628;
  --mg-status-danger-ink: #ffb4ab;   --mg-status-danger-bg: #312532;

  --mg-focus: var(--mg-gold-400);
  --mg-overlay: rgba(4, 7, 13, .72);
  --mg-shadow: 0 .6rem 1.6rem rgba(0, 0, 0, .45);
  --mg-tint: rgba(255, 255, 255, .05);
}

:root[data-mg-theme="light"] {
  --mg-bg: var(--mg-day-200);
  --mg-bg-raised: var(--mg-day-100);
  --mg-surface: var(--mg-day-050);
  --mg-surface-raised: var(--mg-day-050);
  --mg-surface-sunken: var(--mg-day-300);
  --mg-line: var(--mg-day-500);
  --mg-line-strong: var(--mg-day-400);

  --mg-ink: var(--mg-ink-light);
  --mg-ink-muted: var(--mg-ink-soft-light);

  --mg-accent: var(--mg-gold-600);
  --mg-accent-hover: var(--mg-gold-500);
  --mg-on-accent: var(--mg-gold-ink);    /* NOT white: 2.6:1 fails, this is 7.3:1 */
  --mg-accent-text: var(--mg-gold-700);

  --mg-ok: var(--mg-green-700);
  --mg-warn: #9a5b00;
  --mg-danger: var(--mg-red-600);
  --mg-info: #1a63c2;   /* 4.9:1 on sunken; #1d6fd0 was 4.4 on the page and 4.1 sunken */
  --mg-on-ok: #ffffff;        /* 6.5:1 on the light ok */

  --mg-status-neutral-ink: #344054;  --mg-status-neutral-bg: #eef2f7;
  --mg-status-info-ink: #16499c;     --mg-status-info-bg: #eaf2ff;
  --mg-status-pending-ink: #7a5300;  --mg-status-pending-bg: #fff5d6;
  --mg-status-ok-ink: #146c3a;       --mg-status-ok-bg: #e6f5eb;
  --mg-status-callback-ink: #9d174d; --mg-status-callback-bg: #fdf0f6;
  --mg-status-progress-ink: #5b21b6; --mg-status-progress-bg: #f3ecff;
  --mg-status-waiting-ink: #9a3412;  --mg-status-waiting-bg: #fff1e6;
  --mg-status-danger-ink: #a01812;   --mg-status-danger-bg: #fff0ee;

  --mg-focus: var(--mg-gold-700);  /* 5.8:1 on white; gold-600 was 2.6:1, under the 3:1 floor */
  --mg-overlay: rgba(24, 32, 47, .45);
  --mg-shadow: 0 .6rem 1.6rem rgba(24, 32, 47, .14);
  --mg-tint: rgba(24, 32, 47, .05);
}

/* A surface that has not opted into an explicit theme follows the machine,
   without overriding a choice the user made. */
@media (prefers-color-scheme: light) {
  :root:not([data-mg-theme="dark"]) {
    --mg-bg: var(--mg-day-200);
    --mg-bg-raised: var(--mg-day-100);
    --mg-surface: var(--mg-day-050);
    --mg-surface-raised: var(--mg-day-050);
    --mg-surface-sunken: var(--mg-day-300);
    --mg-line: var(--mg-day-500);
    --mg-line-strong: var(--mg-day-400);
    --mg-ink: var(--mg-ink-light);
    --mg-ink-muted: var(--mg-ink-soft-light);
    --mg-accent: var(--mg-gold-600);
    --mg-accent-hover: var(--mg-gold-500);
    --mg-on-accent: var(--mg-gold-ink);
    --mg-accent-text: var(--mg-gold-700);
    --mg-ok: var(--mg-green-700);
    --mg-warn: #9a5b00;
    --mg-danger: var(--mg-red-600);
    --mg-info: #1a63c2;   /* 4.9:1 on sunken; #1d6fd0 was 4.4 on the page and 4.1 sunken */
    --mg-on-ok: #ffffff;        /* 6.5:1 on the light ok */

    --mg-status-neutral-ink: #344054;  --mg-status-neutral-bg: #eef2f7;
    --mg-status-info-ink: #16499c;     --mg-status-info-bg: #eaf2ff;
    --mg-status-pending-ink: #7a5300;  --mg-status-pending-bg: #fff5d6;
    --mg-status-ok-ink: #146c3a;       --mg-status-ok-bg: #e6f5eb;
    --mg-status-callback-ink: #9d174d; --mg-status-callback-bg: #fdf0f6;
    --mg-status-progress-ink: #5b21b6; --mg-status-progress-bg: #f3ecff;
    --mg-status-waiting-ink: #9a3412;  --mg-status-waiting-bg: #fff1e6;
    --mg-status-danger-ink: #a01812;   --mg-status-danger-bg: #fff0ee;
    --mg-focus: var(--mg-gold-700);  /* 5.8:1 on white; gold-600 was 2.6:1, under the 3:1 floor */
    --mg-overlay: rgba(24, 32, 47, .45);
    --mg-shadow: 0 .6rem 1.6rem rgba(24, 32, 47, .14);
    --mg-tint: rgba(24, 32, 47, .05);
  }
}

/* ---- 3. COMPONENT ------------------------------------------------------
 * One widget's worth of decisions. A component rule reads these; it never
 * reaches past them into the primitive layer.
 * ---------------------------------------------------------------------- */
:root {
  --mg-btn-bg: var(--mg-accent);
  --mg-btn-ink: var(--mg-on-accent);
  --mg-btn-bg-hover: var(--mg-accent-hover);
  --mg-btn-radius: var(--mg-radius);
  --mg-btn-pad: var(--mg-space-3) var(--mg-space-5);

  --mg-card-bg: var(--mg-surface);
  --mg-card-line: var(--mg-line);
  --mg-card-radius: var(--mg-radius-lg);
  --mg-card-pad: var(--mg-space-5);
  --mg-card-shadow: var(--mg-shadow);

  --mg-field-bg: var(--mg-surface-sunken);
  --mg-field-line: var(--mg-line);
  --mg-field-ink: var(--mg-ink);
  --mg-field-radius: var(--mg-radius-sm);
  --mg-field-pad: var(--mg-space-3) var(--mg-space-4);
}

/* ---- focus -------------------------------------------------------------
 * One ring, everywhere, on the surface it lands on. Never removed: a focus
 * ring is the only thing a keyboard user has to see where they are.
 * ---------------------------------------------------------------------- */
/* Deliberately UNSCOPED and single-specificity: three customer-facing pages
   had no focus style whatsoever, so a keyboard visitor had no idea where they
   were. A bare :focus-visible reaches them, and any page rule with a real
   selector (a:focus-visible, .btn:focus-visible) still outranks it, so no
   existing design is overridden. */
:focus-visible {
  outline: 3px solid var(--mg-focus);
  outline-offset: 3px;
}

/* A comfortable hit area on the pages a CUSTOMER uses -- booking, invoices,
   estimates, warranties. Opt-in via <body class="mg-public"> rather than
   global, because a 44px floor is right on a phone in a driveway and wrong in
   a 400-row job table where density is the feature. */
.mg-public button,
.mg-public .btn,
.mg-public input[type="submit"],
.mg-public input[type="button"],
.mg-public a.btn {
  min-height: var(--mg-tap);
}

/* ---- file pickers ------------------------------------------------------
 * The browser's own grey "Choose file" button ignored the theme on every
 * screen that has one (job photos, invoice logo, booking and portal photos,
 * backup restore). Unscoped and single-specificity, like the focus ring, so
 * a page's own rule still wins.
 * ---------------------------------------------------------------------- */
input[type="file"] {
  color: var(--mg-ink-muted);
  font: inherit;
  max-width: 100%;
}
input[type="file"]::file-selector-button {
  margin-right: var(--mg-space-3);
  padding: var(--mg-space-2) var(--mg-space-4);
  border: 1px solid var(--mg-field-line);
  border-radius: var(--mg-field-radius);
  background: var(--mg-surface-raised);
  color: var(--mg-ink);
  font: 600 0.875rem/1.2 var(--mg-font-ui);
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover {
  border-color: var(--mg-accent);
}

/* ---- motion vocabulary -------------------------------------------------
 * Four behaviours, shared by every surface, so a card settling on the
 * marketing site and a job row arriving in the desktop app move the same
 * way. Opacity and transform only -- never width/height/top, which force
 * layout on every frame.
 * ---------------------------------------------------------------------- */
@keyframes mg-rise   { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@keyframes mg-fade   { from { opacity: 0; } to { opacity: 1; } }
@keyframes mg-pop    { from { opacity: 0; transform: scale(.94); }  to { opacity: 1; transform: none; } }
@keyframes mg-sheen  { from { transform: translateX(-60%); } to { transform: translateX(160%); } }

.mg-rise { animation: mg-rise var(--mg-dur-slow) var(--mg-ease-out) both; }
.mg-fade { animation: mg-fade var(--mg-dur-base) var(--mg-ease-out) both; }
.mg-pop  { animation: mg-pop  var(--mg-dur-base) var(--mg-ease-spring) both; }

/* Stagger a list without writing a delay per child. Caps at 12 so a 400-row
   job table never waits three seconds to finish arriving. */
.mg-stagger > * { animation: mg-rise var(--mg-dur-base) var(--mg-ease-out) both; }
.mg-stagger > :nth-child(1)  { animation-delay: 0ms; }
.mg-stagger > :nth-child(2)  { animation-delay: 40ms; }
.mg-stagger > :nth-child(3)  { animation-delay: 80ms; }
.mg-stagger > :nth-child(4)  { animation-delay: 120ms; }
.mg-stagger > :nth-child(5)  { animation-delay: 160ms; }
.mg-stagger > :nth-child(6)  { animation-delay: 200ms; }
.mg-stagger > :nth-child(7)  { animation-delay: 240ms; }
.mg-stagger > :nth-child(8)  { animation-delay: 280ms; }
.mg-stagger > :nth-child(9)  { animation-delay: 320ms; }
.mg-stagger > :nth-child(10) { animation-delay: 360ms; }
.mg-stagger > :nth-child(11) { animation-delay: 400ms; }
.mg-stagger > :nth-child(n+12) { animation-delay: 440ms; }

/* A press should be felt. 120ms is below the threshold where it reads as
   lag and above the threshold where it reads as nothing. */
.mg-press { transition: transform var(--mg-dur-fast) var(--mg-ease-out); }
.mg-press:active { transform: translateY(1px) scale(.995); }

/* Loading that holds its space, so nothing jumps when the data lands. */
@keyframes mg-shimmer { from { background-position: -160% 0; } to { background-position: 260% 0; } }
.mg-skeleton {
  background: linear-gradient(90deg, var(--mg-surface) 30%, var(--mg-surface-raised) 50%, var(--mg-surface) 70%);
  background-size: 220% 100%;
  animation: mg-shimmer 1.4s linear infinite;
  border-radius: var(--mg-radius-sm);
  color: transparent !important;
  user-select: none;
}

/* ---- scroll reveal ------------------------------------------------------
 * Content is VISIBLE by default. The hidden start state only exists once
 * .mg-reveal-ready is on <html>, which script adds after confirming both an
 * IntersectionObserver and that the visitor has not asked for less motion.
 *
 * That order matters: parking content at opacity 0 in the stylesheet and
 * waiting for an observer is how a page ships blank to anyone with script
 * blocked, an old browser, or a failed request. The first still frame -- what
 * a thumbnail, a shared link and a skimming reader all get -- must be the
 * finished page.
 * ---------------------------------------------------------------------- */
.mg-reveal-ready .mg-reveal{
  opacity:0;
  transform:translateY(14px);
  transition:opacity var(--mg-dur-slow) var(--mg-ease-out),
             transform var(--mg-dur-slow) var(--mg-ease-out);
  will-change:opacity, transform;
}
.mg-reveal-ready .mg-reveal.mg-in{opacity:1;transform:none;will-change:auto}

/* Children arrive in sequence once their parent is in view. */
.mg-reveal-ready .mg-reveal-group > *{
  opacity:0;transform:translateY(12px);
  transition:opacity var(--mg-dur-base) var(--mg-ease-out),
             transform var(--mg-dur-base) var(--mg-ease-out);
}
.mg-reveal-ready .mg-reveal-group.mg-in > *{opacity:1;transform:none}
.mg-reveal-ready .mg-reveal-group.mg-in > :nth-child(1){transition-delay:0ms}
.mg-reveal-ready .mg-reveal-group.mg-in > :nth-child(2){transition-delay:60ms}
.mg-reveal-ready .mg-reveal-group.mg-in > :nth-child(3){transition-delay:120ms}
.mg-reveal-ready .mg-reveal-group.mg-in > :nth-child(4){transition-delay:180ms}
.mg-reveal-ready .mg-reveal-group.mg-in > :nth-child(5){transition-delay:240ms}
.mg-reveal-ready .mg-reveal-group.mg-in > :nth-child(n+6){transition-delay:300ms}

/* ---- operator surfaces -------------------------------------------------
 * admin.php and tech.php are dense by design: a 44px floor would be wrong in
 * a 400-row job table where seeing more rows IS the feature. What they do
 * need is a felt response -- tech.php had no :active state at all, so a
 * technician tapping a button in a van got no confirmation the tap landed.
 *
 * Transform only, 120ms: below the threshold where it reads as lag, above the
 * one where it reads as nothing, and it never touches layout.
 * ---------------------------------------------------------------------- */
.mg-app button,
.mg-app .btn,
.mg-app .iconbtn,
.mg-app [role="button"]{
  transition:transform var(--mg-dur-fast) var(--mg-ease-out),
             filter var(--mg-dur-fast) var(--mg-ease-out);
}
.mg-app button:active,
.mg-app .btn:active,
.mg-app .iconbtn:active,
.mg-app [role="button"]:active{
  transform:translateY(1px) scale(.995);
  filter:brightness(1.08);
}

/* ---- reduced motion ----------------------------------------------------
 * Not "less" motion -- none of the decorative kind, with every element left
 * in its finished state. A reveal that never runs must not leave content at
 * opacity 0, which is the classic way this goes wrong.
 * ---------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .mg-rise, .mg-fade, .mg-pop,
  .mg-stagger > * {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .mg-skeleton { animation: none !important; }
  .mg-press { transition: none !important; }
  .mg-reveal, .mg-reveal-group > * {
    opacity: 1 !important; transform: none !important; transition: none !important;
  }
}

/* ---- reduced motion, globally -----------------------------------------
 * The block above covers this file's own classes. This covers everything
 * else on every surface: 35 app pages and the desktop app had no
 * prefers-reduced-motion handling of any kind. Durations are collapsed
 * rather than removed, so transitionend/animationend listeners still fire
 * and nothing waits forever on an event that never comes.
 * ---------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}
