@import "./tokens.css";

/* ============================================================
   Reset
   ============================================================ */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

/* Set while the consultation dialog is open. showModal() makes the page behind
   inert to clicks and to the tab order, but it does not stop it scrolling, so a
   trackpad gesture over the backdrop still slides the page under the panel.
   scrollbar-gutter keeps the layout from jumping sideways as the bar goes. */
html.has-modal {
  overflow: hidden;
  scrollbar-gutter: stable;
}

/* Clearance for the fixed mobile CTA bar. Without it the bar sits on top of the
   last thing on the page, which is the footer's legal row — privacy policy and
   terms became untappable at every phone width. The breakpoint is repeated from
   MobileCtaBar.astro because a @media condition cannot read a custom property. */
@media (max-width: 47.9375rem) {
  body {
    padding-bottom: calc(var(--mobile-bar-h) + env(safe-area-inset-bottom, 0px));
  }
}

body {
  margin: 0;
  background: var(--c-taupe);
  color: var(--c-ink-soft);
  font-family: var(--font-body);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Wide content must scroll inside its own container, never the page. */
  overflow-x: hidden;
}

h1,
h2,
h3,
h4,
h5,
h6 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--fw-light);
  line-height: var(--lh-heading);
  color: var(--c-ink);
  text-wrap: balance;
  /* Uppercase display type contains genuinely long unbreakable words
     ("TRANSFORMED.", "TRANSFORMATIONS", "DECLUTTERING"). Any one of them can
     exceed a 360px viewport. hyphens:auto breaks them at a legitimate point
     with a visible hyphen; break-word is the last-resort net so a heading can
     never push the page into horizontal overflow. */
  overflow-wrap: break-word;
  hyphens: auto;
}

p,
ul,
ol,
dl,
figure,
blockquote {
  margin: 0;
}

img,
picture,
video,
svg {
  display: block;
  max-width: 100%;
  height: auto;
}

a {
  color: inherit;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

/* Emphasis is 600, everywhere. Left alone, the browser default is 700, which put
   a weight into the page that the type system does not define and that no font
   file is loaded for, so it was being synthesised. */
strong,
b {
  font-weight: var(--fw-semibold);
}

:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 3px;
}

/* Visible only to screen readers. */
.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;
}

.skip-link {
  position: absolute;
  top: -100%;
  left: var(--s-4);
  z-index: var(--z-modal);
  padding: var(--s-3) var(--s-5);
  background: var(--c-navy);
  color: var(--c-on-dark);
  font-family: var(--font-display);
  font-weight: var(--fw-medium);
  font-size: var(--t-micro);
  letter-spacing: var(--ls-button);
  text-transform: uppercase;
  text-decoration: none;
}

.skip-link:focus {
  top: var(--s-4);
}

/* ============================================================
   Layout primitives
   ============================================================ */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.container--narrow {
  max-width: var(--container-narrow);
}

.container--wide {
  max-width: var(--container-wide);
}

.section {
  padding-block: var(--section-y);
}

/* Cream surfaces are light enough to carry softer secondary tones and still
   clear WCAG AA, so muted text relaxes here. Declared as variables rather than
   as per-component overrides so any element inside a cream surface inherits it.
   Values solved against #ede8e4 / #f5f2f0: 4.60:1 and 4.53:1 respectively. */
.section--cream,
.section--cream-light,
.hero__panel,
.phero__panel,
.ab__heroPanel,
.card,
.qf,
/* These three paint a cream ground of their own and were missing from the list,
   so their copy stayed on the taupe ink while the section around them relaxed.
   Found by walking the rendered pages for cream backgrounds whose text did not
   match — see `node wordpress/tools/preview.mjs --check`. */
.pr__card,
.pr__aside,
.eeat__who,
.bento__cell {
  --c-ink-muted: #6f6661;
  --c-taupe-deeper: #78655a;
  /* Reading copy relaxes here too — rgb(111,102,97), 4.60:1 on #ede8e4 and
     5.03:1 on #f5f2f0. This is the half of Doug's spec that could not live in
     tokens.css, because the same value is 2.64:1 on the taupe ground and would
     have failed AA across roughly half the body copy on the site.

     Deliberately the SAME value as --c-ink-muted above: on cream the softer
     tone is simply what body copy looks like, and the primary/secondary
     distinction only has room to exist on taupe, where body must hold 4.5:1
     and muted cannot go lighter without breaking it. */
  --c-ink-soft: #6f6661;

  /* Restating `color` here is not redundant, and leaving it out is a trap worth
     naming: redefining a custom property only reaches elements that THEMSELVES
     declare color: var(--c-ink-soft). Anything that merely inherits its color
     — a bare <p>, li.sg__card, li.steps__item, li.spaces__item — inherits the
     value already computed on `body` at :root scope, which is the taupe ink.
     Without this line 25 elements sat at #484441 on a cream ground while their
     siblings that did declare the token sat at #6f6661, which is a worse
     mismatch than the one this change set out to fix.
     .section--dark below has always done exactly this, for the same reason. */
  color: var(--c-ink-soft);
}

.section--cream {
  background: var(--c-cream);
}

.section--cream-light {
  background: var(--c-cream-light);
}

.section--taupe {
  background: var(--c-taupe);
}

.section--dark {
  background: var(--c-black);
  color: var(--c-on-dark-muted);
}

.section--dark :where(h1, h2, h3, h4) {
  color: var(--c-on-dark);
}

/* ============================================================
   Type utilities
   ============================================================ */

.display {
  font-size: var(--t-hero);
  line-height: var(--lh-display);
  letter-spacing: var(--ls-display);
  text-transform: uppercase;
}

.h1 {
  font-size: var(--t-h1);
  letter-spacing: var(--ls-display);
  text-transform: uppercase;
}

.h2 {
  font-size: var(--t-h2);
  letter-spacing: var(--ls-display);
  text-transform: uppercase;
}

.h3 {
  font-size: var(--t-h3);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.lead {
  font-size: var(--t-lead);
  color: var(--c-ink-muted);
  text-wrap: pretty;
}

/* Long-form body copy. Used on service and city pages. */
.prose > * + * {
  margin-top: var(--s-5);
}

.prose p,
.prose li {
  text-wrap: pretty;
  /* Long unbreakable tokens appear in body copy — notably the contact address
     hello@messybunsorganizedspaces.com, which alone exceeded a 320px viewport
     on the legal pages. */
  overflow-wrap: break-word;
}

.prose :where(h2) {
  margin-top: var(--s-8);
  font-size: var(--t-h2);
  letter-spacing: var(--ls-display);
  text-transform: uppercase;
}

.prose :where(h3) {
  margin-top: var(--s-7);
  font-size: var(--t-h3);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.prose :where(ul, ol) {
  padding-left: var(--s-5);
}

.prose :where(li) + :where(li) {
  margin-top: var(--s-2);
}

.prose :where(a) {
  color: var(--c-navy);
  font-weight: var(--fw-semibold);
}

.prose :where(strong) {
  color: var(--c-ink);
  font-weight: var(--fw-semibold);
}

/* ============================================================
   Buttons — sharp rectangles, uppercase, letterspaced.
   The pill variant exists only for the nav CTA.
   ============================================================ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: var(--s-4) var(--s-6);
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-family: var(--font-display);
  font-size: var(--t-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-button);
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color var(--dur-base) var(--ease-out),
    color var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.btn:active {
  transform: translateY(1px);
}

/* The one place the action color is declared for the whole site. Every primary
   CTA routes through this class — nav, hero, CTABand, ReviewWall, footer,
   pricing, service and area pages, and both form submits — so the repaint is one
   rule, not thirty. Navy deliberately stays on links, selected states, focus
   rings and progress bars: those say "this is interactive", which is a different
   job from "do this now". */
.btn--primary {
  background: var(--c-action);
  color: var(--c-on-dark);
}

.btn--primary:hover {
  background: var(--c-action-hover);
}

.btn--secondary {
  background: var(--c-taupe-btn);
  color: var(--c-on-dark);
}

.btn--secondary:hover {
  background: var(--c-taupe-btn-hover);
}

.btn--outline {
  background: transparent;
  border-color: var(--c-ink);
  color: var(--c-ink);
}

.btn--outline:hover {
  background: var(--c-ink);
  color: var(--c-on-dark);
}

/* The nav CTA, and the only pill on the site. It used to be a ghost button —
   transparent with a hairline border — which is the weakest CTA treatment there
   is, on the single most-seen CTA on the site. Filled in the action color it
   actually reads as the thing to press. The pill shape stays: it is the one
   deliberate exception to the square-corner rule (see tokens.css). */
.btn--pill {
  border-radius: var(--radius-pill);
  border-color: transparent;
  background: var(--c-action);
  color: var(--c-on-dark);
  padding-inline: var(--s-6);
}

.btn--pill:hover {
  background: var(--c-action-hover);
  color: var(--c-on-dark);
}

.btn--block {
  width: 100%;
}

/* Text link that reads as an action without button weight. */
.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-family: var(--font-display);
  font-size: var(--t-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-button);
  text-transform: uppercase;
  text-decoration: none;
  color: var(--c-navy);
}

.link-arrow::after {
  content: "→";
  transition: transform var(--dur-base) var(--ease-out);
}

.link-arrow:hover::after {
  transform: translateX(4px);
}

/* ============================================================
   Cards and grids
   ============================================================ */

.card {
  background: var(--c-cream);
  padding: var(--s-6);
  border-radius: var(--radius);
}

.card--outline {
  background: transparent;
  border: var(--border-hairline);
}

/* Grid and flex items default to min-width:auto, so a track sizes itself to the
   longest unbreakable word inside it and pushes the page into horizontal
   overflow. Note that overflow-wrap:break-word does NOT prevent this — it lets
   text break once the box is constrained, but it does not reduce the item's
   intrinsic min-content contribution. min-width:0 is what actually constrains
   the box. This bit the hero panels at 360px via "TRANSFORMATIONS". */
.grid > *,
.hero__grid > *,
.phero__grid > *,
.ab__heroGrid > *,
.owner__grid > *,
.owner__copy > *,
.svc__grid > *,
.svc__split > *,
.city__grid > *,
.ct__grid > *,
.pr__split > *,
.ab__split > * {
  min-width: 0;
}

.grid {
  display: grid;
  gap: var(--s-5);
}

.grid--2 {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
}

.grid--3 {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}

.grid--4 {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
}

/* ============================================================
   Scroll reveal — opt-in, and a no-op under reduced motion.
   ============================================================ */

.reveal {
  opacity: 0;
  transform: translateY(20px);
  transition:
    opacity var(--dur-reveal) var(--ease-out),
    transform var(--dur-reveal) var(--ease-out);
}

.reveal.is-visible {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .reveal {
    opacity: 1;
    transform: none;
  }
}

/* ============================================================
   Motion
   ------------------------------------------------------------
   Rules this obeys, because "animation" is usually where a fast
   site goes to die:

     1. transform and opacity only. Those are composited on the
        GPU. Animating width, height, top, left or margin forces
        layout on every frame; box-shadow and filter force paint.
     2. Zero additional JavaScript. The stagger is CSS nth-child.
        The scroll effects use native scroll-driven animations,
        which run off the main thread, so total blocking time
        stays where it is.
     3. Every effect sits behind prefers-reduced-motion, and the
        scroll-driven ones behind @supports, so a browser without
        them shows the finished state rather than nothing.
   ============================================================ */

/* ---------- staggered reveal ----------
   The reveal itself is already IntersectionObserver-driven. This only offsets
   the transition so a grid arrives as a sequence rather than as one block.
   Free: no extra observers, no extra script, just a delay. */
@media (prefers-reduced-motion: no-preference) {
  :where(.sg__grid, .steps, .spaces, .bento, .city__proj, .eeat__grid, .pf__grid, .tst__grid)
    > *:nth-child(2) {
    transition-delay: 70ms;
  }

  :where(.sg__grid, .steps, .spaces, .bento, .city__proj, .eeat__grid, .pf__grid, .tst__grid)
    > *:nth-child(3) {
    transition-delay: 140ms;
  }

  :where(.sg__grid, .steps, .spaces, .bento, .city__proj, .eeat__grid, .pf__grid, .tst__grid)
    > *:nth-child(4) {
    transition-delay: 210ms;
  }

  :where(.sg__grid, .steps, .spaces, .eeat__grid, .pf__grid) > *:nth-child(5) {
    transition-delay: 280ms;
  }

  :where(.sg__grid, .steps, .spaces, .eeat__grid, .pf__grid) > *:nth-child(6) {
    transition-delay: 350ms;
  }
}

/* ---------- underline draw on action links ----------
   scaleX on a pseudo-element, so it composites. A width transition here would
   relayout the line on every frame. */
.link-arrow {
  position: relative;
}

.link-arrow::before {
  content: "";
  position: absolute;
  left: 0;
  bottom: -3px;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-base) var(--ease-out);
}

.link-arrow:hover::before,
.link-arrow:focus-visible::before {
  transform: scaleX(1);
}

@media (prefers-reduced-motion: reduce) {
  .link-arrow::before {
    transition: none;
  }
}

/* ---------- scroll-driven drift ----------
   Native scroll-driven animation. Runs on the compositor, needs no scroll
   listener, and costs nothing in TBT. Chromium supports it today; Safari and
   Firefox fall back to the static transform, which is the finished state.

   The 1.06 scale exists so the vertical travel never exposes an edge. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* Plain selectors, not :global(). That is Astro scoped-style syntax and is
       invalid in a global stylesheet, so the whole rule was being dropped and the
       drift silently never ran. */
    .hero__media img,
    .phero__media img,
    .sap__map img {
      animation: drift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }

    @keyframes drift {
      from {
        transform: scale(1.06) translateY(-1.4%);
      }
      to {
        transform: scale(1.06) translateY(1.4%);
      }
    }
  }
}

/* Any wide element (table, gallery strip) scrolls itself, not the body. */
.scroll-x {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
