/* ===========================================================================
   Edge of Defense: site.css

   ADDITIONS ONLY. theme.css is the design system and ships unmodified; this
   file holds what the site needs on top of it. Keeping them separate means
   every override is visible in one place and theme.css stays a clean upstream.

   Never hard-code a colour, size, or spacing step here. Use the tokens from
   theme.css :root. If a value you need doesn't exist, add the token there
   first.

   Bracketed codes in comments are element tags; see TAGS.md, or open any
   page with ?tags to see them on screen.
   ========================================================================= */


/* ---------------------------------------------------------------------------
   WORDMARK  [h9pz] [f6wz]
   The logo is a CSS mask rather than an inline SVG: inlining costs ~17KB
   gzipped on every page, whereas a mask is fetched once and cached for the
   whole site. `background: currentColor` preserves the recolour-on-hover
   behaviour theme.css expects.
   ------------------------------------------------------------------------ */

.eod-wordmark {
  display: block;
  aspect-ratio: 4917 / 776;
  background-color: currentColor;
  -webkit-mask: url("../assets/logo-wordmark.svg") no-repeat center / contain;
          mask: url("../assets/logo-wordmark.svg") no-repeat center / contain;
}

/* If masking is unavailable the block would render as a solid bar, which is
   worse than no logo. Fall back to set type instead. */
@supports not ((-webkit-mask-image: url("#m")) or (mask-image: url("#m"))) {
  .eod-wordmark {
    background: none;
    aspect-ratio: auto;
    font-family: var(--eod-font-display);
    font-size: 1.25rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    text-decoration: none;
    color: currentColor;
  }
  .eod-wordmark::after { content: "Edge of Defense"; }
}


/* ---------------------------------------------------------------------------
   MOBILE NAVIGATION  [h6bk] [h2mq]
   theme.css hides .eod-nav__link below 780px and documents that a disclosure
   menu must replace it, and this is that menu. Behaviour lives in site.js.
   ------------------------------------------------------------------------ */

.eod-navtoggle {
  display: none;
  align-items: center;
  gap: var(--eod-s1);
  padding: var(--eod-s1) var(--eod-s2);
  background: transparent;
  color: var(--eod-text);
  border: 1px solid var(--eod-line-strong);
  border-radius: var(--eod-radius);
  font-family: var(--eod-font-body);
  font-size: var(--eod-size-label);
  font-weight: 600;
  letter-spacing: var(--eod-tracking-label);
  text-transform: uppercase;
  cursor: pointer;
}
.eod-navtoggle:hover { border-color: var(--eod-accent); color: var(--eod-accent-bright); }

/* Three bars that become an X when open. */
.eod-navtoggle__bars,
.eod-navtoggle__bars::before,
.eod-navtoggle__bars::after {
  display: block;
  width: 18px;
  height: 2px;
  background: currentColor;
  transition: transform var(--eod-dur) var(--eod-ease),
              opacity var(--eod-dur) var(--eod-ease);
}
.eod-navtoggle__bars { position: relative; }
.eod-navtoggle__bars::before,
.eod-navtoggle__bars::after { content: ""; position: absolute; left: 0; }
.eod-navtoggle__bars::before { top: -6px; }
.eod-navtoggle__bars::after  { top: 6px; }

.eod-navtoggle[aria-expanded="true"] .eod-navtoggle__bars { background: transparent; }
.eod-navtoggle[aria-expanded="true"] .eod-navtoggle__bars::before { transform: translateY(6px) rotate(45deg); }
.eod-navtoggle[aria-expanded="true"] .eod-navtoggle__bars::after  { transform: translateY(-6px) rotate(-45deg); }

@media (max-width: 780px) {
  .eod-navtoggle { display: inline-flex; }

  /* Re-show the links theme.css hides, but inside the disclosure panel. */
  .eod-nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: var(--eod-s2) var(--eod-s3) var(--eod-s3);
    background: var(--eod-ground);
    border-bottom: 1px solid var(--eod-line);
    box-shadow: 0 12px 24px rgba(var(--eod-shadow-rgb), 0.45);
  }
  .eod-nav[hidden] { display: none; }

  .eod-nav .eod-nav__link {
    display: block;
    padding-block: var(--eod-s2);
    font-size: 1.0625rem;
    border-bottom: 1px solid var(--eod-line);
  }
  .eod-nav .eod-nav__link[aria-current="page"] { border-bottom-color: var(--eod-accent); }

  .eod-nav__cta { margin-top: var(--eod-s3); justify-content: center; }
}

/* Above the breakpoint the panel is always open and the toggle is gone, so
   the hidden attribute JS uses must not apply. */
@media (min-width: 781px) {
  .eod-nav[hidden] { display: flex; }
}

/* Unprefixed backdrop-filter only landed in Safari 18, so theme.css's header
   blur is absent on every earlier Safari, including current iOS, which makes
   this the one browser-support gap on this site that affects people using
   up-to-date devices rather than only the 2020 floor. -webkit-backdrop-filter
   has worked since Safari 9. Cosmetic either way: the header background is 86%
   opaque, so it stays readable with no blur at all. */
.eod-header { -webkit-backdrop-filter: blur(10px); }

.eod-header__inner { position: relative; }


/* ---------------------------------------------------------------------------
   HERO  [p7dk] [p3wn] [p9zc]
   Photo, hatch field, and the chrome mark stacked. theme.css already provides
   .eod-hatch (the heraldic vert hatch) and fades it out toward the bottom;
   the photo sits underneath that, behind a scrim tuned so --eod-text still
   clears AAA over the brightest part of the image.
   ------------------------------------------------------------------------ */

.eod-hero {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  min-height: clamp(30rem, 68vh, 44rem);
  padding-block: var(--eod-s7);
}

.eod-hero__photo,
.eod-hero__photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center right;
}
.eod-hero__photo { z-index: -3; }

/* Scrim. Heavier on the left where the copy sits, lighter on the right where
   the house and the mark are.

   This is the narrow-viewport version, and it stays heavy on purpose. Below
   1024px the container is full-bleed and the hero copy wraps most of the way
   across, so there is no "right-hand side" free of text to open up. The
   desktop treatment is in the min-width block further down. */
.eod-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  background:
    /* The two right-hand stops were 0.42 and 0.52. Composited against the
       actual photo that put the whole image between 1.00x and 1.09x the
       luminance of the flat page ground -- a maximum contrast of 1.09:1, so
       the photograph read only in a dark room on a good monitor and vanished
       under any ambient light. The source is itself a dark night shot, and the
       scrim was heaviest exactly where the image carried the most information.
       The left-hand stops are untouched: all hero copy sits left of ~55%,
       where the scrim still runs 0.57 or heavier. */
    linear-gradient(100deg,
      rgba(var(--eod-ground-rgb), 0.92) 0%,
      rgba(var(--eod-ground-rgb), 0.80) 38%,
      rgba(var(--eod-ground-rgb), 0.22) 68%,
      rgba(var(--eod-ground-rgb), 0.28) 100%),
    linear-gradient(to bottom,
      rgba(var(--eod-ground-rgb), 0.55) 0%,
      transparent 30%,
      var(--eod-ground) 97%);
}

/* The mark. Rendered chrome rather than the flat SVG, because at this size the
   modelling reads, and it is the site's one moment of visual weight. The flat
   currentColor SVG still handles header, footer, and favicon, where a bevel
   does nothing and raster would be soft. */
.eod-hero__mark {
  position: absolute;
  right: -2%;
  top: 50%;
  transform: translateY(-50%);
  width: min(38vw, 420px);
  height: auto;
  z-index: -1;
  opacity: 0.8;
  pointer-events: none;
  filter: drop-shadow(0 8px 24px rgba(var(--eod-shadow-rgb), 0.55));
}

.eod-hero__inner { position: relative; }

.eod-hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--eod-s2);
  margin: var(--eod-s4) 0 var(--eod-s3);
  max-width: none;
}

.eod-hero__meta { max-width: 60ch; }

@media (max-width: 860px) {
  .eod-hero {
    min-height: 0;
    padding-block: var(--eod-s6);
  }
  /* Below this width the mark would sit behind the headline. Move it out of
     the text's way and drop it back. */
  .eod-hero__mark {
    right: -18%;
    width: 62vw;
    opacity: 0.28;
  }
  .eod-hero__photo img { object-position: 72% center; }
}

@media (max-width: 520px) {
  .eod-hero__mark { opacity: 0.18; }
}

/* Desktop hero scrim. A single straight ramp: the photograph runs at about
   half brightness on the left where the copy sits and reaches full brightness
   at the right edge. Client direction, 2026-08-10.

   The previous version held a dark plateau to 64% and then dropped to nearly
   clear by 80%. That gave the copy more cover, but a 0.88-to-0.12 fall across
   a sixth of the width read as a visible edge running down the picture rather
   than as light. A constant-slope ramp has no edge to see anywhere in it,
   which is the whole point of the change.

   Below 1024px the copy wraps across ~90% of the width, so there is no clear
   right-hand side to hand back to the photograph. The base scrim above stays
   as it is and this block does not apply.

   The cost is contrast over the copy, and it is steep. Measured in Chrome
   against the real inked text rects (Range.getClientRects per element, not
   the block boxes, which run much wider than the text), worst case across
   1024/1180/1280/1440/1680/1920/2560:

       --eod-text       13.88 -> 2.86   h1, eyebrow, buttons
       --eod-text-dim    6.60 -> 1.36   lead paragraph
       --eod-text-mute   5.09 -> 2.89   meta strip

   All three are below WCAG AA (4.5:1), and the lead is below 3:1, which is
   the floor for large text. The worst readings are where the copy crosses the
   lit porch, which the ramp no longer covers: a 50% scrim on the left edge
   has thinned to roughly 0.25 by the time it reaches the end of a line.

   This is a deliberate trade of legibility for the photograph, made on client
   direction with the numbers above on the table. It is not an oversight and
   it is not a regression to be quietly fixed. See CLAUDE.md, Known open
   issues, before changing it back. */
@media (min-width: 1024px) {
  .eod-hero::before {
    background:
      linear-gradient(100deg,
        rgba(var(--eod-ground-rgb), 0.50) 0%,
        rgba(var(--eod-ground-rgb), 0.00) 100%);
  }

  /* Centred in the gap between where the copy ends and the right edge, and
     lifted above the middle so it sits with the roofline rather than the
     porch. The copy ends at 63% at 1024px and 52% at 2560px, so the midpoint
     of that gap moves from ~81% to ~76%; 78% splits the difference and keeps
     the whole mark on screen at every width. */
  .eod-hero__mark {
    right: auto;
    left: 78%;
    top: 40%;
    transform: translate(-50%, -50%);
  }
}

/* 1024 to 1179 is the awkward range: the copy still reaches 63% of the width,
   so the gap is only ~370px while the mark wants 389px of it. Narrow the mark
   and push it further right, or its left edge lands under the end of the lead. */
@media (min-width: 1024px) and (max-width: 1179px) {
  .eod-hero__mark {
    left: 81%;
    width: min(34vw, 420px);
  }

  /* Two bottom fades were stacked here. theme.css fades .eod-hatch to full
     ground from 0% to 92%, and the scrim above ran a second one of its own
     from 30% to 97%. Multiplied together they took the photo to 2% at y=80%
     and 0% by y=95%, which is why the house had no base. One fade does the
     job, and it only needs the last third to blend the hero into the page. */
  .eod-hero.eod-hatch::after {
    background: linear-gradient(to bottom,
      transparent 0%,
      transparent 68%,
      var(--eod-ground) 100%);
  }
}


/* ---------------------------------------------------------------------------
   SERVICE FILTER  [s3fq]
   Progressive enhancement: the control ships hidden and site.js reveals it.
   With JS off, all twelve services are simply shown.
   ------------------------------------------------------------------------ */

.eod-filter {
  display: flex;
  flex-wrap: wrap;
  gap: var(--eod-s1);
  margin-bottom: var(--eod-s3);
}

.eod-filter__btn {
  padding: 0.5rem var(--eod-s2);
  background: transparent;
  color: var(--eod-text-dim);
  /* --eod-line is 1.44:1 against the ground: a control whose only boundary is
     a border nobody can see. WCAG 1.4.11 wants 3:1 for the visual boundary of
     a UI component. --eod-line-strong is 3.46:1 on the ground and 3.28:1 on
     the surface these sit over. --eod-line stays where it belongs, on the
     hairline dividers, which are decorative and out of 1.4.11's scope. */
  border: 1px solid var(--eod-line-strong);
  border-radius: var(--eod-radius);
  font-family: var(--eod-font-body);
  font-size: var(--eod-size-label);
  font-weight: 600;
  letter-spacing: var(--eod-tracking-label);
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--eod-dur) var(--eod-ease),
              border-color var(--eod-dur) var(--eod-ease),
              background var(--eod-dur) var(--eod-ease);
}
/* Hover brightens the label only. The border already sits at --eod-line-strong
   at rest, and the next step up is the accent, which is the active state, so a
   hover that borrows it would read as "selected". Three distinct states:
   dim label / bright label / accent label + accent border + wash. */
.eod-filter__btn:hover { color: var(--eod-text); }
.eod-filter__btn.is-active {
  color: var(--eod-accent-bright);
  border-color: var(--eod-accent);
  background: var(--eod-accent-wash);
}

.eod-filter__status:empty { display: none; }
.eod-filter__status { margin-bottom: var(--eod-s2); }

.eod-plate[hidden] { display: none; }

/* theme.css lifts the plate 3px and turns its top rule accent on hover. No
   plate contains a link. Checked against the built output, 0 of 14 hold an
   <a>, so the lift promises a navigation that does not exist, and does it at
   the moment interest is highest: someone has read a service and moved the
   cursor onto it. Accent compounds that, because on this site accent is the
   interactive colour: links, CTA fill, focus ring.

   :focus-within is in the same theme.css rule and cannot ever match: a plate
   holds an h3 and three <p>s, nothing focusable. It is neutralised here rather
   than left to imply a keyboard path that does not exist.

   The background raise is kept. It helps track position in a fourteen-card
   grid without suggesting a destination.

   If plates ever gain links, delete this block rather than working around it. */
.eod-plate:hover,
.eod-plate:focus-within {
  transform: none;
  border-top-color: var(--eod-line-strong);
}

/* Sits directly under the price, tighter than the track line below it. */
.eod-plate__note {
  margin: 0.25rem 0 0;
  font-size: var(--eod-size-small);
}

.eod-plate__track {
  margin: var(--eod-s1) 0 0;
  font-size: var(--eod-size-label);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}


/* ---------------------------------------------------------------------------
   PAGE FURNITURE
   ------------------------------------------------------------------------ */

.eod-section__more { margin-top: var(--eod-s4); }

.eod-contact-grid,
.eod-footer__grid { display: grid; gap: var(--eod-s4); }

/* 272px, not 240px. Both bounds are tight and measured:

     lower: the email address is one unbreakable token measuring 267px, and a
             240px floor yielded 262px columns at a 900px viewport, the one
             width where the address still wrapped.
     upper: a 320px viewport minus the container's 2 x 24px padding leaves
             exactly 272px. A floor above that overflows the container, which
             costs 320px the clean bill of health it currently has.

   That leaves a 5px window. If the address or the container padding ever
   changes, re-measure rather than nudging this number. */
.eod-contact-grid { grid-template-columns: repeat(auto-fit, minmax(272px, 1fr)); }

/* Body face, not display. Cinzel is a caps-only face, so it rendered the
   address as TYEWELLS@EDGEOFDEFENSE.COM, wider than the column and in the
   wrong case for something a reader has to copy down by hand. At 22px it
   needed ~371px and the auto-fit column never exceeds 356px, so it wrapped
   mid-token at every width from 1180 down to 360, twice leaving an orphan "M"
   on its own line. Shrinking Cinzel until it fit meant ~17.8px at the 1024px
   breakpoint, which is no longer a display treatment.

   The phone number fits either face; both are set here so two adjacent
   contact blocks don't read as different decisions. */
.eod-contact__big {
  font-family: var(--eod-font-body);
  /* 19px, not 20px: at 20px the address measures 281px, which is wider than
     the 272px of content a 320px viewport leaves after container padding. 19px
     brings it to ~267px and keeps it on one line at every width down to 320. */
  font-size: 1.1875rem;
  font-weight: 500;
  margin-bottom: var(--eod-s1);
  /* Kept as the safety net it was added as. It is no longer load-bearing at
     any tested width. The address now fits on one line from 1180 to 360,
     but a longer address should still wrap rather than scroll the page. */
  overflow-wrap: break-word;
}
.eod-contact__big a { text-decoration: none; }

.eod-footer__mark { width: 180px; color: var(--eod-text-dim); }
.eod-footer__mark:hover { color: var(--eod-accent-bright); }
.eod-footer__tagline { margin-top: var(--eod-s2); max-width: 42ch; }
.eod-footer__legal { margin: 0; }

/* Long-form pages (privacy) need vertical rhythm between headings. */
.eod-prose h2 { margin-top: var(--eod-s5); }
.eod-prose h2:first-of-type { margin-top: var(--eod-s3); }


/* ---------------------------------------------------------------------------
   PLACEHOLDER MARKER
   Deliberately loud. Unconfirmed copy must be impossible to ship by accident.
   This is a security consultancy, and a stray lorem-ipsum paragraph costs
   more credibility here than a plain design ever would.
   Remove the wrapping <span class="eod-ph"> as each item is confirmed.
   ------------------------------------------------------------------------ */

.eod-ph {
  display: inline;
  padding: 0.1em 0.3em;
  background: rgba(var(--eod-alert-rgb), 0.12);
  box-shadow: inset 0 0 0 1px var(--eod-alert);
  border-radius: var(--eod-radius);
  color: var(--eod-text);
}
.eod-ph::before {
  content: "UNCONFIRMED";
  display: inline-block;
  margin-right: 0.5em;
  font-size: 0.65em;
  font-weight: 600;
  letter-spacing: var(--eod-tracking-label);
  color: var(--eod-alert);
  vertical-align: 0.15em;
}


/* ---------------------------------------------------------------------------
   TAG INSPECTOR  [?tags]
   Styling only; behaviour is in site.js. Inert unless the page is opened with
   ?tags or the keyboard shortcut is used.
   ------------------------------------------------------------------------ */

body.eod-tags-on [data-eid] {
  outline: 1px dashed var(--eod-accent);
  outline-offset: 2px;
  position: relative;
}
body.eod-tags-on [data-eid]::after {
  content: attr(data-eid);
  position: absolute;
  top: 0;
  left: 0;
  z-index: 9999;
  padding: 1px 4px;
  background: var(--eod-accent);
  color: var(--eod-ground);
  font: 600 10px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.04em;
  pointer-events: none;
  /* Tags are lowercase. Without this, a label on a .eod-btn inherits the
     button's uppercase transform and reads as a different tag than the one
     in TAGS.md. */
  text-transform: none;
}
body.eod-tags-on [data-eid]:hover { outline-color: var(--eod-accent-bright); }

.eod-tagbar {
  position: fixed;
  bottom: var(--eod-s2);
  left: 50%;
  transform: translateX(-50%);
  z-index: 10000;
  padding: var(--eod-s1) var(--eod-s2);
  background: var(--eod-surface-raise);
  border: 1px solid var(--eod-accent);
  border-radius: var(--eod-radius);
  color: var(--eod-text);
  font-size: var(--eod-size-small);
  box-shadow: 0 8px 24px rgba(var(--eod-shadow-rgb), 0.6);
}


/* ---------------------------------------------------------------------------
   SPOTLIGHT  [?focus=<eid>]
   Dims the page and raises one element, for reviewing copy a section at a
   time. Behaviour is in site.js. Inert unless ?focus is present.
   ------------------------------------------------------------------------ */

.eod-spotlight-backdrop {
  position: fixed;
  inset: 0;
  z-index: 9990;
  background: rgba(var(--eod-shadow-rgb), 0.78);
  /* Clicks pass straight through. The backdrop used to swallow them and
     dismiss itself, which meant one stray click while reading killed the
     spotlight with no way back short of a reload. Escape is the way out. */
  pointer-events: none;
}

.eod-spotlight-target {
  position: relative;
  z-index: 9991;
  /* The backdrop sits behind this, so the target needs its own ground or the
     dimming shows through the gaps between lines of text. */
  background: var(--eod-ground);
  outline: 2px solid var(--eod-accent-bright);
  outline-offset: 10px;
  border-radius: var(--eod-radius);
  box-shadow: 0 0 0 100vmax rgba(var(--eod-shadow-rgb), 0.05),
              0 18px 50px rgba(var(--eod-shadow-rgb), 0.6);
}
/* Sections already carry a band colour; keep it rather than flattening. */
.eod-band .eod-spotlight-target,
.eod-spotlight-target.eod-band { background: var(--eod-surface); }

.eod-spotlight-label {
  position: fixed;
  left: 50%;
  bottom: var(--eod-s3);
  transform: translateX(-50%);
  z-index: 9992;
  max-width: min(90vw, 60ch);
  padding: var(--eod-s2) var(--eod-s3);
  background: var(--eod-surface-raise);
  border: 1px solid var(--eod-accent);
  border-radius: var(--eod-radius);
  color: var(--eod-text);
  font-size: var(--eod-size-small);
  text-align: center;
  box-shadow: 0 12px 32px rgba(var(--eod-shadow-rgb), 0.7);
}


/* ---------------------------------------------------------------------------
   REDUCED MOTION

   Nothing needed here. theme.css already caps transition-duration and
   animation-duration to 0.01ms with !important on *, *::before and *::after
   under prefers-reduced-motion, which covers every transition added in this
   file, including the hamburger-to-X morph and the filter button states.

   The one thing that rule cannot reach is motion driven from JavaScript.
   site.js checks the same media query before creating the IntersectionObserver
   and reveals everything in final position instead; if you add scripted
   motion later, guard it the same way.
   ------------------------------------------------------------------------ */
