/* Base rules carried over verbatim from the design's <helmet> block. */
html { scroll-behavior: smooth }
body {
  margin: 0;
  background: #0C0C0B;
  color: #F2F0EA;
  font-family: 'Space Grotesk', sans-serif;
  -webkit-font-smoothing: antialiased;
}
::selection { background: #F2F0EA; color: #0C0C0B }

@media (max-width: 820px) {
  [data-slide] { padding: 64px 6vw 56px !important }
  [data-slide] > div:first-child { flex-wrap: wrap; gap: 3px 18px; font-size: 9.5px !important }
  [data-slide] > div:nth-child(2) {
    display: flex !important;
    flex-direction: column;
    align-items: stretch !important;
    justify-content: center;
    gap: 22px !important;
    margin-top: 8px !important;
    min-height: 0;
  }
  /* Scaled off --logo-h rather than pinned to a fixed px, so a panel that sets
     a taller mark stays proportionally taller here too. */
  [data-s-logo] { height: calc(var(--logo-h) * .6) !important; margin-bottom: 13px !important }
  [data-s-title] { font-size: clamp(30px, 8.5vw, 44px) !important }
  [data-s-desc] { font-size: 14px !important; margin-top: 14px !important }
  [data-s-mock] { display: flex !important; justify-content: center; min-height: 0 }
  [data-s-mock] > div:not(#p2-proto) { min-width: 640px; flex: none }
}

/* ----------------------------------------------------------------- panel logo
   The wrapper owns the height (set inline per panel by PanelLogo.astro) and the
   mark scales to it off its own viewBox, so marks of different aspect ratios
   sit on the same optical line. Colour is inherited: the monochrome SVGs are
   drawn with currentColor, so each mark picks up the ink of the panel it lands
   on rather than carrying a fill that only suits one background. */
[data-s-logo] > svg { height: 100%; width: auto; aspect-ratio: var(--logo-ar); display: block }

/* ---------------------------------------------------------------- image slots
   The design used a drag-and-drop <image-slot> widget for the portrait and the
   three personal-project shots. None were filled, so they compile to empty
   frames — drop an <img> inside one and it takes over with no other changes. */
.img-slot {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  box-sizing: border-box;
  background: rgba(242, 240, 234, .035);
  border: 1.5px dashed rgba(242, 240, 234, .22);
  text-align: center;
  padding: 16px;
}
.img-slot > span {
  font-family: 'Space Mono', monospace;
  font-size: 10px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: rgba(242, 240, 234, .38);
  max-width: 80%;
}
.img-slot > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.img-slot:has(img) { border-style: solid; border-color: transparent }
.img-slot:has(img) > span { display: none }

/* -------------------------------------------------------------- globe tooltip
   Positioned by site.js from the projected screen position of the hovered
   marker; the wrapper (#globe-wrap) is the containing block. */
.globe-tip {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -100%);
  transition: opacity .16s ease;
  padding: 8px 12px 9px;
  border: 1px solid rgba(242, 240, 234, .3);
  background: rgba(12, 12, 11, .92);
  backdrop-filter: blur(6px);
  white-space: nowrap;
  font-family: 'Space Mono', monospace;
  text-transform: uppercase;
}
.globe-tip.is-on { opacity: 1 }
.globe-tip.is-below { transform: translate(-50%, 0) }
.globe-tip > b {
  display: block;
  font-size: 10.5px;
  font-weight: 400;
  letter-spacing: .16em;
  color: #F2F0EA;
}
.globe-tip > i {
  display: block;
  margin-top: 4px;
  font-size: 9.5px;
  font-style: normal;
  letter-spacing: .12em;
  color: rgba(242, 240, 234, .6);
}

/* ---------------------------------------------------------------- notes list
   <sc-for> over `rubs` and `filteredPosts`. The chips filter client-side in
   site.js; the styling below is the design's inline styling, lifted to classes
   because the markup is now generated rather than authored per-row. */
.rub {
  font-family: 'Space Mono', monospace;
  font-size: 10.5px;
  letter-spacing: .14em;
  padding: 9px 16px;
  border-radius: 100px;
  cursor: pointer;
  user-select: none;
  border: 1px solid rgba(242, 240, 234, .25);
  background: transparent;
  color: rgba(242, 240, 234, .7);
}
.rub.is-on {
  border-color: transparent;
  background: #F2F0EA;
  color: #0C0C0B;
  font-weight: 700;
}

.note {
  display: grid;
  grid-template-columns: 170px 1fr auto;
  gap: 28px;
  align-items: baseline;
  padding: 26px 6px;
  border-top: 1px solid rgba(242, 240, 234, .12);
  cursor: pointer;
  color: inherit;
  text-decoration: none;
}
.note:hover { background: rgba(242, 240, 234, .04) }
.note[hidden] { display: none }
.note-tag {
  font-family: 'Space Mono', monospace;
  font-size: 10px;
  letter-spacing: .16em;
  color: rgba(242, 240, 234, .5);
}
.note-title {
  font-size: clamp(20px, 1.8vw, 28px);
  font-weight: 500;
  letter-spacing: -.015em;
  line-height: 1.25;
}
.note-meta {
  font-family: 'Space Mono', monospace;
  font-size: 10px;
  letter-spacing: .12em;
  color: rgba(242, 240, 234, .4);
  display: flex;
  gap: 18px;
  align-items: baseline;
}
.note-arrow { font-size: 14px; color: rgba(242, 240, 234, .7) }

@media (max-width: 820px) {
  .note { grid-template-columns: 1fr; gap: 8px }
}

/* ---------------------------------------------------------------- prototype
   Keyframes the Client 360 prototype animates against, from its own <helmet>. */
@keyframes fadeUp { from { opacity: 0; transform: translateY(14px) } to { opacity: 1; transform: none } }
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
@keyframes draw { to { stroke-dashoffset: 0 } }
@keyframes growBar { from { width: 0 } }
@keyframes toastIn { from { opacity: 0; transform: translate(-50%, 8px) } to { opacity: 1; transform: translate(-50%, 0) } }
@keyframes pulseDot { 0%, 100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, .22) } 65% { box-shadow: 0 0 0 5px rgba(0, 0, 0, 0) } }
@keyframes blink { 50% { opacity: .2 } }

#client360 input::placeholder { color: rgba(26, 25, 23, .38) }
#client360 button { font-family: 'Space Grotesk', sans-serif }
/* The phones' inner panes scroll (the idle demo pans them), but a desktop
   scrollbar track down the edge of a phone mockup breaks the illusion. */
#client360 [data-noscroll] { scrollbar-width: none; -ms-overflow-style: none }
#client360 [data-noscroll]::-webkit-scrollbar { display: none; width: 0; height: 0 }

/* ---------------------------------------------------------------- phones
   The source design shipped a media query for the project slides only, so the
   page-level grids still ran desktop-first and pushed the document wider than
   the viewport. These collapse them; nothing above 820px is affected. */
@media (max-width: 820px) {
  [data-nav] { padding: 16px 5vw !important; gap: 12px }
  [data-nav] > div:nth-child(2) { display: none }          /* SEATTLE — 47.6°N */
  [data-nav] > div:last-child { gap: 18px !important; font-size: 10px }

  [data-about-grid] { grid-template-columns: 1fr !important; gap: 48px !important }
  [data-stats] { grid-template-columns: 1fr 1fr !important }
  [data-personal] { grid-template-columns: 1fr !important }

  /* Role rows: title and date stack instead of colliding. */
  [data-roles] > div { flex-wrap: wrap; gap: 4px }
  [data-roles] > div > div:first-child { font-size: 16px !important }

  /* Section headers that pair a label with a right-aligned note. */
  #work > div:first-child > div:first-child { flex-wrap: wrap; gap: 8px }
  #contact > div:last-child { flex-direction: column; align-items: flex-start; gap: 10px }
}

@media (max-width: 520px) {
  /* Wordmark and links share one row — tighten both so they don't touch. */
  [data-nav] > a:first-child { font-size: 10px }
  [data-nav] > div:last-child { gap: 12px !important; font-size: 9px }
  [data-nav] > div:last-child a { letter-spacing: .1em !important }
}

/* ------------------------------------------------------------- slide shadows
   Each project slide enters by sliding left over the previous one, so its
   leading edge casts onto the panel underneath. Two layers — a soft contact
   edge and a very wide ambient falloff — both offset left only; the rest
   spills outside #stack-wrap's overflow:hidden and never shows. The tints come
   from site.js, keyed to the colour of the panel being covered so the shadow
   deepens that panel's own hue instead of greying it out.

   --slide-shadow-o gates the whole thing and defaults to 0, which matters:
   every slide waiting its turn is parked flush against the container's right
   edge, and a shadow there would bleed back across the panel on screen as a
   permanent dark band. The stack timeline in site.js raises it only while a
   slide is actually travelling. Slide one never casts — nothing under it. */
[data-slide] {
  --slide-shadow-o: 0;
  box-shadow:
    -8px 0 34px -18px var(--slide-shadow-contact, transparent),
    -60px 0 130px -54px var(--slide-shadow-ambient, transparent);
}

/* ---------------------------------------------------------------- blob cursor
   Custom cursor over the project slides: a gooey trail of rose blobs (reactbits
   BlobCursor, ported to vanilla GSAP) with a PRIVATE DEMO tag riding the lead
   blob. site.js injects the markup and toggles .blob-cursor-live on <body>
   while the pointer is inside a [data-slide]; without it none of this runs. */
#blob-cursor {
  position: fixed;
  inset: 0;
  z-index: 100;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
}
#blob-cursor .blob-goo { position: absolute; inset: 0; filter: url(#blob-goo-filter) }
#blob-cursor .blob {
  position: absolute;
  top: 0;
  left: 0;
  border-radius: 50%;
  background: #1A1917;
  /* Opaque: partial alpha here gets stretched by the filter's alpha crush into
     a visible band where the blobs overlap. The blur still makes the goo. */
  opacity: 1;
  will-change: transform;
}
#blob-label {
  position: absolute;
  top: 0;
  left: 0;
  font-family: 'Space Mono', monospace;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .22em;
  line-height: 1.6;
  text-transform: uppercase;
  text-align: center;
  text-indent: .22em;   /* recentre — tracking pads the right edge only */
  color: #F2F0EA;
  will-change: transform;
}
body.blob-cursor-live [data-slide] { cursor: none }

/* ------------------------------------------------------------- note tooltip
   A read-time pill that trails the pointer down the notes list. Deliberately
   NOT the blob cursor: that thing is 140px of goo, which swallows a headline.
   This is small enough to sit beside the words it's talking about, so the
   native cursor stays put and the row stays readable underneath. */
.note-tip {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 100;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  /* Offset off the hotspot so the pill sits beside the pointer, not under it. */
  margin: 15px 0 0 17px;
  padding: 7px 14px 8px;
  border-radius: 100px;
  background: #F2F0EA;
  color: #0C0C0B;
  font-family: 'Space Mono', monospace;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .18em;
  text-indent: .18em;    /* recentre — tracking pads the right edge only */
  text-transform: uppercase;
  white-space: nowrap;
  will-change: transform;
}
.note-tip > i { font-style: normal; font-weight: 400; color: rgba(12, 12, 11, .45) }

/* ------------------------------------------------------------- agent status
   The chip above the hero headline, shaped like the status line an agent shows
   while it works: a spinner plus a label that cycles through what it's doing.
   The activities are product-design ones and none of them are real — it reads
   as a signature, not as telemetry.

   site.js drives the label. The markup ships with the first phrase already in
   it and the track sized by its own content, so with JS off the chip renders
   as a static, correct-looking status line rather than an empty pill. */
.agent-status {
  display: inline-flex;
  align-items: center;
  gap: 11px;
  padding: 10px 20px 10px 14px;
  border: 1px solid rgba(242, 240, 234, .18);
  border-radius: 100px;
  background: rgba(242, 240, 234, .045);
  backdrop-filter: blur(6px);
}
.agent-status > svg { display: block; flex: none; width: 19px; height: 19px; color: #F2F0EA }

/* Clipping window for the label. Its width is auto until site.js takes over,
   then explicit — the pill has to grow and shrink between phrases of different
   lengths, and width only transitions from one length to another. */
/* Flex rather than block: as a block this would lay the label out on a line
   box struck by the track's own inherited 16px font, which baseline-shoves the
   label a couple of pixels down off the spinner's centre. A flex line has no
   strut, so `center` here means centred. */
.agent-status-track {
  display: flex;
  align-items: center;
  overflow: hidden;
  height: 18px;   /* full ascender-to-descender box at 12px, or it clips */
  transition: width .45s cubic-bezier(.4, 0, .2, 1);
}
/* flex:none + nowrap: keeps its natural width even when the track around it is
   mid-transition and narrower, which is what makes offsetWidth measurable. */
.agent-status-line {
  flex: none;
  white-space: nowrap;
  font-family: 'Space Mono', monospace;
  font-size: 12px;
  line-height: 18px;
  letter-spacing: .16em;
  text-transform: uppercase;
  /* A bright band travelling left to right through dimmed text. The gradient
     is symmetric and flat at both ends, so repeat-x tiles it seamlessly, and
     one keyframe cycle moves it by exactly one tile (2x the element width) —
     the loop has no visible restart. */
  background-image: linear-gradient(100deg,
    rgba(242, 240, 234, .42) 20%, #F2F0EA 50%, rgba(242, 240, 234, .42) 80%);
  background-size: 200% 100%;
  background-repeat: repeat-x;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  animation: astShimmer 3s linear infinite;
  transition: opacity .26s ease, transform .26s ease;
}
.agent-status-line.is-out { opacity: 0; transform: translateY(-8px) }
/* Entry start state. transition:none so setting it doesn't animate — the swap
   drops the phrase into place below the window, then releases it. */
.agent-status-line.is-pre { opacity: 0; transform: translateY(8px); transition: none }

@keyframes astShimmer { from { background-position: 100% 0 } to { background-position: -100% 0 } }

/* Spinner: svg-loaders #33 (blocks-shuffle-5). Keyframes verbatim from the
   source, with the class and animation names moved into this file's namespace;
   fill:currentColor is what lets the chip colour it. */
.ast-dot { transform-origin: 50% 50%; fill: currentColor }
@keyframes astDot1 { 9.0909090909%{transform:translate(-12px, 0)} 18.1818181818%{transform:translate(0px, 0)} 27.2727272727%{transform:translate(0px, 0)} 36.3636363636%{transform:translate(12px, 0)} 45.4545454545%{transform:translate(12px, 12px)} 54.5454545455%{transform:translate(12px, 12px)} 63.6363636364%{transform:translate(12px, 12px)} 72.7272727273%{transform:translate(12px, 0px)} 81.8181818182%{transform:translate(0px, 0px)} 90.9090909091%{transform:translate(-12px, 0px)} 100%{transform:translate(0px, 0px)} }
.ast-dot:nth-child(1) { animation: astDot1 4s infinite }
@keyframes astDot2 { 9.0909090909%{transform:translate(0, 0)} 18.1818181818%{transform:translate(12px, 0)} 27.2727272727%{transform:translate(0px, 0)} 36.3636363636%{transform:translate(12px, 0)} 45.4545454545%{transform:translate(12px, 12px)} 54.5454545455%{transform:translate(12px, 12px)} 63.6363636364%{transform:translate(12px, 12px)} 72.7272727273%{transform:translate(12px, 12px)} 81.8181818182%{transform:translate(0px, 12px)} 90.9090909091%{transform:translate(0px, 12px)} 100%{transform:translate(0px, 0px)} }
.ast-dot:nth-child(2) { animation: astDot2 4s infinite }
@keyframes astDot3 { 9.0909090909%{transform:translate(-12px, 0)} 18.1818181818%{transform:translate(-12px, 0)} 27.2727272727%{transform:translate(0px, 0)} 36.3636363636%{transform:translate(-12px, 0)} 45.4545454545%{transform:translate(-12px, 0)} 54.5454545455%{transform:translate(-12px, 0)} 63.6363636364%{transform:translate(-12px, 0)} 72.7272727273%{transform:translate(-12px, 0)} 81.8181818182%{transform:translate(-12px, -12px)} 90.9090909091%{transform:translate(0px, -12px)} 100%{transform:translate(0px, 0px)} }
.ast-dot:nth-child(3) { animation: astDot3 4s infinite }
@keyframes astDot4 { 9.0909090909%{transform:translate(-12px, 0)} 18.1818181818%{transform:translate(-12px, 0)} 27.2727272727%{transform:translate(-12px, -12px)} 36.3636363636%{transform:translate(0px, -12px)} 45.4545454545%{transform:translate(0px, 0px)} 54.5454545455%{transform:translate(0px, -12px)} 63.6363636364%{transform:translate(0px, -12px)} 72.7272727273%{transform:translate(0px, -12px)} 81.8181818182%{transform:translate(-12px, -12px)} 90.9090909091%{transform:translate(-12px, 0px)} 100%{transform:translate(0px, 0px)} }
.ast-dot:nth-child(4) { animation: astDot4 4s infinite }
@keyframes astDot5 { 9.0909090909%{transform:translate(0, 0)} 18.1818181818%{transform:translate(0, 0)} 27.2727272727%{transform:translate(0, 0)} 36.3636363636%{transform:translate(12px, 0)} 45.4545454545%{transform:translate(12px, 0)} 54.5454545455%{transform:translate(12px, 0)} 63.6363636364%{transform:translate(12px, 0)} 72.7272727273%{transform:translate(12px, 0)} 81.8181818182%{transform:translate(12px, -12px)} 90.9090909091%{transform:translate(0px, -12px)} 100%{transform:translate(0px, 0px)} }
.ast-dot:nth-child(5) { animation: astDot5 4s infinite }
@keyframes astDot6 { 9.0909090909%{transform:translate(0, 0)} 18.1818181818%{transform:translate(-12px, 0)} 27.2727272727%{transform:translate(-12px, 0)} 36.3636363636%{transform:translate(0px, 0)} 45.4545454545%{transform:translate(0px, 0)} 54.5454545455%{transform:translate(0px, 0)} 63.6363636364%{transform:translate(0px, 0)} 72.7272727273%{transform:translate(0px, 12px)} 81.8181818182%{transform:translate(-12px, 12px)} 90.9090909091%{transform:translate(-12px, 0px)} 100%{transform:translate(0px, 0px)} }
.ast-dot:nth-child(6) { animation: astDot6 4s infinite }
@keyframes astDot7 { 9.0909090909%{transform:translate(12px, 0)} 18.1818181818%{transform:translate(12px, 0)} 27.2727272727%{transform:translate(12px, 0)} 36.3636363636%{transform:translate(0px, 0)} 45.4545454545%{transform:translate(0px, -12px)} 54.5454545455%{transform:translate(12px, -12px)} 63.6363636364%{transform:translate(0px, -12px)} 72.7272727273%{transform:translate(0px, -12px)} 81.8181818182%{transform:translate(0px, 0px)} 90.9090909091%{transform:translate(12px, 0px)} 100%{transform:translate(0px, 0px)} }
.ast-dot:nth-child(7) { animation: astDot7 4s infinite }
@keyframes astDot8 { 9.0909090909%{transform:translate(0, 0)} 18.1818181818%{transform:translate(-12px, 0)} 27.2727272727%{transform:translate(-12px, -12px)} 36.3636363636%{transform:translate(0px, -12px)} 45.4545454545%{transform:translate(0px, -12px)} 54.5454545455%{transform:translate(0px, -12px)} 63.6363636364%{transform:translate(0px, -12px)} 72.7272727273%{transform:translate(0px, -12px)} 81.8181818182%{transform:translate(12px, -12px)} 90.9090909091%{transform:translate(12px, 0px)} 100%{transform:translate(0px, 0px)} }
.ast-dot:nth-child(8) { animation: astDot8 4s infinite }
@keyframes astDot9 { 9.0909090909%{transform:translate(-12px, 0)} 18.1818181818%{transform:translate(-12px, 0)} 27.2727272727%{transform:translate(0px, 0)} 36.3636363636%{transform:translate(-12px, 0)} 45.4545454545%{transform:translate(0px, 0)} 54.5454545455%{transform:translate(0px, 0)} 63.6363636364%{transform:translate(-12px, 0)} 72.7272727273%{transform:translate(-12px, 0)} 81.8181818182%{transform:translate(-24px, 0)} 90.9090909091%{transform:translate(-12px, 0)} 100%{transform:translate(0px, 0)} }
.ast-dot:nth-child(9) { animation: astDot9 4s infinite }

@media (max-width: 520px) {
  /* The longest phrases would otherwise push the pill past the 5vw gutter. */
  .agent-status { gap: 9px; padding: 8px 15px 8px 11px }
  .agent-status > svg { width: 17px; height: 17px }
  .agent-status-line { font-size: 10.5px; letter-spacing: .11em }
}

/* ---------------------------------------------------------- contact pills
   The two CTAs in the contact section. These were inline-styled, which meant
   the .hv-27/.hv-28 hover rules could never win — an inline `background` beats
   any selector. Real classes so the hover states actually land. */
.btn {
  position: relative;
  isolation: isolate;         /* so ::before can sit at z-index:-1 above the fill */
  overflow: hidden;           /* clips the sweep to the pill */
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 18px 34px;
  border: 1px solid transparent;
  border-radius: 100px;
  font-family: 'Space Mono', monospace;
  font-size: 12px;
  letter-spacing: .16em;
  text-decoration: none;
  will-change: transform;
  transition:
    transform .42s cubic-bezier(.22, 1, .36, 1),
    background-color .3s ease,
    border-color .3s ease,
    color .22s ease .08s;     /* delayed: flips once the fill is most of the way up */
}
.btn:hover { transform: translateY(-2px) }
.btn:active { transform: translateY(0); transition-duration: .09s }
.btn:focus-visible { outline: 2px solid #F2F0EA; outline-offset: 3px }

/* Arrow swap: the resting glyph leaves up-and-right, its twin follows it in
   from down-and-left. One box, two glyphs, overflow hidden. */
.btn-arrow {
  position: relative;
  overflow: hidden;
  flex: none;
  width: 1.15em;
  height: 1.15em;
}
.btn-arrow > span {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform .42s cubic-bezier(.22, 1, .36, 1);
}
.btn-arrow > span:nth-child(2) { transform: translate(-130%, 130%) }
.btn:hover .btn-arrow > span:nth-child(1) { transform: translate(130%, -130%) }
.btn:hover .btn-arrow > span:nth-child(2) { transform: translate(0, 0) }

/* Solid: lights up to white. */
.btn--solid { background: #F2F0EA; color: #0C0C0B; font-weight: 700 }
.btn--solid:hover { background: #FFFFFF }

/* Ghost: cream fill wipes up from the bottom edge and the label inverts. */
.btn--ghost { border-color: rgba(242, 240, 234, .35); color: #F2F0EA }
.btn--ghost::before {
  content: '';
  position: absolute;
  inset: -1px;                /* covers the border box, not just the padding box */
  z-index: -1;
  background: #F2F0EA;
  transform: translateY(101%);
  transition: transform .45s cubic-bezier(.22, 1, .36, 1);
}
.btn--ghost:hover { border-color: #F2F0EA; color: #0C0C0B }
.btn--ghost:hover::before { transform: translateY(0) }

/* ---------------------------------------------------------------- motion
   Everything visual is GSAP-driven; honour the OS-level opt-out. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto }
  #blob-cursor { display: none }

  /* Pills keep their hover *states*, lose the travel. The arrow swap has
     nowhere to go without motion, so the twin just stays parked offscreen. */
  .btn, .btn-arrow > span, .btn--ghost::before { transition: none }
  .btn:hover { transform: none }
  .btn:hover .btn-arrow > span:nth-child(1) { transform: none }
  .btn:hover .btn-arrow > span:nth-child(2) { transform: translate(-130%, 130%) }

  /* The Client 360 prototype ships its own CSS motion — entrance fades on the
     cards and rows, and two infinite loops (the health-pill pulse and the LIVE
     blink) that would keep running long after site.js has declined to start the
     idle director. Everything in there goes quiet; client360.js separately
     skips the stroke-draw, which is the one animation whose *end* state the
     chart depends on. */
  #client360 *, #client360 *::before, #client360 *::after {
    animation: none !important;
    transition: none !important;
  }

  /* Chip goes quiet: no shuffle, no shimmer, and site.js stops cycling the
     label — so the flat fill has to replace the gradient it was clipped from. */
  .ast-dot, .agent-status-line { animation: none }
  .agent-status-line {
    background-image: none;
    color: rgba(242, 240, 234, .8);
    -webkit-text-fill-color: currentColor;
  }
}
