/* =========================================================================
   contact.css — the contact MODAL. Loaded on every page.

   WAS A PAGE UNTIL 2026-07-28 (Figma frame 4305:276). Troy asked for a modal
   and for /contact to be deleted rather than kept as a fallback. Everything
   inside the dialog — heading, fields, tiles — is unchanged from the page; the
   page chrome (its own body background, sticky masthead, page glow) is gone,
   and a scrim + window + focus management replaced it.

   Scoped under `.ct-` throughout, so loading this on every page cannot touch
   anything else. The one global is `body.ct-modal-open`, which locks scroll.

   DELIBERATE DEVIATIONS FROM THE FIGMA FRAME — see HANDOFF §4c:
   - The MM/DD/YY field is GONE. Unlabelled date field; Troy's call.
   - Every field has a real <label>. The frame used placeholder-as-label, which
     disappears the moment someone types.
   - The phone tile is gone until there is a real number — the frame's
     (501) 123-4567 is an Arkansas placeholder.
   ========================================================================= */

/* ---- The spacing scale --------------------------------------------------
   FOUR steps at the 1440x900 reference — 8 / 16 / 28 / 40 — each far enough
   from the last to be read rather than measured, and nothing in the modal is
   allowed to invent a fifth.

     --ct-bond    ~8px   a label and the control it names; a heading and its
                         own sub-line. Reads as "this belongs to that".
     --ct-field  ~16px   one field to the next. Twice the bond, so a group
                         and a gap are told apart on sight, not by measuring.
     --ct-zone   ~28px   header | form | tiles. The largest gaps in the
                         dialog and the only ones this size, so the three
                         zones separate with no rule, panel or decoration.
     --ct-frame  ~40px   the dialog's own inset, identical on all four sides.

   WHY: the modal previously ran 58 / 12 / 9 / 21 / 6 / 12 / 12 / 6 / 14 / 19
   down the page from nine unrelated clamps. A label sat 6px above its own
   input and 12px below the previous field — two gaps close enough that the
   eye reads noise instead of grouping — and the header was separated from
   the form by 21px, less than a field is from its own neighbour. Nothing
   grouped, so nothing looked considered.

   Every vertical step is vh-fluid, like the rest of this file: the window is
   capped at 92dvh and the message box absorbs the slack, so the SCALE has to
   give ground on a short screen before the box does.

   The `calc(Nvh - Xpx)` terms are that rule made literal. A plain vh term with
   a floor was measured growing every gap on a phone — a 1.8vh field step is
   12px at 667px tall, where the old clamp bottomed out at 10 — and the phone
   is the one viewport with nothing to spare. The offset steepens each step so
   it lands on the full desktop value at 900 while collapsing to the OLD floor
   at 667: zone 16 / field 10 / bond 6, i.e. exactly what a short screen gets
   today. The scale is a desktop improvement that costs a phone nothing.

   --ct-frame is the one vw term because it is horizontal. Recalibrated from
   3.2vw, which did not reach its 40px ceiling until a 1250px viewport, so a
   1000px laptop got 32px of padding on a window that had already settled at
   its full 720. 5.3vw tops out at 752 = 720 + the scrim's own --sp-4 gutters,
   which is the exact width below which the window stops being 720 — so the
   padding is now constant whenever the modal itself is. */
.ct-modal {
  --ct-bond: clamp(6px, .9vh, 10px);
  --ct-field: clamp(10px, calc(2.6vh - 7px), 18px);
  --ct-zone: clamp(16px, calc(5.1vh - 18px), 32px);
  --ct-frame: clamp(20px, 5.3vw, 40px);
}

/* Below 640 the name/email row stacks and the tiles wrap, so the dialog is
   height-critical at ANY viewport height — a phone in portrait has more vh
   than a laptop and still less room, because the same content is a single
   column nearly twice as tall. The vh terms cannot see that: at 390x844 they
   read "tall screen" and open every gap on the one layout with nothing to
   give. Below the breakpoint the scale sits at its floor, which is where a
   phone already was. */
@media (max-width: 639px) {
  .ct-modal {
    --ct-bond: 6px;
    --ct-field: 10px;
    --ct-zone: 16px;
  }
}

/* ---- Scrim + window -----------------------------------------------------
   The window is a column that scrolls INTERNALLY. The page behind is locked
   rather than left scrollable, because a background that moves under a dialog
   is the single most disorienting thing a modal can do on a phone. */
body.ct-modal-open {
  position: fixed;
  width: 100%;
  overflow: hidden;
}

.ct-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  padding: var(--sp-4);
}
.ct-modal[hidden] { display: none; }

.ct-modal__scrim {
  position: absolute;
  inset: 0;
  background: rgba(3, 6, 12, .72);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
.ct-modal.is-in .ct-modal__scrim { opacity: 1; }

/* "An independent window" — it reads as its own surface floating over the
   site, not a takeover: inset from every edge, its own border and shadow. */
.ct-modal__window {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(720px, 100%);
  /* dvh, not vh: on mobile Safari `vh` is the LARGEST viewport (address bar
     retracted), so a 92vh window is taller than the screen the moment the bar
     is showing — which is exactly when someone first opens it. */
  max-height: min(92dvh, 940px);
  /* A safety valve that should never engage. The layout below is sized so the
     content always fits; if a future edit makes it too tall, scrolling is a
     far better failure than clipping the Submit button off the bottom. */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  background: linear-gradient(180deg, #0a1120 0%, var(--bg-deep) 60%);
  border: 1px solid var(--stroke-strong);
  border-radius: var(--r-xl);
  box-shadow: 0 40px 80px -20px rgba(0, 0, 0, .8), 0 0 0 1px rgba(80, 161, 249, .10);
  /* Contains the line plate. Without it the plate's compositing reached past
     the window and washed out the scrim's blur — the page behind snapped from
     softened to sharp, which reads as the modal having lost its backdrop. */
  isolation: isolate;
  opacity: 0;
  transform: translateY(12px) scale(.985);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.ct-modal.is-in .ct-modal__window { opacity: 1; transform: none; }
.ct-modal__window:focus { outline: none; }

/* The flex chain that makes "no scroll" hold at any height: body -> form ->
   the message field -> the textarea wrapper. Every link needs `min-height: 0`,
   because a flex item's default `min-height: auto` refuses to shrink below its
   content and the chain silently stops working. The message box is the only
   thing with `flex: 1`, so it is the single element that gives up space. */
.ct-modal__body {
  display: flex;
  flex-direction: column;
  /* NOT `min-height: 0`. That was here to make the grow chain work, and it is
     what let the body shrink BELOW its own content on short screens — the
     children then overlapped instead of overflowing, so the window's scrollbar
     never appeared and nothing looked wrong to a test that only asked "is the
     last element inside the window?". It wasn't; Submit sat 105px inside the
     message box on a 667px-tall phone.

     The message box does not need this to grow: its size comes from the
     `clamp()` flex-basis, not from distributed surplus. Without it the body
     stays at content height and the window's overflow-y takes over — which is
     the honest outcome when a heading, four fields, a submit and two tiles
     genuinely do not fit in 604px. */
  min-height: auto;
  padding: var(--ct-frame);
}

/* ABSOLUTE, top-right. This was `float: right` + `position: sticky`, which
   worked until the window became a flex column on 2026-07-28 — floats are
   IGNORED on flex items, so the button silently became an ordinary first
   child and sat top-LEFT. It looked deliberate, which is why it survived a
   screenshot review. Absolute positioning cannot be quietly cancelled by a
   parent's display mode. */
.ct-modal__close {
  position: absolute;
  /* One field-step in from the corner, so even the button that floats outside
     the content column is measured off the same scale as everything in it. */
  top: var(--ct-field);
  right: var(--ct-field);
  z-index: 3;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--stroke);
  border-radius: var(--r-pill);
  background: rgba(10, 16, 28, .8);
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.ct-modal__close svg { width: 18px; height: 18px; }
.ct-modal__close:hover { color: var(--text); border-color: var(--stroke-strong); }
.ct-modal__close:focus-visible { outline: 2px solid var(--vos-blue); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .ct-modal__scrim,
  .ct-modal__window { transition: none; }
  .ct-modal__window { transform: none; }
}

/* =========================================================================
   contact.css — /contact only. Figma frame 4305:276.

   Scoped under `.contact` (the <body> class) and `#contact-*` ids, same rule
   the section files follow, so it can never reach a page that hasn't opted in.

   DEVIATIONS FROM THE FIGMA FRAME, all deliberate — see HANDOFF §4c:
   - The MM/DD/YY field is GONE. It carried no label in the design and nothing
     on a contact form needs a date.
   - Every field has a real <label>. The frame used placeholder text as the
     label, which disappears the moment someone types.
   - The nav drops "Book a Demo". That frame predates the nav retarget and
     showed two competing CTAs; the live site settled on one.
   - The phone tile is gone until there is a real number — the frame's
     (501) 123-4567 is an Arkansas placeholder.
   ========================================================================= */


/* ---- Figma line-work (4305:277) ----------------------------------------
   The swervy plate from the frame. Figma draws it as a WHITE FILL at 3%
   opacity masked by the line pattern — not a coloured image, and not a
   gradient. Reproduced the same way: the pattern is the mask, the colour is
   a flat white, so it picks up the window's own background rather than
   painting a grey rectangle over it.

   A second, softer mask fades it out toward the form. The plate exists to
   give the top-right corner some movement behind the heading; over the
   fields it would just be noise behind text people have to read.

   `loading="lazy"` inside a `hidden` modal means the browser never fetches
   this until someone actually opens contact — so a 172 KB decorative plate
   costs nothing on page load. That is the only reason it is worth shipping
   at this size; fine line-work barely compresses, and quality 25 was within
   8 KB of quality 78. */
.ct-modal__lines {
  position: absolute;
  top: 0;
  right: 0;
  width: 78%;
  max-width: 620px;
  aspect-ratio: 1100 / 733;
  pointer-events: none;
  z-index: 0;
  /* Figma paints this as a WHITE FILL at low opacity MASKED by the line
     plate — the pattern is the stencil, not the ink. Built the same way here.
     The first attempt shipped the plate as an <img> with `filter: invert()`
     and `mix-blend-mode: screen`; that lit up the whole dialog and bled
     through to the scrim. A mask cannot do that — it only ever subtracts.

     Two masks intersected: the line-work, and a linear fade so the plate
     lives in the top-right corner and is gone before it reaches the fields.
     Over text it would just be noise behind something people must read. */
  background: #fff;
  opacity: .05;
  -webkit-mask-image: url("/assets/img/contact/lines.webp"),
                      linear-gradient(210deg, #000 0%, rgba(0, 0, 0, .5) 40%, transparent 74%);
  mask-image: url("/assets/img/contact/lines.webp"),
              linear-gradient(210deg, #000 0%, rgba(0, 0, 0, .5) 40%, transparent 74%);
  -webkit-mask-size: cover, cover;
  mask-size: cover, cover;
  -webkit-mask-repeat: no-repeat, no-repeat;
  mask-repeat: no-repeat, no-repeat;
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
  border-top-right-radius: var(--r-xl);
}
/* Everything the reader interacts with sits above the plate. */
.ct-modal__body { position: relative; z-index: 1; }

@media (prefers-reduced-motion: no-preference) {
  .ct-modal.is-in .ct-modal__lines { transition: opacity 420ms var(--ease) 60ms; }
}

/* ---- Head --------------------------------------------------------------- */
.ct-head {
  text-align: center;
  max-width: 640px;
  margin-inline: auto;
  /* One zone step below the frame. That is what clears the close button —
     the button's bottom edge lands at --ct-field + 40px, so a zone above the
     pill leaves a real gap rather than the 7px the old clamp produced, which
     is why "Contact Vaulted OS" looked pinned under the X instead of sitting
     at the top of its own block. */
  padding-top: var(--ct-zone);
  flex-shrink: 0;
}
/* The pill is a kicker, not a separate zone: bond it to the heading and let
   the whole header — pill, title, sub — read as one object with a zone of air
   under it. It used to sit 12.6px above the title and 9px above the sub, i.e.
   the same distance from the title as the title was from its own sub. */
.ct-head .vos-eyebrow-pill { margin-bottom: var(--ct-bond); }
/* Figma 4305:342 is 62px on a 260px box — that is --fs-display territory at
   desktop, but it must not blow past the viewport on a phone. */
.ct-h1 {
  font-size: clamp(1.75rem, 4.4vh, 3.25rem);
  font-weight: var(--fw-bold);
  line-height: 1.05;
  letter-spacing: -.02em;
  color: var(--text);
}
.ct-sub {
  margin-top: var(--ct-bond);
  /* Whichever of the two dimensions is scarcer. The title above it is sized
     off viewport HEIGHT — that is what this dialog is short of — so a sub
     locked to the width-based --fs-lead broke the pair apart on a wide, short
     screen: the title shrank and the line under it did not. Taking the vh term
     alone was worse, because it made the sub BIGGER on a phone (18.6px at
     390x844) and bought a second wrapped line on the one viewport that cannot
     afford it. min() keeps --fs-lead's width behaviour and adds a height
     ceiling: 19.8px at 1440x900, 16px on a phone, 16px on a 600px-tall
     laptop. */
  font-size: min(var(--fs-lead), max(1rem, 2.2vh));
  font-weight: var(--fw-light);
  line-height: 1.4;
  color: var(--text-muted);
}

/* ---- Form ---------------------------------------------------------------
   619px in Figma. The grid is one column on a phone and two for the
   name/email row above 640, which is where two 302px fields stop being
   thumb-hostile. */
.ct-form {
  max-width: none;
  margin: var(--ct-zone) auto 0;
  display: flex;
  flex-direction: column;
  min-height: auto;
  gap: var(--ct-field);
  width: 100%;
}
/* Only the message field grows and shrinks. */
/* This one — and ONLY this one — may shrink; it overrides the .ct-field pin. `min-height: 0` here is safe
   because the wrapper inside it carries the real 88px floor; the body and form
   above stay at `min-content` so they can never collapse under their children.
   That split is the whole fix: one element gives up space, nothing overlaps,
   and the window only scrolls once even the floor will not fit. */
.ct-field--grow { /* no longer special — the box below is sized directly */ }
.ct-row {
  flex-shrink: 0;
  display: grid;
  /* Same step both ways: side by side above 640, stacked below it, and either
     way these are two fields sitting next to each other. */
  gap: var(--ct-field);
}
@media (min-width: 640px) {
  .ct-row { grid-template-columns: 1fr 1fr; }
}

/* The bond: label -> its own control -> its own error line. This is the gap
   that has to be visibly SMALLER than --ct-field, or a label reads as floating
   between two fields rather than belonging to the one under it. */
.ct-field { display: flex; flex-direction: column; gap: var(--ct-bond); }

/* Real labels, not placeholders. The Figma frame used placeholder text as the
   only label — it vanishes on first keystroke and screen readers treat it
   inconsistently. Visible, small, and above the field. */
.ct-label {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
}
.ct-label .ct-req { color: var(--vos-blue); }
.ct-optional {
  font-weight: var(--fw-light);
  color: var(--text-dim);
}

/* Figma 4305:347: 63px tall, 10px radius, 20px padding, drop shadow
   0 4px 15px #050a14, and NO stroke — the edge you see is the shadow against
   the page. Reproduced with a hairline that carries the same read at 1x. */
.ct-input,
.ct-textarea {
  width: 100%;
  padding: 0 var(--sp-5);
  font-family: inherit;
  font-size: var(--fs-body);
  font-weight: var(--fw-light);
  line-height: 1.25;
  color: var(--text);
  background: var(--bg-panel);
  border: 1px solid var(--stroke);
  border-radius: 10px;
  box-shadow: 0 4px 15px #050a14;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
  -webkit-appearance: none;
  appearance: none;
}
.ct-input { height: clamp(46px, 6.2vh, 60px); }
.ct-textarea {
  /* NO min-height and NO fixed height — it fills the wrapper, and the wrapper
     is what flexes. A min-height here would re-introduce the overflow this
     whole layout exists to prevent. `resize` is off for the same reason:
     dragging it taller would push Submit off the bottom. */
  height: 100%;
  padding: var(--sp-3) var(--sp-5);
  border-radius: 18px;
  resize: none;
  position: relative;
  z-index: 1;
  display: block;
}
.ct-input::placeholder,
.ct-textarea::placeholder { color: var(--text-dim); }
.ct-input:hover,
.ct-textarea:hover { border-color: var(--stroke-strong); }
.ct-input:focus,
.ct-textarea:focus {
  outline: none;
  border-color: var(--vos-blue);
  background: var(--bg-panel-2);
}
/* Keyboard users still get a ring — :focus above only recolours the border,
   which is not enough on its own. */
.ct-input:focus-visible,
.ct-textarea:focus-visible {
  outline: 2px solid var(--vos-blue);
  outline-offset: 2px;
}
/* Field-level error state, set by the inline script on submit. */
.ct-input[aria-invalid="true"],
.ct-textarea[aria-invalid="true"] { border-color: var(--destructive, #ff6b6b); }
.ct-error {
  font-size: var(--fs-sm);
  color: var(--destructive, #ff6b6b);
  min-height: 0;
}
.ct-error:empty { display: none; }

/* The hologram wash inside the message box — Figma 4305:355, an image plate
   under a mask at 70%, NOT a CSS gradient. Sits behind the textarea's own
   transparent-ish fill and is clipped by the wrapper. */
.ct-textarea-wrap {
  position: relative;
  /* AN EXPLICIT HEIGHT, not a flex-grow. Four attempts at making this the
     flexible element all failed the same way: `min-height: 0` let the body
     shrink under its own children and they OVERLAPPED — silently, with no
     scrollbar, so a test asking "is the last element inside the window?"
     passed while Submit sat 105px inside the message box on a phone.
     `min-content` then refused to shrink at all and the window scrolled on
     desktop instead.

     A number the viewport derives is predictable, inspectable, and cannot
     collapse: 15vh keeps the whole dialog inside a 900px-tall laptop with
     nothing to scroll, and the 88px floor still reads as somewhere you write
     a paragraph. Taller screens get up to 190px. */
  height: clamp(88px, 15vh, 190px);
  flex: 0 0 auto;
  border-radius: 18px;
  overflow: hidden;
  isolation: isolate;
}
.ct-holo {
  position: absolute;
  left: 50%;
  bottom: -14%;
  width: 150%;
  transform: translateX(-50%);
  opacity: .55;
  pointer-events: none;
  z-index: 0;
  /* THE REAL FIGMA MASK, not a gradient. 4305:356 is "Ellipse 7428": a FLAT
     #D9D9D9 ellipse (rx 235.5, ry 47) inside a 911x534 box, softened by
     feGaussianBlur stdDeviation 110 — there is no gradient anywhere in it.

     This CSS previously used `radial-gradient(...)`, which is precisely the
     substitution SECTION-CONTRACT calls the single most repeated defect on
     this build: a gradient applies the falloff a second time on top of the
     blur, collapsing a broad wash into a hot centre. Caught by opening the
     asset instead of trusting the generated Tailwind, which had described it
     as a mask-image all along.

     mask-size matches the SVG's own 911x534 so the ellipse keeps its aspect;
     contain would letterbox it and stretch the falloff. */
  -webkit-mask-image: url("/assets/img/contact/hologram-mask.svg");
  mask-image: url("/assets/img/contact/hologram-mask.svg");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center bottom;
  mask-position: center bottom;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
}
/* The textarea must not paint over the wash, so its own fill is dropped here
   and the wrapper carries the surface instead. */
.ct-textarea-wrap .ct-textarea {
  background: transparent;
  box-shadow: none;
  border: 0;
}
.ct-textarea-wrap {
  background: var(--bg-panel);
  border: 1px solid var(--stroke);
  box-shadow: 0 4px 15px #050a14;
  transition: border-color var(--dur) var(--ease);
}
.ct-textarea-wrap:hover { border-color: var(--stroke-strong); }
.ct-textarea-wrap:focus-within { border-color: var(--vos-blue); }

/* ---- Submit -------------------------------------------------------------
   Figma 4305:360 stacks three copies of the button: two blurred glow layers
   (16.2px @ 5%, 8.2px @ 43%) under the real one. Reproduced as ::before /
   ::after so it stays one element in the DOM. */
.ct-submit {
  position: relative;
  width: 100%;
  height: clamp(44px, 5.6vh, 52px);
  /* No margin. It carried a 2px nudge that put Submit 14.6px under the
     message box while every other field sat 12.6px apart — a difference too
     small to be intentional and too large to be nothing. The form's own gap
     is the only thing that spaces this now. */
  flex-shrink: 0;
  font-family: inherit;
  font-size: 1.125rem;
  font-weight: var(--fw-semibold);
  color: var(--text);
  background: rgba(80, 161, 249, .10);
  border: 1px solid var(--vos-blue);
  border-radius: var(--r-md);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  cursor: pointer;
  transition: background var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.ct-submit::before,
.ct-submit::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--vos-blue);
  border-radius: var(--r-md);
  pointer-events: none;
}
.ct-submit::before { filter: blur(8.2px); opacity: .43; }
.ct-submit::after  { filter: blur(16.2px); opacity: .05; }
.ct-submit:hover { background: rgba(80, 161, 249, .18); }
.ct-submit:active { transform: translateY(1px); }
.ct-submit[disabled] { opacity: .55; cursor: progress; }

/* Honeypot. Off-screen rather than display:none — some bots skip hidden
   fields, and a positioned field still looks fillable to them. Never
   announced, never tabbable. */
.ct-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ---- Form status -------------------------------------------------------- */
.ct-status {
  /* It is a flex child of the form, so the form's gap already spaces it; the
     old margin stacked on top of that and pushed it a step and a half down. */
  padding: var(--sp-4) var(--sp-5);
  border-radius: var(--r-lg);
  font-size: var(--fs-body);
  line-height: 1.5;
}
.ct-status:empty { display: none; }
.ct-status[data-state="ok"] {
  border: 1px solid rgba(139, 227, 202, .45);
  background: rgba(139, 227, 202, .08);
  color: var(--vos-mint);
}
.ct-status[data-state="error"] {
  border: 1px solid rgba(255, 107, 107, .45);
  background: rgba(255, 107, 107, .08);
  color: #ff9a9a;
}

/* ---- Contact tiles ------------------------------------------------------
   Figma 4305:368 is three 237px tiles. Two ship — the phone tile is held
   until there is a real number. Auto-fit rather than a fixed 3-up so the row
   stays centred at two and needs no edit when the third returns. */
.ct-tiles {
  /* Flex, not `grid + auto-fit + minmax(237px, max-content)`. That track
     definition collapsed to a single column even with room for two, and left
     the pair visually off-centre. Flex-wrap centres honestly and only breaks
     to a second line when the tiles genuinely do not fit. */
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  /* Row gap is a field step: when the pair wraps on a phone, the second tile
     is a sibling under the first and should sit exactly as far from it as one
     form field does from the next. Column gap stays --sp-8; it is horizontal
     and answers to nothing in the vertical rhythm. */
  gap: var(--ct-field) var(--sp-8);
  /* A zone above the rule, a field below it. The hairline is the boundary, so
     the space that has to read as a zone change is the one before it; below
     it the tiles only need not to crowd their own divider. */
  margin: var(--ct-zone) auto 0;
  padding: var(--ct-field) 0 0;
  border-top: 1px solid var(--stroke);
  flex-shrink: 0;
  list-style: none;
}
.ct-tile {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
.ct-tile__icon {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: clamp(40px, 5vh, 52px);
  height: clamp(40px, 5vh, 52px);
  border: 1px solid var(--stroke);
  border-radius: var(--r-md);
  background: var(--bg-panel);
  color: var(--text);
}
.ct-tile__icon svg { width: 22px; height: 22px; }
.ct-tile__label {
  font-size: var(--fs-body);
  font-weight: var(--fw-semibold);
  color: var(--text);
}
.ct-tile__value {
  display: block;
  margin-top: var(--sp-1);
  font-size: var(--fs-body);
  font-weight: var(--fw-light);
  color: var(--text-muted);
  transition: color var(--dur) var(--ease);
}
a.ct-tile:hover .ct-tile__value,
a.ct-tile:focus-visible .ct-tile__value { color: var(--vos-blue); }

@media (prefers-reduced-motion: reduce) {
  .ct-modal * { transition: none !important; }
}
