/* ==========================================================================
   SECTIONS - hero, stats, video helix, community, brands, work, contact
   ========================================================================== */

/* ============================== HERO ============================== */
.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: clamp(120px, 16vh, 180px) clamp(70px, 10vh, 110px);
}

.hero__inner {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--pad);
  display: grid;
  /* The right column is sized, not a fraction. It used to be 1.1fr against the
     copy's 0.9fr, which was right when it held a five-card fan that wanted all
     the room it could get. It holds one 400px picture now, so a fraction just
     left dead space beside the photo and squeezed the headline into a column
     too narrow for it - which is what had "People" sitting alone on its own
     line. Give the picture exactly what it draws at and the copy takes the
     rest. */
  grid-template-columns: minmax(0, 1fr) clamp(240px, 30vw, 400px);
  align-items: center;
  gap: clamp(28px, 4vw, 64px);
}

.hero__title {
  font-size: clamp(2.15rem, 3.4vw, 3.5rem);
  margin-bottom: 22px;
  /* Evens the lines out instead of filling each one to the brim and dropping
     whatever is left onto the last. Without it this headline breaks after
     "product" and again after "People", leaving a one-word line in the middle
     of a three-line block. */
  text-wrap: balance;
}

.hero__body {
  max-width: 46ch;
  color: var(--ink-2);
  font-size: clamp(1rem, 0.45vw + 0.92rem, 1.16rem);
  text-wrap: pretty;
}

.hero__cta {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 32px;
}

.scroll-hint {
  position: absolute;
  bottom: 26px;
  left: 50%;
  translate: -50% 0;
  width: 26px;
  height: 42px;
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--line);
  display: grid;
  justify-items: center;
  padding-top: 9px;
}
.scroll-hint span {
  width: 3px; height: 7px;
  border-radius: 2px;
  background: var(--ink-3);
  animation: scroll-tick 2s var(--ease) infinite;
}
@keyframes scroll-tick {
  0%, 100% { transform: translateY(0);    opacity: 0; }
  30%      { opacity: 1; }
  70%      { transform: translateY(13px); opacity: 0; }
}

@media (max-width: 980px) {
  /* 168px at the top used to be clearance for the eyebrow, which was absolutely
     positioned above the copy. That is gone, so this is now only clearing the
     floating nav. */
  .hero { min-height: auto; padding-block: 124px 40px; }
  .hero__inner { grid-template-columns: 1fr; }
  .hero__copy { text-align: center; }
  .hero__body { margin-inline: auto; }
  .hero__cta { justify-content: center; }
  .scroll-hint { display: none; }

  /* One column stacks the portrait under the copy, because that is the order
     it is written in and the desktop layout wants it on the right. Above the
     headline is where it belongs on a phone, so pull it back up and drop it
     well below the desktop size.

     Not all the way back to the 64px it was, though. It shared the hero with
     five floating cards then and only had to be an avatar; it is the only
     image in the hero now, and at 64px it read as a stray dot above a very
     large headline. */
  .portrait {
    order: -1;
    width: clamp(96px, 26vw, 132px);
    margin: 0 auto 30px;
  }
}

/* ============================== STATS ============================== */
.stats { padding-block: clamp(10px, 3vw, 30px); }

.stats__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(10px, 1.4vw, 18px);
}

.stat {
  padding: clamp(18px, 2.2vw, 30px);
  border-radius: var(--r-lg);
  text-align: center;
}
.stat__value {
  font-size: clamp(1.6rem, 3vw, 2.5rem);
  font-weight: 600;
  letter-spacing: -0.04em;
  line-height: 1;
  background: linear-gradient(180deg, #fff, rgba(255, 255, 255, 0.62));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.stat__label {
  margin-top: 9px;
  font-size: 0.8rem;
  color: var(--ink-3);
  letter-spacing: -0.005em;
}

@media (max-width: 720px) {
  .stats__grid { grid-template-columns: repeat(2, 1fr); }
}

/* ============================== SECTION HEADS ============================== */
.section__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 28px;
  margin-bottom: clamp(36px, 5vw, 64px);
}
.section__head--center {
  justify-content: center;
  text-align: center;
}
.section__head--center .eyebrow { justify-content: center; }
.section__head--center .section__lede { margin-inline: auto; }

.section__foot {
  display: flex;
  justify-content: center;
  margin-top: clamp(40px, 5vw, 64px);
}

@media (max-width: 820px) {
  .section__head { flex-direction: column; align-items: flex-start; }
  .section__head--center { align-items: center; }
}

/* Filter chips */
.chips {
  display: inline-flex;
  gap: 5px;
  padding: 5px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.05);
  box-shadow: inset 0 0 0 1px var(--line);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  flex: none;
}
.chip {
  padding: 9px 18px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-3);
  font-size: 0.86rem;
  font-weight: 500;
  cursor: pointer;
  transition: color 0.25s var(--ease), background-color 0.25s var(--ease);
}
.chip:hover { color: var(--ink-2); }
.chip.is-active {
  color: #000;
  background: #fff;
  box-shadow: 0 4px 14px -6px rgba(255, 255, 255, 0.6);
}

/* ============================== VIDEO HELIX ==============================
   A spine down the middle of the section with the videos orbiting it. See
   assets/js/helix.js for the mechanism; this file only sets the room up.

   .helix        a tall empty box. Its height is the length of the ride, and
                 helix.js writes it, because it depends on the card size.
   .helix__stage sticks to the viewport while that box scrolls past, so the
                 cards pass through a frame that is holding still.
   .helix__orbit the 3D space itself. Everything in it is placed by transform.
   ------------------------------------------------------------------------ */
.helix {
  position: relative;
  min-height: 60vh;          /* only until helix.js has measured */
}

.helix__stage {
  position: sticky;
  top: 0;
  height: 100svh;
  /* Full bleed. The swing wants the whole window; boxed into the text column
     it runs out of room to throw the cards apart long before it looks wide. */
  width: 100vw;
  margin-inline: calc(50% - 50vw);
}

/* The camera sits here rather than on the stage, and deliberately WITHOUT
   `transform-style: preserve-3d`.

   With preserve-3d the cards share one 3D space, and two tilted planes that
   overlap do what real planes do - they slice through each other, and you get
   a hard seam across both. Flattening means each card is projected through the
   same camera (so the perspective is still one consistent room) but then drawn
   as a flat picture, which can never intersect its neighbour. The cost is that
   the browser stops sorting them by depth, so helix.js sets z-index instead. */
.helix__orbit {
  position: absolute;
  inset: 0;
  perspective: 1500px;
  perspective-origin: 50% 48%;
}

/* The axis the cards turn around. It sits one orbit radius back (helix.js
   writes the translateZ), so every card really does pass in front of it.

   Two parts: a soft column of light wide enough to be seen around the cards,
   and the hairline itself down the middle of it. */
.helix__spine {
  position: absolute;
  left: 50%;
  top: -12%;
  width: 300px;
  height: 124%;
  /* Centring is the `translate` property, not `transform`, so helix.js can own
     `transform` outright and push the spine back to the axis. */
  translate: -50%;
  z-index: 1;                /* the cards all orbit in front of the axis */
  background: radial-gradient(closest-side at 50% 50%,
    rgba(255, 154, 77, 0.13) 0%,
    rgba(255, 154, 77, 0.05) 45%,
    transparent 76%);
  pointer-events: none;
}

.helix__spine::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 0;
  width: 1px;
  height: 100%;
  transform: translateX(-50%);
  background: linear-gradient(
    to bottom,
    transparent 0%,
    rgba(255, 154, 77, 0.08) 12%,
    rgba(255, 214, 175, 0.66) 50%,
    rgba(255, 154, 77, 0.08) 88%,
    transparent 100%);
  box-shadow: 0 0 28px 3px rgba(255, 154, 77, 0.24);
}

/* ---- the two particle layers --------------------------------------------
   Shared `.dust` styling lives in base.css - it is used site-wide. This is
   only what the video section does differently: brighter than the ambient
   one, and drifted against the scroll by helix.js writing --dust once a
   frame, which is what ties it to the ride rather than leaving it floating
   on top of it. */
.dust--helix {
  inset: -20% 0;
  transform: translate3d(0, var(--dust, 0px), 0);
}
.helix.is-riding .dust--helix { will-change: transform; }

/* No grid here. The site-wide one is already showing through behind this, and
   a second one on top of it lands off-register and reads as a moire rather
   than as one grid. The dot fields are what this layer is for. */
.dust--helix .dust__grid { display: none; }
.dust--helix .dust__field { opacity: 0.55; }
.dust--helix .dust__field--far { opacity: 0.4; }

/* The swarm. Unlike the dust it is not on a keyframe at all - helix.js puts
   every particle on the same orbit as the cards and redraws the lot each
   frame, so they go round the spine with them and part around the cursor.

   Two canvases because one can only be either in front of the cards or behind
   them: particles sweeping the near side of the spine are drawn into the front
   one and cross over the cards, everything turned away goes in the back. */
.helix__swarm {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.helix__swarm--back { z-index: 1; }
.helix__swarm--front { z-index: 3000; }
.helix--flat .helix__swarm { display: none; }

/* ---- a card on the orbit ---- */
.helix .card3d {
  position: absolute;
  left: 50%;
  top: 50%;
  /* Real width comes from helix.js, sized off the stage rather than the
     window - past the point the layout stops growing, so should the grid. */
  width: 30%;
  opacity: 0;                /* helix.js owns opacity from the first frame */
  animation: none;           /* ...so the shared card entrance must not run */
  /* Only so the hover lift has depth to move in. The orbit above is flat, so
     this space stops at the card and cannot reach a neighbour. */
  transform-style: preserve-3d;
}

/* One compositor layer per card is worth it while the section is on screen,
   and worth giving back the moment it is not. */
.helix.is-riding .card3d { will-change: transform, opacity; }

/* Only the card you are actually looking at is a click target - the ones
   mid-turn are half-transparent and would be a lottery to hit. */
.helix .card3d { pointer-events: none; }
.helix .card3d.is-live { pointer-events: auto; }

/* --near runs 1 at the front of the orbit to 0 once a card has turned away.

   Once it has turned away it stops being a card and reads as just its picture:
   the caption, the pane behind it and the lit rim all go together. Fading only
   the caption leaves a slab of empty dark chrome hanging under the thumbnail,
   which looks like a card that failed to load. */
.helix .card3d__meta { opacity: var(--near, 1); }
.helix .card3d__face::before,
.helix .card3d__face::after { opacity: var(--near, 1); }

/* --cs is the card's own size in the broken grid. The caption tracks it, so a
   small card reads as a small card rather than as a caption with a picture on
   top of it. */
.helix .card3d__meta { padding: calc(12px * var(--cs, 1)) calc(8px * var(--cs, 1)) calc(5px * var(--cs, 1)); }
.helix .card3d__title { font-size: calc(0.9rem * var(--cs, 1)); }
.helix .card3d__sub { font-size: calc(0.76rem * var(--cs, 1)); }

.helix .card3d__face {
  background: linear-gradient(158deg,
    rgba(255, 255, 255, calc(0.062 * var(--near, 1))) 0%,
    rgba(255, 255, 255, calc(0.022 * var(--near, 1))) 46%,
    rgba(255, 255, 255, calc(0.040 * var(--near, 1))) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, calc(0.16 * var(--near, 1))),
    inset 0 -1px 0 rgba(255, 255, 255, calc(0.045 * var(--near, 1))),
    0 40px 90px -50px rgba(0, 0, 0, 0.9),
    0 0 calc(60px * var(--near, 0)) calc(-24px * var(--near, 0)) rgba(255, 154, 77, 0.28);
}

/* Nothing here is thinned for a phone any more: below 760px the section is the
   flat grid below, so the spine, the count and both dust layers are already
   `display: none` and there is no orbit left to make cheaper. */

/* Full bleed pulls the stage out of the page's side padding, so the section's
   own padding has to come back or the cards sit under the header above. */
.helix { margin-block: clamp(10px, 2vw, 26px); }

/* ---- the flat path -----------------------------------------------------
   Reduced motion and every handheld get the same cards as an ordinary grid:
   no sticky, no orbit, no rAF, and the section collapses to its natural
   height. helix.js decides; this is the whole of what the decision costs. */
.helix--flat { height: auto !important; min-height: 0; }
.helix--flat .helix__stage {
  position: static;
  height: auto;
  perspective: none;
  /* The orbit is full bleed because the swing wants the whole window. A grid
     does not, and left full bleed its cards break out of the page's side
     padding and run to the screen edge. */
  width: auto;
  margin-inline: 0;
}
.helix--flat .helix__orbit {
  position: static;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(clamp(240px, 30vw, 340px), 1fr));
  gap: clamp(16px, 2.4vw, 32px);
  transform-style: flat;
}
.helix--flat .helix__spine,
.helix--flat .dust,
.helix--flat .card3d {
  position: static;
  width: auto;
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
.helix--flat .card3d__meta { opacity: 1; }

/* ============================== COMMUNITY ============================== */
.community {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(300px, 0.65fr);
  gap: clamp(18px, 2.4vw, 34px);
  align-items: start;
}

.perks {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(12px, 1.5vw, 20px);
}

.perk {
  padding: clamp(20px, 2.2vw, 28px);
  border-radius: var(--r-lg);
}
.perk h3 {
  font-size: 1.02rem;
  font-weight: 580;
  margin-bottom: 8px;
  letter-spacing: -0.02em;
}
.perk p { font-size: 0.9rem; color: var(--ink-2); line-height: 1.55; }

.pricing {
  padding: clamp(26px, 3vw, 38px);
  border-radius: var(--r-xl);
  text-align: center;
  position: sticky;
  top: 100px;
}
.pricing__badge {
  display: inline-block;
  padding: 6px 14px;
  border-radius: 999px;
  font-size: 0.74rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--accent-1);
  background: rgba(255, 154, 77, 0.09);
  box-shadow: inset 0 0 0 1px rgba(255, 154, 77, 0.22);
}
.pricing__name {
  margin-top: 18px;
  font-size: 1.18rem;
  font-weight: 580;
}
.pricing__amount {
  margin-top: 14px;
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 6px;
}
.pricing__num {
  font-size: clamp(3rem, 5vw, 4rem);
  font-weight: 600;
  letter-spacing: -0.05em;
  line-height: 1;
  background: var(--accent-grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.pricing__per { color: var(--ink-3); font-size: 1rem; }
.pricing__note {
  margin: 16px auto 24px;
  max-width: 30ch;
  font-size: 0.84rem;
  color: var(--ink-2);
  line-height: 1.55;
}
.pricing__fine { margin-top: 14px; font-size: 0.76rem; color: var(--ink-4); }

@media (max-width: 980px) {
  .community { grid-template-columns: 1fr; }
  .pricing { position: static; }
}
@media (max-width: 620px) {
  .perks { grid-template-columns: 1fr; }
}

/* ============================== BRAND MARQUEE ============================== */
.marquee__label {
  text-align: center;
  font-size: 0.78rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-4);
  margin-bottom: 34px;
}

.marquee {
  position: relative;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
}

.marquee__track {
  display: flex;
  align-items: center;
  gap: clamp(38px, 6vw, 84px);
  width: max-content;
  animation: marquee 46s linear infinite;
}
.marquee:hover .marquee__track { animation-play-state: paused; }

/* Few enough brands to show at once - hold them still and centre them. */
.marquee--static {
  -webkit-mask-image: none;
          mask-image: none;
  padding-inline: var(--pad);
}
.marquee--static .marquee__track {
  animation: none;
  width: auto;
  max-width: var(--wrap);
  margin-inline: auto;
  flex-wrap: wrap;
  justify-content: center;
  row-gap: 30px;
}

@keyframes marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* Logos are inline SVG using fill: currentColor, so colour and hover are just
   `color` and `opacity` - no filters, no per-file colour variants. */
.brand {
  /* One number sizes the whole logo: the mark's height comes straight off it
     and the wordmark beside it is a fraction of it, so the two never drift out
     of proportion with each other. Was a flat 24px, which held its own against
     five hero cards but reads as a footnote now they are gone. */
  --brand-h: clamp(28px, 2.5vw, 36px);
  flex: none;
  display: flex;
  align-items: center;
  gap: 0.42em;
  color: #fff;
  opacity: 0.5;
  transition: opacity 0.35s var(--ease);
}
.brand:hover,
.brand:focus-visible { opacity: 1; }

/* These four logos have wildly different aspect ratios, so matching their raw
   heights makes some read far heavier than others. --logo-scale is per-brand
   optical balancing, set in brand-logos.js. */
.brand__mark {
  display: block;
  height: calc(var(--brand-h) * var(--logo-scale, 1));
}
.brand__mark svg {
  display: block;
  height: 100%;
  width: auto;
}

/* Wordmark beside a symbol-only logo, and the fallback when a brand has no
   logo entry at all. */
.brand__word {
  font-size: calc(var(--brand-h) * 0.78);
  font-weight: 570;
  letter-spacing: -0.035em;
  white-space: nowrap;
  line-height: 1;
}

/* The static row is the one this site actually uses, so this is the size that
   counts - the base above only applies if the marquee ever starts scrolling.
   Held still and centred, the logos can afford to be bigger than they would be
   sliding past. */
.marquee--static .brand { --brand-h: clamp(30px, 3.2vw, 44px); opacity: 0.66; }

@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none; }
  .marquee { overflow-x: auto; }
}

/* ============================== SERVICES ============================== */
.services {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(12px, 1.6vw, 20px);
}

.service {
  padding: clamp(22px, 2.4vw, 32px);
  border-radius: var(--r-lg);
  min-height: 250px;
  display: flex;
  flex-direction: column;
  transition: transform 0.45s var(--ease);
}
.service:hover { transform: translateY(-4px); }

/* The numeral used to be a 0.74rem label with `margin-bottom: auto`, which
   pushed the copy to the foot of the card. Every card is a different length,
   so every title landed at a different height and the row read as ragged.
   Everything now stacks from the top, and since the numeral is one line at a
   fixed size on all three, the titles start level. */
.service__num {
  font-size: clamp(2.8rem, 3.8vw, 3.9rem);
  font-weight: 620;
  line-height: 0.9;
  letter-spacing: -0.05em;
  font-variant-numeric: tabular-nums;
  margin-bottom: clamp(16px, 1.8vw, 24px);
  /* Bright at the top corner and gone by the bottom, so it sits behind the
     copy in the reading order without needing to be positioned behind it. */
  background: linear-gradient(150deg,
    rgba(255, 255, 255, 0.95) 0%,
    rgba(255, 255, 255, 0.55) 38%,
    rgba(255, 255, 255, 0.10) 100%);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  opacity: 0.34;
}

/* Two lines' worth whether or not the title needs it, so the body copy starts
   level across the row too. Dropped when the cards go single file, where there
   is nothing to line up against and it would only add a gap. */
.service h3 {
  font-size: 1.08rem;
  font-weight: 580;
  margin: 0 0 10px;
  letter-spacing: -0.022em;
  min-height: calc(2em * 1.25);
}
.service p { font-size: 0.9rem; color: var(--ink-2); line-height: 1.55; }

@media (max-width: 980px) { .services { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) {
  .services { grid-template-columns: 1fr; }
  .service { min-height: 0; }
  .service h3 { min-height: 0; }
}

/* ============================== CONTACT ============================== */
.contact {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: clamp(30px, 4vw, 68px);
  padding: clamp(28px, 4vw, 60px);
  border-radius: var(--r-xl);
}

.contact__meta {
  margin-top: 34px;
  display: grid;
  gap: 18px;
}
.contact__meta li {
  display: grid;
  gap: 3px;
  padding-top: 16px;
  border-top: 1px solid var(--line-soft);
}
.contact__meta span {
  font-size: 0.74rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-4);
}
.contact__meta a { color: var(--ink); border-bottom: 1px solid var(--ink-4); padding-bottom: 1px; transition: border-color 0.25s; }
.contact__meta a:hover { border-color: var(--accent-1); }
.contact__meta p { color: var(--ink-2); }

/* -- Form -- */
.form { display: grid; gap: 16px; align-content: start; }

.field { display: grid; gap: 7px; }
.field label {
  font-size: 0.82rem;
  font-weight: 500;
  color: var(--ink-2);
}
.field .opt { color: var(--ink-4); font-weight: 400; }

.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 14px 16px;
  border: 0;
  border-radius: var(--r-sm);
  background: rgba(255, 255, 255, 0.045);
  box-shadow: inset 0 0 0 1px var(--line);
  color: var(--ink);
  font-size: 0.95rem;
  transition: box-shadow 0.25s var(--ease), background-color 0.25s var(--ease);
  appearance: none;
}
.field textarea { resize: vertical; min-height: 120px; line-height: 1.55; }

.field select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5l5-5' stroke='rgba(255,255,255,.5)' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 16px center;
  padding-right: 40px;
  cursor: pointer;
}
.field select option { background: #121216; color: #fff; }

.field input::placeholder,
.field textarea::placeholder { color: var(--ink-4); }

.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: none;
  background: rgba(255, 255, 255, 0.075);
  box-shadow: inset 0 0 0 1px rgba(255, 154, 77, 0.55), 0 0 0 4px rgba(255, 154, 77, 0.1);
}

.field.has-error input,
.field.has-error select,
.field.has-error textarea {
  box-shadow: inset 0 0 0 1px rgba(255, 122, 122, 0.6);
}

.field__error {
  font-size: 0.78rem;
  color: #ff9c9c;
  min-height: 0;
}
.field__error:empty { display: none; }

.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }

/* Honeypot - visually gone, never announced, still fillable by bots. */
.hp {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.form__status {
  font-size: 0.88rem;
  line-height: 1.5;
  min-height: 0;
}
.form__status:empty { display: none; }
.form__status.is-ok  { color: #8ff0bd; }
.form__status.is-bad { color: #ff9c9c; }

.form__fine { font-size: 0.76rem; color: var(--ink-4); }

.spinner {
  width: 15px; height: 15px;
  border-radius: 50%;
  border: 2px solid rgba(0, 0, 0, 0.25);
  border-top-color: rgba(0, 0, 0, 0.75);
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

@media (max-width: 900px) {
  .contact { grid-template-columns: 1fr; }
}
@media (max-width: 480px) {
  .field-row { grid-template-columns: 1fr; }
}

/* ============================== FOOTER ============================== */
.footer {
  padding-block: clamp(40px, 6vw, 70px);
  border-top: 1px solid var(--line-soft);
}
.footer__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 22px;
}
.footer__name { font-weight: 580; letter-spacing: -0.02em; }
.footer__co   { font-size: 0.84rem; color: var(--ink-3); margin-top: 3px; }

.footer__social { display: flex; gap: 6px; }
.footer__social a {
  display: grid;
  place-items: center;
  width: 42px; height: 42px;
  border-radius: 999px;
  color: var(--ink-3);
  background: rgba(255, 255, 255, 0.04);
  box-shadow: inset 0 0 0 1px var(--line-soft);
  transition: color 0.25s var(--ease), background-color 0.25s var(--ease), transform 0.25s var(--ease);
}
.footer__social a:hover {
  color: var(--ink);
  background: rgba(255, 255, 255, 0.1);
  transform: translateY(-2px);
}

.footer__legal { font-size: 0.8rem; color: var(--ink-4); }

@media (max-width: 720px) {
  .footer__inner { flex-direction: column; text-align: center; }
}

/* ============================== REVEAL ============================== */
[data-reveal] {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity 0.8s var(--ease), transform 0.8s var(--ease);
}
[data-reveal].is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  [data-reveal] { opacity: 1; transform: none; transition: none; }
}
