/* Shared motion tokens and the primitives that ride on them.

   Durations live here and are mirrored in site/motion.js, so a value is only
   ever changed in two places that sit next to each other in a diff. Anything
   that reads as one gesture shares a token rather than picking its own number
   — the whole point of the pass is that switching a tab, filtering the work
   grid and swapping a testimonial should feel like the same hand. */
:root {
  --ik-ease: cubic-bezier(.22, 1, .36, 1);
  --ik-ease-in: cubic-bezier(.4, 0, 1, 1);

  --ik-indicator: 220ms;   /* tabs sliding */
  --ik-group: 210ms;       /* filtered group cross-fade */
  --ik-swap: 240ms;        /* text state swap */
  --ik-swap-fast: 200ms;   /* faq answer swap */
  --ik-count: 180ms;       /* number pop-in */
  --ik-expand: 250ms;      /* card resize / accordion */
  --ik-modal-in: 220ms;
  --ik-modal-out: 160ms;
  --ik-reveal: 370ms;
  --ik-arrow: 180ms;
}

/* ---- sliding indicator ------------------------------------------------ */
/* The container becomes the positioning context the helper measures against.
   Tabs are lifted above the indicator so it reads as a background plate that
   the label sits on, not a panel covering it. */
.ik-has-indicator {
  position: relative;
}

.ik-has-indicator > button {
  position: relative;
  z-index: 1;
}

.ik-indicator {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  display: block;
  opacity: 0;
  pointer-events: none;
  will-change: transform, width, height;
}

.ik-indicator.is-on {
  opacity: 1;
}

/* Work filter pills. With an indicator carrying the fill, the pressed button
   keeps only its colour change — two filled pills during a slide would read
   as a bug. The colour is deliberately quicker than the slide so the label
   has resolved by the time the plate arrives under it. */
#work .filter-bar .ik-indicator {
  border: 1px solid var(--action);
  border-radius: 999px;
  background: var(--action);
  box-shadow: 0 8px 22px color-mix(in srgb, var(--action) 20%, transparent);
}

#work .filter-bar.ik-has-indicator .filter-button[aria-pressed="true"] {
  border-color: transparent;
  background: transparent;
  box-shadow: none;
}

#work .filter-bar.ik-has-indicator .filter-button {
  transition: color 150ms ease, border-color 150ms ease;
}

/* Hover on a pill that is not the active one still needs to read, but it must
   not paint a second filled pill next to the indicator. */
#work .filter-bar.ik-has-indicator .filter-button:hover[aria-pressed="true"] {
  background: transparent;
  border-color: transparent;
}

/* Studio console rail. Vertical on desktop, horizontal under 900px — the
   helper measures both without being told which, so only the skin differs. */
#studio-console .sc-tabs .ik-indicator {
  border: 1px solid #f0f4f8;
  border-radius: 7px;
  background: linear-gradient(#f4f6f8, #dfe6ee);
  box-shadow: 0 8px 18px rgb(var(--studio-cast) / 0.22);
}

#studio-console .sc-tabs.ik-has-indicator .sc-tab[aria-selected="true"] {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  /* The 2px nudge was the old way of showing selection. The indicator does
     that job now, and keeping both made the label step sideways under a plate
     that was still arriving. */
  transform: none;
}

#studio-console .sc-tabs.ik-has-indicator .sc-tab {
  transition: color 150ms var(--sc-ease, ease);
}

/* Comparison category pills. The pill the label sits on is the only thing
   that moves; the podium, the lanes and the cards are untouched by a category
   change and must stay exactly where they are. */
#taste-stage .taste-stage-tabs .ik-indicator {
  border-radius: 999px;
  background: var(--ink);
}

html[data-skin="nocturne"] #taste-stage .taste-stage-tabs .ik-indicator {
  background: #0b162b;
}

#taste-stage .taste-stage-tabs.ik-has-indicator button[aria-selected="true"] {
  background: transparent;
}

html[data-skin="nocturne"] #taste-stage .taste-stage-tabs.ik-has-indicator button[aria-selected="true"] {
  background: transparent;
}

/* The three comparison stills resolve together once the next one has actually
   decoded, so the lanes never show a half-swapped set. */
#taste-stage [data-taste-img] {
  will-change: opacity;
}

/* ---- group cross-fade for filtered collections ------------------------ */
.ik-group-fade {
  transition: opacity var(--ik-group) var(--ik-ease);
}

.ik-group-fade.is-swapping {
  opacity: 0.28;
}

/* ---- arrow links ------------------------------------------------------ */
/* Only the glyph moves. The anchor keeps its box, so hit areas and text
   positions are untouched — the arrow is wrapped in an inline-block purely so
   it has a transform box of its own. */
.ik-arrow {
  display: inline-block;
  transition: transform var(--ik-arrow) var(--ik-ease);
  will-change: transform;
}

a:hover > .ik-arrow,
a:focus-visible > .ik-arrow,
button:hover > .ik-arrow,
button:focus-visible > .ik-arrow,
a:hover .button-copy > .ik-arrow,
a:focus-visible .button-copy > .ik-arrow {
  transform: translateX(2px);
}

/* Diagonal glyphs travel along their own axis or the motion reads as a slip. */
.ik-arrow[data-ik-arrow="up-right"] {
  transition: transform var(--ik-arrow) var(--ik-ease);
}

a:hover > .ik-arrow[data-ik-arrow="up-right"],
a:focus-visible > .ik-arrow[data-ik-arrow="up-right"],
a:hover .button-copy > .ik-arrow[data-ik-arrow="up-right"],
a:focus-visible .button-copy > .ik-arrow[data-ik-arrow="up-right"] {
  transform: translate(1.6px, -1.6px);
}

/* ---- hero headline reveal --------------------------------------------- */
/* Gated on cloud-entry-active, which cloud-entry.js sets synchronously from a
   blocking script above the hero markup — so the headline is never painted in
   its final state and then re-hidden. Three consequences worth keeping:

   - A visitor who skips the entrance (back/forward, in-site link, reduced
     motion) gets cloud-entry-done instead, the rule never matches, and the
     headline is plain static text. That is the "once, and only as part of the
     existing entrance" requirement — there is nothing to replay.
   - The class is removed when the entrance ends, so even if the reveal
     listener never runs the headline becomes visible on its own. There is no
     failure mode where the hero is left blank.
   - The lines are the two spans already in the markup, so nothing is split and
     the phrase stays intact for assistive tech. */
html.cloud-entry-active .hero h1 > span {
  display: inline-block;
  opacity: 0;
  transform: translateY(7px);
}

html.cloud-entry-active .hero h1.is-revealed > span {
  opacity: 1;
  transform: none;
  transition: opacity var(--ik-reveal) var(--ik-ease), transform var(--ik-reveal) var(--ik-ease);
}

html.cloud-entry-active .hero h1.is-revealed > span:nth-child(2) {
  transition-delay: 40ms;
}

/* ---- modal open/close -------------------------------------------------- */
.ik-modal-anim {
  animation: ik-modal-in var(--ik-modal-in) var(--ik-ease) both;
}

.ik-modal-anim-backdrop {
  animation: ik-fade-in var(--ik-modal-in) var(--ik-ease) both;
}

.ik-modal-closing .ik-modal-anim {
  animation: ik-modal-out var(--ik-modal-out) var(--ik-ease-in) both;
}

.ik-modal-closing .ik-modal-anim-backdrop {
  animation: ik-fade-out var(--ik-modal-out) var(--ik-ease-in) both;
}

@keyframes ik-modal-in {
  from { opacity: 0; transform: scale(.985); }
  to   { opacity: 1; transform: none; }
}

@keyframes ik-modal-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: scale(.985); }
}

@keyframes ik-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes ik-fade-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* On a phone the scale reads as a wobble rather than a lift, so the panel
   just fades. */
@media (max-width: 720px) {
  @keyframes ik-modal-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  @keyframes ik-modal-out {
    from { opacity: 1; }
    to   { opacity: 0; }
  }
}

/* ---- configurator loading --------------------------------------------- */
.ik-embed-shell {
  position: relative;
}

.ik-embed-veil {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: grid;
  place-content: center;
  gap: 10px;
  justify-items: center;
  padding: 20px;
  text-align: center;
  background: color-mix(in srgb, var(--surface, #101722) 88%, transparent);
  transition: opacity var(--ik-group) var(--ik-ease);
}

.ik-embed-veil[hidden] {
  display: none;
}

.ik-embed-veil.is-clearing {
  opacity: 0;
  pointer-events: none;
}

/* The tile's own product still is what stands in while the frame arrives —
   the reader sees the thing they clicked, not a grey rectangle pretending to
   be one. */
.ik-embed-poster {
  position: absolute;
  inset: 0;
  z-index: -1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.5;
}

.ik-embed-label {
  margin: 0;
  color: var(--muted, #9aa7b8);
  font-size: 0.8rem;
  letter-spacing: 0.02em;
}

.ik-embed-retry {
  min-height: 40px;
  padding: 0 16px;
  border: 1px solid currentColor;
  border-radius: 999px;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

/* ---- reduced motion ---------------------------------------------------- */
/* Final states, immediately, with every control still doing its job. The
   indicator keeps its position — it is placed with a transform either way,
   the transform simply stops being interpolated. */
@media (prefers-reduced-motion: reduce) {
  .ik-indicator,
  .ik-group-fade,
  .ik-arrow,
  .ik-embed-veil {
    transition: none !important;
  }

  .ik-group-fade.is-swapping {
    opacity: 1;
  }

  a:hover > .ik-arrow,
  a:focus-visible > .ik-arrow,
  button:hover > .ik-arrow,
  button:focus-visible > .ik-arrow,
  a:hover .button-copy > .ik-arrow,
  a:focus-visible .button-copy > .ik-arrow {
    transform: none;
  }

  html.cloud-entry-active .hero h1 > span {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  .ik-modal-anim,
  .ik-modal-anim-backdrop,
  .ik-modal-closing .ik-modal-anim,
  .ik-modal-closing .ik-modal-anim-backdrop {
    animation: none !important;
  }
}
