/* dirtxearthworks.com - our own layer.
 *
 * Why a second stylesheet at all: mirror/assets/index-xA7zmFVz.css is a PURGED
 * Tailwind capture. It holds the ~900 class names the original site happened to
 * use and nothing else, and there is no framework build left to re-run - the
 * deploy is `wrangler pages deploy public`. A class we invent there renders as
 * nothing, silently. So anything the purge cannot express lives here instead.
 *
 * This file is linked AFTER the inherited sheet, so plain cascade order wins.
 * No !important except where an inherited utility sits on the same element at
 * the same weight - and there is none in here so far.
 *
 * Every class name used as a selector below is one the captured markup already
 * carries. test/classes.test.mjs unions these selectors into the vocabulary the
 * generated pages are checked against.
 */

/* --- ink ---------------------------------------------------------------- */

:root {
  /* The brand lime, --primary 74 78% 51% (#B6E421), measures 1.49:1 on white.
     It is used AS TEXT in 56 places - every LEARN MORE, every eyebrow, the 18+
     on /about - and in daylight in a cab those are simply gone. This is the
     same 74deg hue at ink weight: #4E6600, 6.51:1 on white.
     The lime itself is not touched. It is on his truck, and on the dark
     surfaces where it sits on a fill it measures 10.42:1 - excellent. */
  --primary-ink: 74 100% 20%;

  /* Which of the two the text utilities resolve to. Declared per SURFACE
     below rather than per element, so it follows the DOM: the nearest
     surface an element sits on wins, at any nesting depth, and a lime chip
     over white and the same chip over the dark hero both come out right
     without either being enumerated. */
  --dx-primary-text: var(--primary-ink);
}

.text-primary { color: hsl(var(--dx-primary-text)); }
.hover\:text-primary:hover { color: hsl(var(--dx-primary-text)); }
.hover\:text-primary\/80:hover { color: hsl(var(--dx-primary-text) / .8); }
.hover\:text-primary\/90:hover { color: hsl(var(--dx-primary-text) / .9); }

/* Dark surfaces: lime is right here, and it is the best contrast on the site. */
.bg-secondary,
.bg-black,
.from-black\/90,          /* photo cards and the hero scrim */
.min-h-\[100svh\],        /* the home hero */
header.bg-transparent {   /* the header while it is still over the hero */
  --dx-primary-text: var(--primary);
}

/* Light surfaces, including ones nested inside a dark section. */
.bg-background,
.bg-background\/95,       /* the header once it has gone solid on scroll */
.bg-card,
.bg-muted,
.bg-white,
.bg-secondary\/5 {
  --dx-primary-text: var(--primary-ink);
}

/* Footer body text was --muted-foreground (#5C6370) on --secondary (#21242C):
   2.57:1, below every threshold there is. Same hue and saturation, lifted to
   #9DA3AF = 6.13:1. One variable, so every use in the footer moves together. */
footer { --muted-foreground: 220 10% 65%; }

/* --- the first screen --------------------------------------------------- */

/* The drone footage is the front door and it stays (Adam, 2026-09-07) - now
   self-hosted rather than embedded. It just was not visible before: the iframe
   rendered at opacity-80 over a photo at opacity-60 under a flat from-black/90
   wash, and the whole stack came out at mean RGB 16-29 against the photo file's
   own 121 - about a tenth of its brightness. A black rectangle with a machine
   somewhere in it.

   The clip carries no opacity class at all now, so it plays at full strength;
   the photo underneath goes to full opacity as the fallback layer, for the
   moment before the clip paints, for a browser that will not play it, and for
   the reduced-motion path at the bottom of this file. */
.min-h-\[100svh\] img.opacity-60 { opacity: 1; }

/* The darkening moves to a scrim that covers the type and lets go of the
   machine. Below 1024px the type stacks low, so the scrim comes up from the
   bottom; from 1024px it is in the left column, so it runs across.
   (768-1023px keeps the bottom-up version - the type is still stacked.)

   The mid-stops are deliberately deep. Reading contrast off the gradient's
   DARKEST stop is a trap: the type box spans the ramp, so the h1's top edge
   lands near the light end, not the dark one. Measured per-pixel on the
   rendered page, the shallower ramp put the phone h1's worst pixel at 2.59:1
   - a fail - while the darkest-stop arithmetic predicted 16:1. These values
   measure 6.58:1 (phone h1) and 7.03:1 (desktop h1) worst-pixel. Re-measure
   on the rendered page, not from these numbers, if you change them. */
.min-h-\[100svh\] .bg-gradient-to-r {
  background-image: linear-gradient(to top,
    rgba(15, 17, 21, .94) 0%,
    rgba(15, 17, 21, .82) 45%,
    rgba(15, 17, 21, .55) 100%);
}
@media (min-width: 1024px) {
  .min-h-\[100svh\] .bg-gradient-to-r {
    background-image: linear-gradient(100deg,
      rgba(15, 17, 21, .92) 0%,
      rgba(15, 17, 21, .86) 38%,
      rgba(15, 17, 21, .40) 68%,
      rgba(15, 17, 21, .35) 100%);
  }
}

/* A full-height hero on a phone means nothing below it ever peeks, and the page
   reads as one screen deep. 78svh puts the top of the next section on screen. */
@media (max-width: 767px) {
  .min-h-\[100svh\] { min-height: 78svh; }
}

/* --- phone mechanics ---------------------------------------------------- */

/* The call bar is fixed to the bottom edge, which on an iPhone is where the
   home indicator lives. safe-area-inset appeared zero times in the whole build. */
.fixed.bottom-0.z-50.md\:hidden > a {
  padding-bottom: calc(1rem + env(safe-area-inset-bottom));
}
/* ...and the bar covers the end of the page. Padding goes on the footer rather
   than on <body>, so the extra height keeps the footer's own dark background
   instead of showing a white strip under it. */
/* footer.pb-8, not footer: the inherited `pb-8` utility is a class, and a bare
   element selector loses to it. Measured 32px before this was specific enough. */
@media (max-width: 767px) {
  footer.pb-8 { padding-bottom: calc(2rem + 3.75rem + env(safe-area-inset-bottom)); }
}

/* 31 controls measured under 44px at 390px. Every one of these is already a
   flex or inline-flex box, so a min-height is the whole fix. */
#mobile-menu > button[type="button"] {   /* the drawer close: a 16px x */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
}
#mobile-menu li a { min-height: 44px; }
a.inline-flex.text-primary.uppercase { min-height: 44px; }  /* six LEARN MORE, 150x24 */
footer a:not([rel="author"]) { min-height: 44px; }           /* twelve rows, 358x24 */
main li > a.hover\:text-primary {                            /* nine service-area rows, 24px */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
/* min-height does nothing to an inline box, so promote the ones that are still
   inline - without disturbing the full-width flex rows or the logo. */
footer a:not([rel="author"]):not(.flex):not(.inline-block) {
  display: inline-flex;
  align-items: center;
}

/* --- motion -------------------------------------------------------------- */

/* prefers-reduced-motion appeared zero times in the build, while the site
   smooth-scrolls and animates on hover throughout. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  /* An autoplaying drone shot is precisely what this query is asking us not to
     serve. The photo underneath is already at full opacity, so hiding the clip
     reveals it with nothing else to do. The page also removes the <video>
     outright in script, which is the only way to stop it downloading and
     playing - this rule is what happens when that script does not run. */
  .min-h-\[100svh\] video { display: none; }
}

/* Cory's real logo, 2026-09-07. The file the mirror captured was 78x64 actual
   pixels with the tagline and phone number about four pixels tall - unreadable at
   any size, and enlarging it only made a bigger blur. He supplied the original;
   it is keyed off its black background (flood-filled from the edges so the black
   INSIDE the shield survives) and re-sized to 400px tall, which stays crisp to
   ~133px on a 3x phone.

   Sized here rather than by editing the markup classes: the header's classes are
   recorded positionally in snapshot/_header-states.json and restyled by index on
   scroll, so a class change in markup silently reverts on the visitor's first
   scroll unless both state arrays are edited to match. */
header .container img[src*="/assets/img/logo."],
header img[src*="/assets/img/logo."] { height: 112px !important; }
footer img[src*="/assets/img/logo."]  { height: 128px !important; }
@media (min-width: 768px) {
  header .container img[src*="/assets/img/logo."],
  header img[src*="/assets/img/logo."] { height: 160px !important; }
  footer img[src*="/assets/img/logo."]  { height: 160px !important; }
}

/* The real logo made the header taller (96px logo -> 128px header on a phone,
   120 -> 168px from 768px). The hero's own padding-top was 96px/80px, which the
   header now overlaps - Adam saw it collide with the "18 years experience" chip.
   Clear the header, plus a little breathing room. Measured, not guessed, and
   re-measured for the oval badge (2026-09-07): header bottom is 144px at 390px
   and 208px at 1280px; these leave 24px above the chip at both. */
section.min-h-\[100svh\] { padding-top: 168px !important; }
@media (min-width: 768px) {
  section.min-h-\[100svh\] { padding-top: 232px !important; }
}

/* --- the anti-spam box ---------------------------------------------------- */

/* Turnstile draws itself into .cf-turnstile. The class comes from Cloudflare's
   script, not from the purged Tailwind build, so it is defined here or
   test/classes.test.mjs (rightly) calls it an invented class.

   min-height reserves the widget's own height before the script has run, so the
   Submit button does not jump down the moment it loads. */
.cf-turnstile {
  margin: 1.25rem 0 .25rem;
  min-height: 65px;
}

/* Shown only when JavaScript is off, where no token can ever be minted. Styled
   as a warning the visitor reads BEFORE filling the form in, not after. */
.dirtx-nojs {
  margin: 1rem 0;
  padding: .75rem 1rem;
  border-left: 3px solid hsl(var(--primary));
  background: rgba(0, 0, 0, .35);
  color: #e5e7eb;
  font-size: .95rem;
  line-height: 1.5;
}
.dirtx-nojs a {
  color: hsl(var(--primary));
  font-weight: 700;
  white-space: nowrap;
}

/* Every box on every form is required now (Adam, 2026-09-20), so every label
   carries a mark. It is aria-hidden - `required` is what a screen reader
   announces, and a visible asterisk in seven labels would otherwise be read out
   as "star" seven times. */
.dirtx-req {
  margin-left: .3em;
  color: hsl(var(--primary));
  font-weight: 800;
}
