/*
 * Enhancements layered on top of the approved page.
 *
 * styles.css is the signed-off design and is not edited. Everything here is
 * either motion that finishes exactly on the approved layout, or interaction
 * states the approved file does not define (focus, press). Nothing here
 * changes how the page looks at rest — with one decided exception, the
 * "Let's connect" label colour, which failed contrast (see below).
 *
 * Two rules keep that true:
 *
 *  1. Motion uses the individual `translate`, `scale` and `opacity`
 *     properties and `clip-path`, never `transform`. The approved CSS already
 *     uses `transform` (the button hover lift, the founder photo's 44px
 *     offset); `translate` composes with it instead of replacing it.
 *
 *  2. Everything that hides content is inside
 *     `prefers-reduced-motion: no-preference`, and keyed on an attribute that
 *     enhance.js adds. If the script fails, or the reader has asked for less
 *     motion, nothing is ever hidden.
 */

:root {
  --ib-ease: cubic-bezier(.2, .8, .2, 1);
  --ib-ease-soft: cubic-bezier(.25, .6, .3, 1);
  --ib-ease-back: cubic-bezier(.34, 1.5, .64, 1);
  --ib-rise: 22px;
}

/* ======================================================= interaction ==
 * Always on, reduced motion included. These are states, not animation.
 */

/* Keyboard focus. The approved file has none, so keyboard users got the
   browser's default ring, which all but disappears against the navy hero. */
.site-header a:focus-visible,
.menu-toggle:focus-visible,
main a:focus-visible,
footer a:focus-visible {
  outline: 2px solid var(--navy);
  outline-offset: 4px;
}
.hero a:focus-visible,
footer a:focus-visible {
  outline-color: #fff;
}

/* A keyboard user reaching the button gets the same lift as a mouse user
   hovering it, and a press settles it back down. */
.button:focus-visible {
  transform: translateY(-2px);
}
.button:active {
  transform: translateY(0);
  transition-duration: .08s;
}

/* "Let's connect" contrast. On the old sage (#8ba78e) a white label was
   2.62:1, so from 2026-09-24 it was navy. Since 2026-09-30 the fill is the
   logo's green, #57744e, where white is 5.23:1 (hover #445c3d: 7.1:1), so
   the label is white again, set in styles.css. Nothing to override here. */

/* The approved hover colour on the nav, eased rather than snapped. */
.site-header nav a {
  transition: color .25s ease;
}

/* ============================================================ motion == */

@media (prefers-reduced-motion: no-preference) {

  /* ------------------------------------------------ hover responses --
     The arrow leans the way it points. */
  .button > span[aria-hidden] {
    transition: translate .3s var(--ib-ease);
  }
  .button:hover > span[aria-hidden],
  .button:focus-visible > span[aria-hidden] {
    translate: 2px -2px;
  }

  /* Contact rows: the icon answers the hover alongside the approved
     underline. The same transition list carries the reveal pop below. */
  .contact .contact-details a .contact-icon {
    transition:
      scale .7s var(--ib-ease-back),
      opacity .4s ease,
      translate .25s var(--ib-ease);
    transition-delay:
      calc(var(--ib-delay, 0s) + .18s),
      calc(var(--ib-delay, 0s) + .18s),
      0s;
  }
  .contact .contact-details a:hover .contact-icon,
  .contact .contact-details a:focus-visible .contact-icon {
    translate: 0 -2px;
  }

  /* The hero has no motion, by decision (2026-09-26). It had a settling
     zoom on the bridge, a staged entrance for the copy and a scroll drift;
     all three were taken out after review. It is exactly as approved. */

  /* ------------------------------------------------- scroll reveals --
     enhance.js tags elements below the fold with data-ib-reveal, adds
     .ib-in as each scrolls into view, and removes both once the motion
     has finished — so a revealed element is back on nothing but the
     approved styles. */
  [data-ib-reveal] {
    transition:
      opacity .9s var(--ib-ease),
      translate .9s var(--ib-ease),
      clip-path 1.2s var(--ib-ease);
    transition-delay: var(--ib-delay, 0s);
  }

  /* The approved button has its own transition; keep it working through
     the reveal rather than overriding it for a second. */
  .button[data-ib-reveal] {
    transition:
      opacity .9s var(--ib-ease) var(--ib-delay, 0s),
      translate .9s var(--ib-ease) var(--ib-delay, 0s),
      transform .2s,
      background .2s;
  }

  [data-ib-reveal]:not(.ib-in) {
    opacity: 0;
    translate: 0 var(--ib-rise);
  }

  /* Image wipes: a hard curtain, drawn rather than faded. clip-path cannot
     interpolate to `none`, so the revealed state is inset(0) until the
     script clears it. */
  [data-ib-reveal^="wipe"] {
    clip-path: inset(0 0 0 0);
  }
  [data-ib-reveal^="wipe"]:not(.ib-in),
  [data-ib-reveal="line"]:not(.ib-in) {
    opacity: 1;
    translate: none;
  }
  [data-ib-reveal="wipe-right"]:not(.ib-in) {
    clip-path: inset(0 100% 0 0);
  }
  [data-ib-reveal="wipe-up"]:not(.ib-in) {
    clip-path: inset(100% 0 0 0);
  }
  /* Jennifer's portrait opens quicker than the other wipes: at 1.2s, after
     the image had loaded, it read as the picture itself being slow. */
  .jennifer-portrait img[data-ib-reveal] {
    transition: clip-path .7s var(--ib-ease);
    transition-delay: var(--ib-delay, 0s);
  }

  /* Client list: each name and its hairline sweep in from the left behind a
     feathered edge. A hard clip was tried first and sliced through letters
     mid-word, which reads as a rendering fault rather than a reveal.

     The mask is a gradient three times the element's width: opaque, a soft
     ramp, then clear. At position 100% the element shows the clear third;
     at 0% the opaque third; in between the ramp crosses it. */
  [data-ib-reveal="line"] {
    -webkit-mask-image: linear-gradient(90deg, #000 40%, transparent 60%);
            mask-image: linear-gradient(90deg, #000 40%, transparent 60%);
    -webkit-mask-size: 300% 100%;
            mask-size: 300% 100%;
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
    -webkit-mask-position: 0% 0;
            mask-position: 0% 0;
    transition:
      -webkit-mask-position 1.4s var(--ib-ease),
      mask-position 1.4s var(--ib-ease);
    transition-delay: var(--ib-delay, 0s);
  }
  [data-ib-reveal="line"]:not(.ib-in) {
    -webkit-mask-position: 100% 0;
            mask-position: 100% 0;
  }

  /* Contact rows: the icon pops in just after its row arrives. */
  [data-ib-reveal]:not(.ib-in) .contact-icon {
    scale: .4;
    opacity: 0;
  }

  /* --------------------------------------------------- mobile menu --
     The approved menu appears by flipping display, which cannot be
     transitioned. An animation plays on the frame it appears instead. */
  @media (max-width: 900px) {
    .site-header nav.open {
      animation: ib-drop .32s var(--ib-ease);
    }
    .site-header nav.open a {
      animation: ib-rise-short .42s var(--ib-ease) backwards;
    }
    .site-header nav.open a:nth-child(2) { animation-delay: .04s; }
    .site-header nav.open a:nth-child(3) { animation-delay: .08s; }
    .site-header nav.open a:nth-child(4) { animation-delay: .12s; }
    .site-header nav.open a:nth-child(5) { animation-delay: .16s; }
  }
}

@keyframes ib-rise       { from { opacity: 0; translate: 0 var(--ib-rise); } }
@keyframes ib-rise-short { from { opacity: 0; translate: 0 8px; } }
@keyframes ib-fade       { from { opacity: 0; } }
@keyframes ib-drop       { from { opacity: 0; translate: 0 -10px; } }

/* Nothing is ever left hidden on paper. */
@media print {
  [data-ib-reveal] {
    opacity: 1 !important;
    translate: none !important;
    clip-path: none !important;
  }
}
