/*==============================================================================
  Digi Kydo — EDITORIAL LAYER

  WHY THIS FILE EXISTS, AND WHY IT IS NOT IN overrides.css.
  overrides.css states its own contract at the top: "if it moves a pixel, it
  does not belong here." Everything below moves pixels on purpose. Brief
  changed 2026-07-29: the goal is no longer pixel-parity with the old
  WordPress site, it is a better layout for /service/*, /our-services/ and
  /about/. Keeping the two intents in separate files means the parity layer
  stays auditable.

  THE CONSTRAINT THIS LAYER IS DESIGNED AROUND: THERE IS NO PHOTOGRAPHY.
  Six of eight service pages have zero images in content_json. So none of the
  composition below depends on an image existing. The hierarchy is carried by
  type scale, an 8px spacing rhythm, colour-blocked bands and rules — all of
  which render identically whether a photo is present or not. Where a real
  image does exist (2 banners, 3 branding gallery shots, 91 portfolio
  thumbnails, 6 client logos) it is used as reinforcement, never as structure.

  COLOUR RULES ARE MEASURED, NOT ASSUMED. See tokens.css for the full matrix.
  The one that constrains every decision here (value changed 2026-07-29 when
  the owner replaced the green accent with the logo's orange; the RULE did not
  change shape, only its number):
      --secondary-color #f95e1a is 3.16:1 on white.
  So an orange band carries INK text, and bright orange text on white is never
  used. The accent survives as: a surface tint, an on-dark accent, and a
  rule/keyline. Orange TEXT is --secondary-deep / --secondary-deepest.

  Load order: LAST, after storefront.css.
==============================================================================*/

/*------------------------------------------------------------------------------
  0. THE CONTRAST FIX  (ui-ux-pro-max priority 1, `color-contrast`)

  core.css:32 sets `body { color: var(--gray-color) }`, i.e. #8f93a5, which
  measures 3.05:1 on white. That was the site's DEFAULT BODY COLOUR, so every
  paragraph on every page failed WCAG AA. Repointed to #565a69 (6.86:1).

  --gray-color itself is deliberately NOT changed: main.css uses it for ~40
  borders, rules and overlays where it is decoration, not text, and rewriting
  the token would move pixels on pages this brief says not to touch. Only the
  text uses are repointed, here.
------------------------------------------------------------------------------*/
body {
  color: var(--text-body-color);
}

/* REGRESSION I CAUSED AND CAUGHT, documented so it is not reintroduced.
   The line above is right for the ~95% of the site that is light, and WRONG
   for the two dark surfaces, which inherited from `body` too. Measured:
       #565a69 on #1a1a20  =  2.53:1   FAIL   (what the line above did)
       #8f93a5 on #1a1a20  =  5.62:1   PASS   (what it was before, and is now)
   The legacy grey was never the problem on dark — it was only ever the
   problem on white. So the dark surfaces are pinned back to it explicitly. */
.shock-footer,
.shock-footer p,
.shock-footer .text,
.menu-grid,
.menu-grid p,
.slide-info,
.slide-info p {
  color: var(--gray-color);
}

/* .gray / .gray-75 as a TEXT colour. Same reasoning: these two selectors are
   the text-bearing uses (descriptions, article prose, list copy). The
   border/overlay uses of the same tokens are untouched. */
.shock-section .content,
.shock-section .content p,
.shock-section .content li,
.basic-intro .description.gray,
.basic-intro .description.gray p,
p.gray,
li.gray {
  color: var(--text-body-color);
}
/* .gray-75 on body-size text resolves to 2.21:1. Only ever legitimate on the
   oversized italic display strings, which are large-text (3:1). */
.shock-section .content .gray-75:not([class*="text-style-"]) {
  color: var(--text-body-color);
}

/* Tables inside the editorial prose. /our-services/ has a two-column
   "your situation -> start here" table in body_html whose <td>s were 2.85:1. */
.ed-prose table { width: 100%; margin: var(--space-15) 0; border-collapse: collapse; }
.ed-prose th,
.ed-prose td {
  padding: 0.625rem 0.75rem 0.625rem 0;
  border-bottom: 1px solid var(--border-subtle);
  text-align: left;
  vertical-align: top;
  color: var(--text-body-color);
  line-height: 1.5;
}
.ed-prose th { font-family: var(--secondary-font); font-weight: 700; color: var(--black-color); }

/*  HOMEPAGE + SHARED COMPONENTS — COLOUR ONLY, NO LAYOUT.
    The brief says do not touch the homepage, and this does not: there is no
    size, weight, spacing or position change below, only a text colour that
    was failing AA. Three components, all measured on / at 1280 and 375:
      .slide-description .description.gray p   3.05:1 -> 6.86:1
      .slide-action-title                      3.05:1 -> 6.86:1
    The big italic scrolling-text (.text-style-8.gray-75, 35px/200) is LARGE
    text, so its threshold is 3:1 — it measures 2.21:1 and still fails, but it
    is a decorative marquee of technology names that is also duplicated as
    real navigation elsewhere. Raising it would visibly change the homepage's
    signature effect, so it is left alone and FLAGGED rather than silently
    restyled or silently ignored.

    NOTE ON SPECIFICITY: dynamic-slider.min.css declares
    `.dynamic-slider .slide-action-title { color: var(--gray-color) }` at
    0,2,0. A bare `.slide-action-title` (0,1,0) loses to it — verified by the
    fix not landing on the first attempt. The selectors below match the
    vendor's own specificity deliberately. */
.dynamic-slider .slide-description .description.gray p,
.dynamic-slider .slide-description .description.gray,
.dynamic-slider .slide-action-title {
  color: var(--text-body-color);
}

/*------------------------------------------------------------------------------
  1. BANDS — the vertical rhythm and the colour blocking.

  Section spacing tiers are 8px multiples throughout: 96 / 64 / 56 / 40 / 24.
------------------------------------------------------------------------------*/
.ed-band {
  position: relative;
  padding-top: 3.5rem;    /* 56px */
  padding-bottom: 3.5rem;
}
@media (min-width: 992px) {
  .ed-band {
    padding-top: var(--space-6);   /* 96px */
    padding-bottom: var(--space-6);
  }
}
.ed-band--tight { padding-top: 2.5rem; padding-bottom: 2.5rem; }
.ed-band--neutral   { background-color: var(--surface-neutral); }
.ed-band--primary   { background-color: var(--surface-primary); }
.ed-band--secondary { background-color: var(--surface-secondary); }
.ed-band--ink       { background-color: var(--surface-ink); color: #fff; }

/* A hairline that reads as a deliberate keyline rather than a Bootstrap <hr>. */
.ed-band + .ed-band:not([class*="--"]) {
  border-top: 1px solid var(--border-subtle);
}

.ed-inner {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 1.5rem;
}
@media (min-width: 768px) {
  .ed-inner { padding: 0 2rem; }
}

/* Long-form prose measure. ui-ux-pro-max §6 `line-length`: 60-75 chars.
   68ch at 16px Lato lands at ~640px, which is the readable band. Before this,
   article copy ran the full 8-column width (~740px, ~95 chars). */
/* 72ch at 16px Lato is ~660px, i.e. ~72 characters — the top of the 60-75
   band in ui-ux-pro-max §6 `line-length`. The old layout ran ~95 characters. */
.ed-measure { max-width: 72ch; }
.ed-measure--wide { max-width: 78ch; }

/*------------------------------------------------------------------------------
  2. TYPE — the hierarchy that does the work photography would have done.
------------------------------------------------------------------------------*/

/* Eyebrow / kicker. Small, but 700 weight and a real colour, so it reads as a
   label and not as faded text. #2d6076 is 6.89:1 on white, 5.94:1 on the
   tints — it passes AA at this size on every surface used here. */
.ed-eyebrow {
  display: block;
  margin: 0 0 var(--space-15);
  font-family: var(--secondary-font);
  font-size: 0.8125rem;  /* 13px */
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--primary-deep);
}
.ed-band--ink .ed-eyebrow { color: var(--secondary-color); } /* 7.25:1 on ink */
.ed-eyebrow--secondary { color: var(--secondary-deep); }

/* Section heading. Fluid between 28px and 44px, so the step from H1 down is
   obvious at every width without a media-query staircase. */
.ed-h2 {
  margin: 0 0 var(--space-15);
  font-family: var(--secondary-font);
  font-size: clamp(1.75rem, 1.25rem + 2vw, 2.75rem);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--black-color);
}
.ed-band--ink .ed-h2 { color: #fff; }

/* The lede. One size up from body, ink-dark, capped measure. This is the
   single biggest reason the old page read as a grey wall: the opening
   paragraphs were the same 16px #8f93a5 as everything else. */
.ed-lede {
  margin: 0 0 var(--space-2);
  max-width: 60ch;
  font-size: clamp(1.0625rem, 1rem + 0.35vw, 1.3125rem);
  line-height: 1.55;
  color: var(--black-color);
}
.ed-band--ink .ed-lede { color: rgba(255, 255, 255, 0.92); }

/* The intro block promotes its FIRST paragraph to lede size. Doing it by
   position rather than by asking the content agent to add a class means it
   works on all eight service pages today, and on any new one tomorrow. */
.ed-lede-block > p:first-child {
  max-width: 60ch;
  font-size: clamp(1.0625rem, 1rem + 0.35vw, 1.3125rem);
  line-height: 1.55;
  color: var(--black-color);
}

/* Article prose inside the editorial bands. */
/* Vertical rhythm is deliberately tighter than the default editorial instinct.
   These articles run 1,200-1,700 words; at 4rem between H2s the /seo/ detail
   band measured 4,552px on its own. Measured back down to ~3,600px at these
   values without the copy feeling cramped — paragraph leading is untouched. */
.ed-prose > * + * { margin-top: 1rem; }
.ed-prose p,
.ed-prose li { color: var(--text-body-color); line-height: 1.6; }
.ed-prose h2 {
  margin: var(--space-3) 0 var(--space-1);
  font-family: var(--secondary-font);
  font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2rem);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.015em;
  color: var(--black-color);
}
/* A short green keyline above each H2 — a section marker that costs no image
   and survives at 375px. */
.ed-prose h2::before {
  content: "";
  display: block;
  width: 48px;
  height: 4px;
  margin-bottom: var(--space-15);
  background-color: var(--secondary-color);
  border-radius: 2px;
}
.ed-prose h3 {
  margin: var(--space-15) 0 var(--space-05);
  font-family: var(--secondary-font);
  font-size: 1.1875rem;  /* 19px */
  font-weight: 700;
  line-height: 1.3;
  color: var(--black-color);
}
.ed-prose ul { margin: var(--space-1) 0; padding-left: 1.25rem; }
.ed-prose li { margin-bottom: var(--space-05); }
.ed-prose strong { color: var(--black-color); }
.ed-prose a {
  color: var(--primary-deep);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
.ed-prose a:hover { text-decoration-thickness: 2px; }

/*------------------------------------------------------------------------------
  3. POINT CARDS — service_points.bullet_points rendered as cards.

  Was: a flat <ul> of Font Awesome check icons in 16px grey, dumped after the
  whole article. The bullets are the most scannable content on the page and
  they were the least visible thing on it.

  The check icon is kept (it is the only icon data that exists) but is drawn
  as a token-coloured SVG-free glyph chip rather than inline text, so the card
  has a fixed visual anchor at a consistent size.
------------------------------------------------------------------------------*/
.ed-points {
  display: grid;
  gap: var(--space-1);
  grid-template-columns: 1fr;
  margin: 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 640px) { .ed-points { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 992px) { .ed-points { grid-template-columns: repeat(3, 1fr); gap: var(--space-15); } }

.ed-point {
  display: flex;
  gap: var(--space-1);
  align-items: flex-start;
  padding: var(--space-15);
  background-color: #fff;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg, 12px);
  /* The left edge is the only colour on the card, so the grid reads as a set
     without six competing filled blocks. */
  border-left: 3px solid var(--secondary-color);
  box-shadow: 0 1px 2px rgba(26, 26, 32, 0.04);
  /* transform/opacity only — never width/height (ui-ux-pro-max §7). */
  transition: transform var(--duration-fast) ease-out,
              box-shadow var(--duration-fast) ease-out;
}
.ed-point:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-small);
}
.ed-point__icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-full);
  background-color: var(--surface-secondary);
  color: var(--secondary-deep);  /* 6.77:1 on the green tint */
  font-size: 0.875rem;
}
.ed-point__text {
  margin: 0;
  font-size: 1rem;   /* never below 16px — iOS auto-zooms smaller body text */
  line-height: 1.55;
  color: var(--black-color);
}

/* Mobile density. At 375px the eleven point cards on /service/seo/ measured
   1,381px stacked one-up. Trimming the chrome (not the type — body stays at
   16px) brings that down without making the cards feel cramped. */
@media (max-width: 639px) {
  .ed-point { padding: var(--space-1); gap: 0.75rem; }
  .ed-point__icon { width: 28px; height: 28px; font-size: 0.8125rem; }
}

/* The bullet group's own heading + its closing line. */
.ed-points-group + .ed-points-group { margin-top: var(--space-4); }
.ed-points-group__head { margin-bottom: var(--space-2); }
.ed-points-group__cta {
  margin: var(--space-2) 0 0;
  font-family: var(--secondary-font);
  font-size: 1.0625rem;
  font-weight: 700;
  color: var(--primary-deep);
}

/*------------------------------------------------------------------------------
  4. PROCESS STEPS — service_steps, which the old template rendered NOWHERE.

  Three rows of real copy per page were sitting unused in content_json. A
  numbered process is the classic no-photography agency module: the numeral is
  the graphic.
------------------------------------------------------------------------------*/
.ed-steps {
  display: grid;
  gap: var(--space-2);
  grid-template-columns: 1fr;
  /* MEASURED BUG, not a preference: without this the shorter step was
     vertically centred in its grid row (step 02 sat 8px below 01 and 03 at
     1280px, so the numerals and titles did not line up across the three
     columns). `start` top-aligns them. */
  align-items: start;
  margin: 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 768px) { .ed-steps { grid-template-columns: repeat(3, 1fr); gap: var(--space-3); } }

.ed-step { position: relative; padding-top: var(--space-2); }
/* The connecting rule between steps. Decorative, so it is hidden from AT and
   dropped entirely on mobile where the steps stack. */
.ed-step::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background-color: var(--border-subtle);
}
.ed-step:first-child::before { background-color: var(--secondary-color); }

.ed-step__num {
  display: block;
  margin-bottom: var(--space-05);
  font-family: var(--secondary-font);
  font-size: clamp(2.5rem, 2rem + 2vw, 3.5rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.03em;
  /* Outlined numeral: the theme's own .text-outline idiom, reused so the
     module looks native to the brand rather than bolted on. Falls back to a
     solid tinted numeral where -webkit-text-stroke is unsupported. */
  color: var(--surface-primary);
  -webkit-text-stroke: 2px var(--primary-deep);
}
/* Browsers without -webkit-text-stroke (it is unprefixed nowhere) would render
   the numeral in the near-white fill and it would vanish. Paint it solid there. */
@supports not (-webkit-text-stroke: 2px black) {
  .ed-step__num { color: var(--primary-deep); }
}
.ed-step__title {
  margin: 0 0 var(--space-05);
  font-family: var(--secondary-font);
  font-size: 1.1875rem;
  font-weight: 700;
  line-height: 1.3;
  color: var(--black-color);
}
.ed-step__body { margin: 0; color: var(--text-body-color); line-height: 1.6; }

/*------------------------------------------------------------------------------
  5. FAQ ACCORDION — native <details>/<summary>.

  Was: Bootstrap collapse with `.show` + aria-expanded="true" on every row, so
  all six answers were open and the "accordion" was just more wall. Native
  <details> is keyboard-operable and screen-reader-announced with zero JS,
  which also means zero new console-error surface.

  Touch target: the <summary> row is min-height 56px (ui-ux-pro-max §2 needs
  >=44px).
------------------------------------------------------------------------------*/
.ed-faq { border-top: 1px solid var(--border-subtle); }
.ed-faq__item { border-bottom: 1px solid var(--border-subtle); }

.ed-faq__q {
  display: flex;
  gap: var(--space-1);
  align-items: center;
  justify-content: space-between;
  min-height: 56px;
  padding: var(--space-15) 0;
  cursor: pointer;
  list-style: none;
  font-family: var(--secondary-font);
  font-size: 1.0625rem;
  font-weight: 700;
  line-height: 1.4;
  color: var(--black-color);
  transition: color var(--duration-fast) ease-out;
}
.ed-faq__q::-webkit-details-marker { display: none; }
.ed-faq__q:hover { color: var(--primary-deep); }
.ed-faq__q:focus-visible {
  outline: 2px solid var(--primary-color);
  outline-offset: 2px;
}

/* The +/- indicator. Built from two bars so it can rotate on open without
   animating layout, and it is not conveying anything by colour alone — the
   open/closed state is also exposed natively by <details>. */
.ed-faq__sign {
  position: relative;
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
}
.ed-faq__sign::before,
.ed-faq__sign::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 14px;
  height: 2px;
  background-color: var(--primary-deep);
  transform: translate(-50%, -50%);
  transition: transform var(--duration-fast) ease-out, opacity var(--duration-fast) ease-out;
}
.ed-faq__sign::after { transform: translate(-50%, -50%) rotate(90deg); }
.ed-faq__item[open] .ed-faq__sign::after {
  transform: translate(-50%, -50%) rotate(0deg);
  opacity: 0;
}

.ed-faq__a {
  padding: 0 0 var(--space-15);
  max-width: 68ch;
  color: var(--text-body-color);
  line-height: 1.65;
}
.ed-faq__item[open] .ed-faq__a { animation: ed-reveal var(--duration-base) ease-out; }
@keyframes ed-reveal {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

/*------------------------------------------------------------------------------
  6. PRIMARY CTA  (ui-ux-pro-max §4 `primary-action`: ONE per screen)

  The old page had no button at all — the two conversion points were inline
  body links in the same colour as the surrounding prose. Every page now ends
  on one ink band with exactly one filled button. Secondary routes (phone,
  email) sit under it as plainly subordinate text links.
------------------------------------------------------------------------------*/
.ed-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.625rem;
  min-height: 52px;           /* >= 44px touch target */
  padding: 0.875rem 2rem;
  border: 2px solid transparent;
  border-radius: var(--radius-full);
  font-family: var(--secondary-font);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  touch-action: manipulation;
  transition: transform var(--duration-fast) ease-out,
              background-color var(--duration-fast) ease-out,
              color var(--duration-fast) ease-out;
}
/* Filled green + INK label. NOT white-on-green: that pair is 2.39:1 and is the
   single most tempting mistake in this palette. Ink on #60b988 is 7.25:1. */
.ed-btn--primary {
  background-color: var(--secondary-color);
  color: var(--black-color);
}
.ed-btn--primary:hover,
.ed-btn--primary:focus-visible {
  background-color: #7ac79c;
  color: var(--black-color);
  transform: translateY(-2px);
}
.ed-btn--ghost {
  border-color: currentColor;
  background-color: transparent;
  color: var(--black-color);
}
.ed-band--ink .ed-btn--ghost { color: #fff; }
.ed-btn--ghost:hover { transform: translateY(-2px); }
.ed-btn:focus-visible { outline: 3px solid var(--primary-color); outline-offset: 3px; }

.ed-cta__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  align-items: center;
  margin-top: var(--space-2);
}
.ed-cta__link {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
  color: rgba(255, 255, 255, 0.85);   /* 12.5:1 on ink */
  text-decoration: underline;
  text-underline-offset: 4px;
}
.ed-cta__link:hover { color: var(--secondary-color); }

/*------------------------------------------------------------------------------
  7. PROOF — portfolio slice + client logos.

  The only genuinely photographic assets this site owns (91 portfolio
  thumbnails, 6 client logos). Everything here is lazy-loaded; the strip sits
  well below the fold on every page that uses it.
------------------------------------------------------------------------------*/
.ed-proof {
  display: grid;
  gap: var(--space-1);
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 640px) { .ed-proof { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 992px) { .ed-proof { grid-template-columns: repeat(6, 1fr); } }

/* object-fit is `contain`, NOT `cover`, and this is not a style preference.
   The 91 "portfolio thumbnails" are brand LOGOS, not photographs — verified by
   eye at 1280px: with `cover` the strip rendered "HEAL(TH)", "Ang(el)" and
   "(Exp)ert" with their wordmarks sliced off at the frame edge. Cropping a
   client's logo is worse than showing it small. `contain` on a white card with
   padding is the treatment a logo asset actually wants. */
.ed-proof__item {
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  padding: var(--space-1);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background-color: #fff;
  aspect-ratio: 1 / 1;   /* reserves space — CLS (ui-ux-pro-max §3) */
  transition: border-color var(--duration-fast) ease-out,
              box-shadow var(--duration-fast) ease-out;
}
.ed-proof__item:hover {
  border-color: var(--secondary-color);
  box-shadow: var(--shadow-small);
}
.ed-proof__item img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  transition: transform var(--duration-slow) ease-out;
}
.ed-proof__item:hover img { transform: scale(1.04); }

.ed-proof__more { margin: var(--space-2) 0 0; }

/* The 3-shot first_gallery that only /service/branding/ has. Sits inline in
   the article, 3-up, and simply never renders on the other seven pages. */
.ed-proof--gallery {
  margin: var(--space-3) 0;
  grid-template-columns: repeat(3, 1fr);
}
.ed-proof--gallery .ed-proof__item {
  aspect-ratio: 3 / 2;
  cursor: default;
  padding: 0;
}
/* These three ARE photographs, unlike the logo thumbnails above, so they take
   `cover`. With the inherited `contain` they rendered 161x107 inside a 217px
   card, floating in white margins. Verified by eye on /service/branding/.
   SEPARATE ISSUE, FLAGGED NOT FIXED: the three images are generic stock
   photos of people in a cafe, not examples of Digi Kydo branding work. That
   is a content decision for the owner, not a CSS one. */
.ed-proof--gallery .ed-proof__item img {
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  object-fit: cover;
}

.ed-logos__title {
  margin: var(--space-3) 0 var(--space-1);
  font-family: var(--secondary-font);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--black-color);
}
.ed-logos {
  display: grid;
  gap: var(--space-1);
  grid-template-columns: repeat(3, 1fr);
  align-items: center;
}
@media (min-width: 768px) { .ed-logos { grid-template-columns: repeat(6, 1fr); } }
.ed-logos__item {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 64px;
  padding: var(--space-1);
}
.ed-logos__item img {
  max-width: 100%;
  max-height: 48px;
  width: auto;
  height: auto;
  object-fit: contain;
}

/*------------------------------------------------------------------------------
  8. SIDE RAIL — the "at a glance" / related-services rail.

  Replaces the old right column, which held a "Tag cloud" heading and ~200px
  of chips against ~6,000px of article, leaving a third of the page empty on
  desktop.
------------------------------------------------------------------------------*/
.ed-split {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: 1fr;
}
@media (min-width: 992px) {
  .ed-split { grid-template-columns: minmax(0, 1fr) 300px; gap: var(--space-5); }
}
/* No rail (e.g. /our-services/, whose eight service links are already the card
   grid at the top of the page, so a rail would just repeat them). Without this
   the 72ch column sat hard left in a 1076px band and left ~400px of dead space
   down the right of the whole article — observed at 1280px. */
.ed-split--single { grid-template-columns: minmax(0, 1fr); }
.ed-split--single > .ed-measure { margin-inline: auto; }
.ed-rail { position: sticky; top: var(--space-2); align-self: start; }
@media (max-width: 991px) { .ed-rail { position: static; } }

.ed-rail__card {
  padding: var(--space-2);
  background-color: var(--surface-neutral);
  border-radius: var(--radius-lg, 12px);
  border: 1px solid var(--border-subtle);
}
.ed-rail__title {
  margin: 0 0 var(--space-1);
  font-family: var(--secondary-font);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--black-color);
}
.ed-rail__list { margin: 0; padding: 0; list-style: none; }
.ed-rail__list li { border-top: 1px solid var(--border-subtle); }
.ed-rail__list li:first-child { border-top: 0; }
.ed-rail__list a {
  display: flex;
  align-items: center;
  min-height: 44px;          /* touch target */
  padding: var(--space-05) 0;
  color: var(--text-body-color);
  text-decoration: none;
  transition: color var(--duration-fast) ease-out;
}
.ed-rail__list a:hover { color: var(--primary-deep); text-decoration: underline; }

/*------------------------------------------------------------------------------
  9. INTRO MEDIA — used only where a real image exists (2 of 8 service pages).
  The layout must not collapse when it is absent, so the image lives in an
  optional grid cell, not in a background or a fixed-height hero.
------------------------------------------------------------------------------*/
.ed-intro { display: grid; gap: var(--space-3); grid-template-columns: 1fr; }
@media (min-width: 992px) {
  .ed-intro--with-media { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: var(--space-5); align-items: center; }
}
.ed-intro__media { margin: 0; }
.ed-intro__media img,
.ed-intro__media picture {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-lg, 12px);
}
.ed-intro__media img { aspect-ratio: 4 / 3; object-fit: cover; }

/*------------------------------------------------------------------------------
  10. REDUCED MOTION. overrides.css §5 already zeroes durations globally; this
  adds the two animations introduced in this file.
------------------------------------------------------------------------------*/
@media (prefers-reduced-motion: reduce) {
  .ed-point,
  .ed-btn,
  .ed-proof__item img,
  .ed-faq__sign::before,
  .ed-faq__sign::after {
    transition: none !important;
  }
  .ed-point:hover,
  .ed-btn:hover,
  .ed-proof__item:hover img { transform: none !important; }
  .ed-faq__item[open] .ed-faq__a { animation: none !important; }
}

/*------------------------------------------------------------------------------
  11. FIXED ENQUIRY RAIL  (owner request, 2026-07-29)

  Two always-visible actions — Call and Email — on every public page. Markup in
  resources/views/partials/enquiry-rail.blade.php; values come from Settings.

  MOTION IS TRANSFORM-ONLY (ui-ux-pro-max §7). The collapsed/expanded states are
  one translateX on the rail, not an animated width or max-width, so expanding
  costs no layout. The label column is a fixed 8.5rem so both actions travel the
  same distance and the two icons stay vertically aligned mid-animation.

  COLOUR, measured (see tokens.css):
    Call  : ink text on --secondary-color #f95e1a          5.48:1   AA
    Email : white text on --surface-ink #1a1a20           17.32:1   AAA
    Email hover/focus becomes the orange fill with ink text, i.e. it converges
    on the Call treatment rather than inventing a third state colour.
  Exactly one of the two is a filled brand action (Call), so the pair still
  reads as one primary + one subordinate (ui-ux-pro-max §4).

  STACKING: z-index 12. Above page content and above .side-widget (11), and
  deliberately BELOW .menu-grid (60), .menu-toggle (70) and the Bootstrap modal
  (1055) — so it can never cover the hamburger, the open off-canvas menu, or a
  dialog.
------------------------------------------------------------------------------*/
.eq-rail {
  --eq-label-w: 8.5rem;
  --eq-size: 56px;
  position: fixed;
  z-index: 12;
  top: 50%;
  right: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* Parked so only the --eq-size icon block is on screen. */
  transform: translate3d(var(--eq-label-w), -50%, 0);
  transition: transform var(--duration-base) var(--ease-emphasis);
}
.eq-rail:hover,
.eq-rail:focus-within {
  transform: translate3d(0, -50%, 0);
}

.eq-rail__item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: calc(var(--eq-size) + var(--eq-label-w));
  min-height: var(--eq-size);
  padding-right: 1rem;
  background-color: var(--surface-ink);
  color: #fff;              /* 17.32:1 on ink */
  text-decoration: none;
  touch-action: manipulation;
  transition: background-color var(--duration-fast) ease-out,
              color var(--duration-fast) ease-out;
}
.eq-rail__item:first-child { border-top-left-radius: var(--radius-md); }
.eq-rail__item:last-child  { border-bottom-left-radius: var(--radius-md); }

/* The one filled brand action. INK on orange, never white on orange — white on
   #f95e1a is 3.16:1 and is the tempting mistake in this palette. */
.eq-rail__item--primary {
  background-color: var(--secondary-color);
  color: var(--black-color);   /* 5.48:1 */
}
.eq-rail__item:hover,
.eq-rail__item:focus-visible {
  background-color: var(--secondary-color);
  color: var(--black-color);
}
.eq-rail__item:focus-visible {
  outline: 3px solid var(--primary-color);
  outline-offset: -3px;   /* inset: the rail is flush to the viewport edge */
}

.eq-rail__icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--eq-size);
  height: var(--eq-size);   /* 56x56 touch target on its own */
}
.eq-rail__icon svg { display: block; }

.eq-rail__label {
  flex: 0 0 auto;
  font-family: var(--secondary-font);
  font-size: 0.9375rem;   /* 15px */
  font-weight: 700;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

/*  BELOW 1280px THE RAIL BECOMES A BOTTOM BAR. THIS BREAKPOINT IS MEASURED,
    NOT GUESSED, AND MY FIRST ATTEMPT AT IT WAS WRONG.

    I first set it at 768px. Then I swept every page at 768 / 992 / 1024 /
    1152 / 1280, scrolling through each one and testing the rail's 56px
    collapsed strip against PAINTED TEXT RUNS (Range.getClientRects, not
    element boxes — a <p> box reaches the column edge even when its glyphs do
    not) and against every interactive control. Results:

      768px   overlapped body copy, portfolio thumbnails, .full-link cards
              AND a form <input> on /contact/
      992px   overlapped portfolio/gallery thumbnail links and /about/ copy
      1024px  overlapped thumbnail links and /service/branding/ copy
      1152px  overlapped thumbnail links and /service/branding/ copy
      1280px  CLEAR on all 12 page types at every scroll position

    The arithmetic agrees: the container is 1140px, so the content's right
    edge sits at (viewport - 1140)/2 + 1140. A 56px rail flush to the viewport
    edge only clears it from 1252px up. 1280 is the next standard breakpoint.

    So the right-edge rail is a >=1280px treatment and everything below it —
    phone AND tablet — gets a two-up bottom bar with both labels permanently
    visible. It is a fixed bar, so the page needs matching bottom clearance:
    that is added to .shock-footer below, not to <body>, because a body
    padding would show as a gap under the ink footer. */
@media (max-width: 1279.98px) {
  .eq-rail {
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    flex-direction: row;
    gap: 0;
    transform: none;
    padding-bottom: env(safe-area-inset-bottom, 0px);
    background-color: var(--surface-ink);
    box-shadow: 0 -2px 12px rgba(26, 26, 32, 0.25);
  }
  .eq-rail:hover,
  .eq-rail:focus-within { transform: none; }
  .eq-rail__item {
    flex: 1 1 50%;
    width: auto;
    min-height: 56px;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem;
    border-radius: 0 !important;
  }
  .eq-rail__icon { width: 24px; height: 24px; }
  .eq-rail__icon svg { width: 20px; height: 20px; }
  .eq-rail__label { font-size: 1rem; }
  /* CLEAR THE BAR — and this is the SECOND attempt, the first was wrong.
     I first put the clearance on `.shock-footer .bottom-bar`. Measured at
     375 and 414 on /blog/: the copyright line was still covered by 35px,
     because .bottom-bar centres its content, so a 72px padding-bottom only
     moves the text up ~36px. Putting the padding on .shock-footer instead
     adds the space BELOW the bottom bar, and because the footer is ink and
     the enquiry bar is ink, the join is seamless. Re-measured: clear at
     375 / 414 / 768 / 1024 / 1152 on /blog/, /contact/ and /. */
  .shock-footer {
    padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px));
  }
  /* And lift the back-to-top widget off it so neither covers the other. */
  .side-widget.to-right {
    height: calc(100% - 56px - env(safe-area-inset-bottom, 0px));
  }
}

@media (prefers-reduced-motion: reduce) {
  .eq-rail,
  .eq-rail__item { transition: none !important; }
}

/*------------------------------------------------------------------------------
  12. THE ORANGE REBRAND — the handful of places that needed more than a token
  swap, because a value that was legible against green is not against orange.
------------------------------------------------------------------------------*/

/* The filled button's hover. Was a lighter green; now a lighter orange, which
   also RAISES the ink contrast (5.48 -> 6.35) rather than lowering it. */
.ed-btn--primary {
  background-color: var(--secondary-color);
  color: var(--black-color);          /* 5.48:1 */
}
.ed-btn--primary:hover,
.ed-btn--primary:focus-visible {
  background-color: var(--secondary-hover-color);   /* 6.35:1 with ink */
  color: var(--black-color);
}

/* Orange text on a light surface uses the DEEPEST variant, not --secondary-deep.
   On the orange tint --secondary-deep is 4.83:1 — a pass, but close enough to
   the 4.5 line that any future tint tweak would break it. Deepest is 6.42:1. */
.ed-eyebrow--secondary { color: var(--secondary-deepest); }
.ed-point__icon { color: var(--secondary-deepest); }   /* 6.42:1 on the tint */

/* THE BADGE. Pre-existing AA failure, surfaced by the audit and fixed here.
   Three states, all measured:

     resting outline badge   #8f93a5 on #fff       3.05:1  FAIL  -> #565a69, 6.86
     .active (accent fill)   #fff   on #60b988     2.39:1  FAIL  -> ink,     5.48
     :hover  (accent fill)   #fff   on #f95e1a     3.16:1  FAIL  -> ink,     5.48

   I CAUSED AND CAUGHT A REGRESSION HERE. My first pass repointed
   `.badge .badge-text.gray` to body colour without excluding the FILLED
   states, which put #565a69 on the orange fill at 2.17:1 — worse than the
   white it replaced. Measured on /portfolio/a-to-z/ and /<post>/, where the
   active chip is `.badge.outline.active`, not `.badge.secondary`. Hence the
   explicit .active / :hover selectors below and the :not() on the grey rule.
   Do not simplify this back into one selector. */
.badge.secondary .badge-text,
.badge.accent .badge-text,
.badge.active .badge-text,
.badge:hover .badge-text,
.link:hover .badge .badge-text {
  color: var(--black-color) !important;
}
.tag-cloud .badge:not(.active):not(:hover) .badge-text.gray,
.badge:not(.active):not(:hover) .badge-text.gray {
  color: var(--text-body-color);
}
/* ...except inside the ink footer, where the grey IS the accessible choice
   (#8f93a5 on #1a1a20 is 5.62:1 and #565a69 would be 2.53:1). */
.shock-footer .badge:not(.active):not(:hover) .badge-text.gray { color: var(--gray-color); }

/*------------------------------------------------------------------------------
  13. THE REMAINING PAGES  (2026-07-29 — /contact/, /gallery/, /blog/, posts,
  /portfolio/ detail, taxonomy archives, 404).

  Same system as §1-9. Nothing below introduces a new colour, a new type step
  or a new spacing value — it composes the tokens already locked in tokens.css.
------------------------------------------------------------------------------*/

/* The article <h1>. Only pages/post uses it: everywhere else the H1 is the
   .text-style-2 lockup. Sized to sit clearly above .ed-h2 without reaching the
   lockup's 83px. */
.ed-h1 {
  margin: 0 0 var(--space-1);
  font-family: var(--secondary-font);
  font-size: clamp(2rem, 1.4rem + 2.6vw, 3rem);
  font-weight: 800;
  line-height: 1.12;
  letter-spacing: -0.025em;
  color: var(--black-color);
}

/*  CONTACT — three tappable actions. Not decoration: these are the only
    above-the-fold routes to the phone and the inbox on the page whose entire
    job is starting a conversation. Values come from Settings. */
.ed-contact {
  display: grid;
  gap: var(--space-1);
  grid-template-columns: 1fr;
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 768px) { .ed-contact { grid-template-columns: repeat(3, 1fr); } }

.ed-contact__card {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  height: 100%;
  min-height: 96px;          /* well over the 44px touch minimum */
  padding: var(--space-15);
  background-color: #fff;
  border: 1px solid var(--border-subtle);
  border-left: 3px solid var(--secondary-color);
  border-radius: var(--radius-lg, 12px);
  text-decoration: none;
  transition: transform var(--duration-fast) ease-out,
              box-shadow var(--duration-fast) ease-out;
}
.ed-contact__card:hover { transform: translateY(-2px); box-shadow: var(--shadow-small); }
.ed-contact__card:focus-visible { outline: 3px solid var(--primary-color); outline-offset: 3px; }
.ed-contact__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  margin-bottom: var(--space-05);
  border-radius: var(--radius-full);
  background-color: var(--surface-secondary);
  color: var(--secondary-deepest);   /* 6.42:1 on the tint */
}
.ed-contact__icon svg { display: block; }
.ed-contact__label {
  font-family: var(--secondary-font);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-body-color);     /* 6.86:1 */
}
.ed-contact__value {
  font-size: 1.0625rem;
  font-weight: 700;
  line-height: 1.4;
  color: var(--black-color);         /* 17.32:1 */
  overflow-wrap: anywhere;
}

/*  CONTACT — the form inside an editorial band. The field markup is the
    theme's, untouched, so only the things that were failing are repointed:
    the floating <label> was #8f93a5 (3.05:1, an AA failure on every field on
    the page) and the input underline was the legacy grey. */
.ed-form { margin-top: var(--space-2); }
.ed-form .form-label,
.ed-form .form-check-label { color: var(--text-body-color); }
.ed-form .form-control {
  color: var(--black-color);
  border-bottom-color: var(--gray-50-color);
}
.ed-form .form-control:focus {
  border-bottom-color: var(--secondary-deep);
  box-shadow: none;
}
.ed-form .form-control:focus-visible {
  outline: 3px solid var(--primary-color);
  outline-offset: 2px;
}
.ed-form .form-check { min-height: 44px; display: flex; align-items: center; gap: 0.5rem; }
.ed-form .form-check-input { margin-top: 0; }
.ed-form .sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

.ed-rail__address {
  margin: 0 0 var(--space-1);
  font-style: normal;
  line-height: 1.6;
  color: var(--text-body-color);
}
.ed-rail__address strong { color: var(--black-color); }

/*  BLOG / ARCHIVE — the card grid. `align-items: stretch` plus a flex card
    body is what makes three cards with different image and title lengths line
    their meta rows up; the old Bootstrap columns did not. */
.ed-posts {
  display: grid;
  gap: var(--space-15);
  grid-template-columns: 1fr;
  align-items: stretch;
  margin-top: var(--space-2);
}
@media (min-width: 640px) { .ed-posts { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 992px) { .ed-posts { grid-template-columns: repeat(3, 1fr); gap: var(--space-2); } }

/* Overrides the older .post-card block in overrides.css §6, which this
   supersedes. Kept as a separate selector rather than editing that file
   because overrides.css is the parity layer and must stay pixel-honest. */
.post-card.ed-post {
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  background-color: #fff;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg, 12px);
  box-shadow: 0 1px 2px rgba(26, 26, 32, 0.04);
  transition: transform var(--duration-fast) ease-out,
              box-shadow var(--duration-fast) ease-out;
}
.post-card.ed-post:hover { transform: translateY(-3px); box-shadow: var(--shadow-small); }

/* aspect-ratio reserves the frame before the image lands — CLS. */
.ed-post__media {
  display: block;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  background-color: var(--surface-neutral);
  border-bottom: 1px solid var(--border-subtle);
}
.ed-post__media img,
.ed-post__media picture {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--duration-slow) ease-out;
}
.post-card.ed-post:hover .ed-post__media img { transform: scale(1.04); }

.ed-post__body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  padding: var(--space-15);
}
.post-card.ed-post .post-meta,
.ed-band .post-meta {
  margin: 0 0 var(--space-05);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-body-color);   /* was #8f93a5, 3.05:1 */
}
.ed-post__topic { color: var(--secondary-deepest); }  /* 7.11:1 on white */
.post-card.ed-post .post-title {
  margin: 0 0 var(--space-05);
  font-family: var(--secondary-font);
  font-size: 1.125rem;
  font-weight: 700;
  line-height: 1.3;
}
.post-card.ed-post .post-title a {
  color: var(--black-color);
  text-decoration: none;
}
.post-card.ed-post .post-title a:hover { text-decoration: underline; text-underline-offset: 3px; }
.post-card.ed-post .post-title a:focus-visible { outline: 3px solid var(--primary-color); outline-offset: 3px; }
.post-card.ed-post .post-excerpt {
  margin: 0;
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--text-body-color);   /* was #8f93a5, 3.05:1 */
}

/*  ARTICLE / CASE STUDY HERO. Full band width, capped so a 1,600px source does
    not become a 1,140px-tall slab, and aspect-ratio'd so it reserves space. */
.ed-article__hero { margin: var(--space-2) 0 0; }
.ed-article__hero img,
.ed-article__hero picture {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-lg, 12px);
}
.ed-article__hero img {
  aspect-ratio: 16 / 9;
  object-fit: cover;
  max-height: 520px;
}

/*  PROJECT / PAGE GALLERY. Used by the portfolio detail band and, through
    .ed-tree, by whatever content-tree renders on /gallery/. */
.ed-gallery,
.ed-tree .gallery.has-gap {
  display: grid;
  gap: var(--space-1);
  grid-template-columns: repeat(2, 1fr);
  margin-top: var(--space-2);
}
@media (min-width: 640px) { .ed-gallery, .ed-tree .gallery.has-gap { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 992px) { .ed-gallery, .ed-tree .gallery.has-gap { grid-template-columns: repeat(4, 1fr); } }

.ed-gallery__item,
.ed-tree .gallery.has-gap > * {
  display: block;
  overflow: hidden;
  aspect-ratio: 1 / 1;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background-color: #fff;
}
.ed-gallery__item img,
.ed-gallery__item picture,
.ed-tree .gallery.has-gap img,
.ed-tree .gallery.has-gap picture {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--duration-slow) ease-out;
}
.ed-gallery__item:hover { border-color: var(--secondary-color); }
.ed-gallery__item:hover img { transform: scale(1.04); }
.ed-gallery__item:focus-visible { outline: 3px solid var(--primary-color); outline-offset: 3px; }

/* content-tree emits Bootstrap columns inside .row wrappers; inside .ed-tree
   the grid above owns the layout, so the column widths are neutralised. */
.ed-tree .gallery.has-gap > [class*="col-"] { width: auto; max-width: none; padding: 0; flex: none; }
.ed-tree .row { --bs-gutter-x: 0; }

/*  404 — the recovery routes as real buttons rather than 13.6px grey chips. */
.ed-linklist {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
}
.ed-linklist .ed-btn { min-height: 48px; padding: 0.75rem 1.5rem; }

/*  Pagination inside an editorial band. */
.ed-band .pagination-wrapper,
.ed-band nav[aria-label$="pagination"] { margin-top: var(--space-3); }

@media (prefers-reduced-motion: reduce) {
  .ed-contact__card,
  .post-card.ed-post,
  .ed-post__media img,
  .ed-gallery__item img { transition: none !important; }
  .ed-contact__card:hover,
  .post-card.ed-post:hover { transform: none !important; }
  .post-card.ed-post:hover .ed-post__media img,
  .ed-gallery__item:hover img { transform: none !important; }
}

/*==============================================================================
  13. THE HOMEPAGE EDITORIAL REGION  (added 2026-07-29)

  WHAT THIS REPLACES. Everything below the homepage's `.zzz` divider was
  partials/page-body: `body_html` poured into one `article.content` column
  inside a `col-md-8`. Measured on / at 1280px before the change:

      1,209 rendered words, 9 H2s, 26 H3s, 5,107px tall
      = 57% of the page's 9,020px height, in one grey column occupying the
        left 62% of the viewport with the right 38% empty
      15 links at the Bootstrap default #0d6efd (3.68:1 on white — a FAIL,
        and not a brand colour)

  The owner's objection was not the styling, it was the journey: a homepage
  is scanned, and that was a document. So the region is now four designed
  modules plus the CTA band, and roughly 40% of the copy is no longer
  rendered here because it already exists on /about/ and /our-services/.
  App\Support\HomeBody holds the routing table and the audit trail.

  NO NEW TOKENS. Every colour, radius, duration and space below is an
  existing custom property. The only literal values are grid track counts and
  the two breakpoints, which match the ones already used in §3 and §4.

  CONTRAST, measured against the surfaces each rule actually lands on
  (the matrix is in tokens.css):
      --black-color        on #fff / #f6f7f9 / #fdf1ea   17.32 / 16.24 / 14.92
      --text-body-color    on #fff / #f6f7f9 / #fdf1ea    6.86 /  6.44 /  5.91
      --secondary-deep     on #fff / #f6f7f9              5.36 /  5.03
      --secondary-deepest  on #fdf1ea                             6.33
  So a link on the orange tint uses DEEPEST, per the rule already stated in
  §12. There is no bright-orange text on any light surface anywhere below.
==============================================================================*/

/*------------------------------------------------------------------------------
  13a. THE OPENER — the two paragraphs that used to be the top of the wall.

  Two columns rather than one, because the first paragraph is the positioning
  statement and the second is the client list: they are different kinds of
  sentence and stacking them at the same size is what made them read as a
  document. The client list is set smaller behind a keyline, i.e. as a
  margin note.
------------------------------------------------------------------------------*/
.ed-opener {
  display: grid;
  gap: var(--space-2);
  grid-template-columns: 1fr;
  align-items: start;
}
@media (min-width: 992px) {
  .ed-opener { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); gap: var(--space-4); }
}
.ed-opener__lead > p {
  margin: 0;
  max-width: 30ch;
  font-family: var(--secondary-font);
  font-size: clamp(1.375rem, 1.05rem + 1.4vw, 2rem);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.015em;
  color: var(--black-color);
}
.ed-opener__lead > p + p { margin-top: var(--space-1); }
.ed-opener__note {
  padding-left: var(--space-15);
  border-left: 3px solid var(--secondary-color);
}
.ed-opener__note > p {
  margin: 0;
  font-size: 1rem;      /* never below 16px — iOS auto-zooms smaller body text */
  line-height: 1.6;
  color: var(--text-body-color);
}
.ed-opener__note > p + p { margin-top: var(--space-1); }
@media (max-width: 991px) {
  /* The 30ch cap is a desktop measure; at 375px it would break the statement
     into a ragged 4-word column. */
  .ed-opener__lead > p { max-width: none; }
}

/*------------------------------------------------------------------------------
  13b. THE SERVICE GRID.

  The eight service blocks were `h3 + p + bare blue link` in a prose column.
  They are the most clickable thing on the homepage and were the least
  visible. They are now the SAME `.card.double-edge.has-icon` tile
  /our-services/ uses — same component, same icons, same "VIEW MORE"
  affordance — deliberately not a second card pattern.

  The card component needs no Bootstrap column (it is `width:100%` + flex
  column in main.css), so it is dropped straight into a CSS grid and the
  gutters come from `gap`.

  NINE CELLS, NOT EIGHT. Eight tiles in a three-up grid leaves a two-card
  orphan row. The section's own closing link — "See all services", already in
  the copy — takes the ninth cell as a tinted tile, so the desktop grid is a
  clean 3x3 and the link stops being a stray line of text under the grid.
------------------------------------------------------------------------------*/
.ed-svc {
  display: grid;
  gap: var(--space-15);
  grid-template-columns: 1fr;
}
@media (min-width: 640px) { .ed-svc { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 992px) { .ed-svc { grid-template-columns: repeat(3, 1fr); gap: var(--space-2); } }
/* Grid items default to min-width:auto, which lets a long unbroken service
   name push the track wider than its share and overflow the band. */
.ed-svc > * { min-width: 0; }

/* main.css gives .card.double-edge a 2.25rem body. At three-up inside the
   1140px band that leaves a 271px text column, which is the /our-services/
   measure and is right there.

   MOBILE DENSITY IS A MEASURED FIX, NOT A PREFERENCE. Nine stacked tiles at
   the desktop chrome measured the region 6,827px tall at 375px, i.e. 283px
   TALLER than the prose wall it replaced — the copy got shorter but the tiles
   got taller. Trimming the chrome (padding, icon, and main.css's 1.75rem gap
   above VIEW MORE) and not the type — body copy stays at 16px, because below
   that iOS auto-zooms the page on focus — takes it back under. */
@media (max-width: 575px) {
  .ed-svc { gap: var(--space-1); }
  .ed-svc .card.double-edge .card-body { padding: 1.25rem; }
  .ed-svc .card.has-icon .title { margin-top: 0.625rem; }
  .ed-svc .card .title { margin-bottom: 0.5rem; }
  .ed-svc .card .card-image-icon { height: 38px; }
  .ed-svc .card .button-wrapper { margin-top: 0; padding-top: var(--space-1); }
}

/* .card is display:flex/column in main.css but .card-body is not a flex
   parent, so `margin-top:auto` on the button wrapper had nothing to push
   against and VIEW MORE sat directly under the copy. Observed at 1280px: the
   three baselines in the top row landed at 466 / 479 / 466px. */
.ed-svc .card .card-body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  align-items: flex-start;
}
.ed-svc .card .card-body > .description { width: 100%; }
.ed-svc .card .description p {
  margin: 0;
  font-size: 1rem;
  line-height: 1.55;
  color: var(--text-body-color);
}
/* The rotated second edge is drawn by an absolutely positioned pseudo-element
   with no layout box, so a card whose text is short leaves its neighbours
   taller. Stretch is the grid default; this states it because the visual
   depends on it. */
.ed-svc .card { height: 100%; }
.ed-svc .card .button-wrapper { margin-top: auto; padding-top: var(--space-15); }
/* The whole tile is the target (main.css .full-link), so the focus ring has to
   be drawn on the card, not on a zero-size anchor. */
.ed-svc .card:focus-within { outline: 3px solid var(--primary-color); outline-offset: 6px; }

.ed-svc__more {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-05);
  min-height: 100%;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  background-color: var(--surface-secondary);
  color: var(--black-color);          /* 14.92:1 on the tint */
  text-decoration: none;
  transition: transform var(--duration-fast) ease-out,
              box-shadow var(--duration-fast) ease-out;
}
/* The ninth cell only lands cleanly in the three-up grid. In the two-up
   range it is an orphan half-tile at the bottom left — observed at 768px — so
   it takes the whole row and reads as the section's closing action instead. */
@media (min-width: 576px) and (max-width: 991px) {
  .ed-svc__more { grid-column: 1 / -1; min-height: 0; padding: var(--space-15) var(--space-2); }
}
.ed-svc__more:hover { transform: translateY(-2px); box-shadow: var(--shadow-small); color: var(--black-color); }
.ed-svc__more:focus-visible { outline: 3px solid var(--primary-color); outline-offset: 3px; }
.ed-svc__more-title {
  font-family: var(--secondary-font);
  font-size: 1.1875rem;   /* 19px, the .card .title size */
  font-weight: 700;
  line-height: 1.3;
}
.ed-svc__more-title::after {
  content: " \2192";
  color: var(--secondary-deepest);    /* 6.33:1 on the tint */
}

/*------------------------------------------------------------------------------
  13c. CARDS — the self-select router and the reasons grid.

  ONE component with a flat modifier, not two, because the two modules differ
  in job (one routes, one reassures) and should not differ in vocabulary.

  `.ed-card__link` swallows the sentence stem the copy already had ("Start
  with SEO."), so the accessible name is "Start with SEO" rather than "SEO"
  and no word of the copy is dropped to make room for a button.
------------------------------------------------------------------------------*/
.ed-cards {
  display: grid;
  gap: var(--space-15);
  grid-template-columns: 1fr;
  align-items: stretch;
  margin: 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 768px) { .ed-cards { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1200px) { .ed-cards--4 { grid-template-columns: repeat(4, 1fr); gap: var(--space-2); } }
.ed-cards > * { min-width: 0; }

.ed-card {
  display: flex;
  flex-direction: column;
  padding: var(--space-2);
  background-color: #fff;
  border: 1px solid var(--border-subtle);
  border-left: 3px solid var(--secondary-color);
  border-radius: var(--radius-md);
  box-shadow: 0 1px 2px rgba(26, 26, 32, 0.04);
  transition: transform var(--duration-fast) ease-out,
              box-shadow var(--duration-fast) ease-out;
}
.ed-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-small); }

/* The reasons grid. No card chrome — four filled boxes next to the router's
   four filled boxes would read as eight of the same thing. A top keyline and
   the type carry it instead, which is the same device §4 uses for steps. */
.ed-card--flat {
  padding: var(--space-15) 0 0;
  background-color: transparent;
  border: 0;
  border-top: 2px solid var(--border-subtle);
  border-radius: 0;
  box-shadow: none;
}
.ed-card--flat:first-child { border-top-color: var(--secondary-color); }
.ed-card--flat:hover { transform: none; box-shadow: none; }

.ed-card__title {
  margin: 0 0 var(--space-05);
  font-family: var(--secondary-font);
  font-size: 1.1875rem;   /* 19px */
  font-weight: 700;
  line-height: 1.3;
  color: var(--black-color);
}
.ed-card__body { margin: 0; font-size: 1rem; line-height: 1.6; color: var(--text-body-color); }
.ed-card__body + .ed-card__body { margin-top: var(--space-1); }

/* Block + fit-content, NOT inline-flex. As a flex row the arrow was a
   separate flex item, so when the label wrapped ("Start with digital
   marketing / strategy" at 768px) the arrow was left stranded on its own line.
   As a block with an nbsp-glued ::after it stays welded to the last word. */
.ed-card__link {
  display: block;
  width: fit-content;
  max-width: 100%;
  min-height: 44px;       /* touch target */
  margin-top: auto;
  padding-top: var(--space-1);
  line-height: 1.4;
  font-family: var(--secondary-font);
  font-size: 1rem;
  font-weight: 700;
  color: var(--secondary-deep);       /* 5.36:1 on white, 5.03:1 on #f6f7f9 */
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
}
.ed-card__link::after { content: "\00a0\2192"; }
.ed-card__link:hover { color: var(--secondary-deepest); text-decoration-thickness: 2px; }
.ed-card__link:focus-visible { outline: 3px solid var(--primary-color); outline-offset: 3px; }
/* On the orange tint --secondary-deep is 4.83:1 — a pass, but §12 already
   ruled that too close to the line, so the tint gets the deepest variant. */
.ed-band--secondary .ed-card__link { color: var(--secondary-deepest); }

/* The line that closes the router ("If none of those is you, ring us…").
   Section-level, not card-level — see the structural rule in HomeBody. */
.ed-cards__outro {
  margin: var(--space-2) 0 0;
  max-width: 60ch;
  font-size: 1.0625rem;
  line-height: 1.6;
  color: var(--black-color);
}

@media (prefers-reduced-motion: reduce) {
  .ed-card,
  .ed-svc__more { transition: none !important; }
  .ed-card:hover,
  .ed-svc__more:hover { transform: none !important; }
}

/* ── Reveal safety net ────────────────────────────────────────────────
   The homepage service grid shipped invisible: cards carried .aos-animate
   yet computed opacity stayed 0, with every ancestor at opacity 1. The
   vendor rule matched and did not apply. Content visibility must never
   depend on an animation library succeeding, so the editorial bands opt
   out of AOS entirely. Motion here was decoration; the section is not. */
.ed-svc > [data-aos],
.ed-cards > [data-aos],
.ed-band [data-aos] {
  opacity: 1 !important;
  transform: none !important;
}
