/* GENERATED by scripts/build.js from each page module's `css` export. */
/* ---- home ---- */
/* ---- 360 viewer: the knot swatches ----
   The cyan ring follows aria-pressed, which main.js moves as the viewer swaps
   variants. Keep it out of the inline style on the button or it stops moving. */
[data-spin-thumb] { box-shadow: none; transition: box-shadow .2s ease; }
[data-spin-thumb][aria-pressed="true"] { box-shadow: 0 0 0 2px #F7F8FA, 0 0 0 4px #0EA5E9; }
[data-spin-thumb]:focus-visible { outline: none; box-shadow: 0 0 0 2px #F7F8FA, 0 0 0 4px #0EA5E9; }

/* ---- clients: rotating quotes ----
   Every slide sits in the SAME grid cell (grid-area:1/1, set by the renderer),
   so the band is as tall as the longest quote and swapping one for another
   moves nothing on the page.

   IN FROM THE LEFT, OUT TO THE LEFT. The incoming quote travels from -36px to
   0 as it fades up; the outgoing one travels back out to -36px as it fades
   down. Both motions happen on the same side, which is what makes it read as
   one strip of quotes moving past rather than two things crossing over.

   visibility, not display: display:none would drop the slide out of the grid
   and the band would collapse onto whichever quote is showing. The delayed
   visibility transition keeps the outgoing slide painted for the length of the
   fade, then takes it out of the tab order. */
[data-quote-slide] { opacity: 0; visibility: hidden; transform: translateX(-36px);
  transition: opacity .55s ease, transform .55s cubic-bezier(.22,.61,.36,1), visibility 0s linear .55s; }
[data-quote-slide][data-on] { opacity: 1; visibility: visible; transform: none;
  transition: opacity .55s ease, transform .55s cubic-bezier(.22,.61,.36,1), visibility 0s; }
[data-quote-dot]:hover { background: #46586E !important; }
[data-quote-dot]:focus-visible { outline: 2px solid #0EA5E9; outline-offset: 3px; }
/* Reduced motion keeps the swap but drops the travel: a cross-fade only. */
@media (prefers-reduced-motion: reduce) {
  [data-quote-slide] { transform: none; transition: opacity .2s linear, visibility 0s linear .2s; }
  [data-quote-slide][data-on] { transform: none; transition: opacity .2s linear, visibility 0s; }
}

/* ---- persona pills ----
   The [aria-pressed="true"] skin that used to live here is gone with the
   buttons: these are links into the pricing page now, and "pressed" was never
   a state any of them could reach. Hover picks up the accent instead, so they
   read as somewhere to go rather than something to select. */
[data-c="pill"] { transition: border-color .18s ease, background-color .18s ease, color .18s ease; }
[data-c="pill"]:hover,
[data-c="pill"]:focus-visible { border-color: #0EA5E9; background: #E9F6FE; color: #0369A1; }

/* ---- color swatches (hero knots, customizer knot/tie) ---- */
[data-c="swatch"]:hover { border-color: #0F172A; }

/* ==================================================================
   THE EDITOR
   Two columns: a pretend browser holding a pretend page, and an
   inspector that changes with whatever is selected in it. Everything
   here is scoped under a [data-ed-*] hook so none of it can leak into
   the rest of the page.
   ================================================================== */
[data-ed-frame] {
  border: 1px solid #E3E6EC; border-radius: 16px; overflow: hidden; background: #FFFFFF;
}
[data-ed-bar] {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 13px 18px; border-bottom: 1px solid #E3E6EC; background: #F7F8FA;
}
[data-ed-address] { font: 400 11px/1 'JetBrains Mono', monospace; color: #5A6478; }
[data-ed-badge] { font: 500 10px/1 'JetBrains Mono', monospace; letter-spacing: .2em; color: #0EA5E9; }
[data-ed-canvas] { padding: 26px 28px 32px; }
[data-ed-hint] { margin: 0 0 18px; font: 400 10px/1 'JetBrains Mono', monospace; letter-spacing: .18em; color: #8B94A7; }
[data-ed-block-label] { font: 500 10px/1 'JetBrains Mono', monospace; letter-spacing: .2em; color: #8B94A7; }

/* A block reads as pressable only once main.js has armed it — before that
   there is nothing to select, and a page that invites a click it cannot
   answer is worse than a static picture. */
[data-ed-block] { position: relative; padding: 14px; margin: 0 -14px; border-radius: 12px; outline: 1px solid transparent; transition: outline-color .16s ease, background-color .16s ease; }
[data-ed-block] + [data-ed-block] { margin-top: 22px; }
[data-editor-on] [data-ed-block] { cursor: pointer; }
[data-editor-on] [data-ed-block]:hover { outline-color: #CFE9FA; background: #FBFDFF; }
[data-editor-on] [data-ed-block][data-ed-on] { outline: 2px solid #0EA5E9; background: #FBFDFF; }
[data-ed-text] { outline: none; }
[data-ed-text]:focus-visible { outline: 2px solid #0EA5E9; outline-offset: 3px; border-radius: 4px; }

[data-ed-strip] { margin: 0; padding: 0; }
[data-ed-pic] { cursor: grab; transition: opacity .16s ease; touch-action: none; }
/* touch-action:none, or the browser claims the gesture for a page scroll and
   the pointer stream is canceled two frames in — the drag simply would not
   start on a phone. Safe here because these are small targets inside a much
   larger panel: there is plenty of canvas either side to scroll from. */
[data-ed-pic] img { pointer-events: none; -webkit-user-drag: none; user-select: none; }

/* --- what a drag looks like ---
   The ghost is what the pointer carries; the original stays where it is at
   reduced opacity so the row never moves until the drop. position:fixed keeps
   the ghost out of the canvas's clipping box, and pointer-events:none keeps it
   out of its own way — without it the thing under the cursor is always the
   ghost, and the drop target can never be worked out. */
[data-ed-ghost] {
  position: fixed; z-index: 60; pointer-events: none;
  border-radius: 10px; border: 1px solid rgba(255,255,255,.5);
  background-size: cover; background-position: center;
  opacity: .68; transform: scale(1.04) rotate(-1.4deg);
  box-shadow: 0 18px 40px rgba(15,23,42,.34);
}
/* The gap the picture would drop into. Absolutely positioned inside the strip,
   which is why the strip is position:relative. */
[data-ed-strip] { position: relative; }
[data-ed-drop] {
  position: absolute; z-index: 3; width: 3px; margin-left: -1.5px;
  border-radius: 999px; background: #0EA5E9;
  box-shadow: 0 0 0 3px rgba(14,165,233,.18);
  pointer-events: none;
}
/* The picture selection is remembered even while another block is selected —
   otherwise clicking away and back would lose your place. But it is only DRAWN
   when the strip is the selected block, so the page never shows two things
   ringed at once and leaves the reader guessing which one the inspector means. */
[data-ed-block][data-ed-on] [data-ed-pic][data-ed-pic-on] img { outline: 2px solid #0EA5E9; outline-offset: 2px; }
[data-ed-pic][data-ed-dragging] { opacity: .28; cursor: grabbing; }
[data-ed-pic][data-ed-dragging] img { outline: 2px dashed #CFE9FA; outline-offset: -2px; }
[data-ed-empty] { margin: 14px 0 0; font: 400 11px/1.6 'JetBrains Mono', monospace; color: #8B94A7; }

/* ---- the map ---- */
[data-ed-map] path { transition: fill .18s ease; }
[data-editor-on] [data-ed-map] path { cursor: pointer; }
/* Hover used to REPLACE the fill with #CFE9FA. Once the states carry a
   coverage tier that is a lie: hovering a fully-covered state showed it in
   the limited-coverage color, so the map contradicted its own legend under
   the cursor. An outline says "clickable" without overwriting the datum. */
[data-editor-on] [data-ed-map] path:hover { stroke: #0EA5E9; stroke-width: 1.6; }
[data-ed-map] path[data-ed-state-on] { stroke: #0F172A; stroke-width: 1.6; }

[data-ed-legend] { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 18px; margin: 16px 0 0; padding: 0; list-style: none; }
[data-ed-legend] li { display: flex; align-items: center; gap: 7px; font: 500 11px/1 'Space Grotesk', system-ui; color: #5A6478; }
[data-ed-legend-chip] { width: 11px; height: 11px; border-radius: 3px; border: 1px solid rgba(15,23,42,.18); }

/* ---- the inspector ----
   IT FOLLOWS THE SELECTION. It used to be sticky at the top of the viewport,
   which meant clicking the map at the bottom of a 900px canvas left the
   controls for it floating beside the heading — the reader clicks a thing and
   the answer appears somewhere else. It now rides a full-height rail and
   translates down to meet whatever block is selected (main.js, trackPanel).

   Sticky is gone rather than combined: a sticky box that is also translated
   is positioned twice and ends up off-screen at the bottom of a scroll. And
   aligning to the selection subsumes what sticky was for, because the block
   you just clicked is on screen by definition. */
[data-ed-rail] { position: relative; }
/* Phone furniture. The grip and the close button belong to the bottom dock
   and are simply not there on a desktop, where the inspector is a column
   beside the canvas with nothing to grip and nothing to close. */
[data-ed-grip], [data-ed-sheet-close] { display: none; }
[data-ed-inspect] {
  position: absolute; top: 0; left: 0; right: 0;
  border: 1px solid #E3E6EC; border-radius: 16px; background: #FFFFFF; overflow: hidden;
  transition: transform .32s cubic-bezier(.4, 0, .2, 1);
}
[data-ed-inspect-head] {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 14px 16px; border-bottom: 1px solid #E3E6EC; background: #F7F8FA;
}
[data-ed-inspect-head] > span { font: 500 10px/1 'JetBrains Mono', monospace; letter-spacing: .2em; color: #5A6478; }
[data-ed-reset] {
  padding: 7px 11px; border: 1px solid #DDE2E9; border-radius: 8px; background: #FFFFFF;
  font: 500 11px/1 'Space Grotesk', system-ui; color: #5A6478; cursor: pointer;
  transition: border-color .16s ease, color .16s ease;
}
[data-ed-reset]:hover { border-color: #0F172A; color: #0F172A; }
[data-ed-panels] { padding: 16px; }
[data-ed-panel-title] { margin: 0 0 4px; font: 600 15px/1.3 'Space Grotesk', system-ui; color: #0F172A; }
[data-ed-note] { margin: 0 0 14px; font: 400 12px/1.55 'Space Grotesk', system-ui; color: #8B94A7; text-wrap: pretty; }
[data-ed-field] { margin-top: 14px; }
/* The field's CAPTION — "Colour", "Size", "State". :not() because the custom
   color picker below is also a direct-child <label> of a field, and without
   the exclusion it inherited the caption's mono face, .2em tracking and
   display:block, which flattened its flex row and made a control look like a
   heading. It is a control, and it says so. */
[data-ed-field] > span:not([data-ed-custom]), [data-ed-field] > label:not([data-ed-custom]) { display: block; margin-bottom: 7px; font: 500 10px/1 'JetBrains Mono', monospace; letter-spacing: .2em; color: #5A6478; }
[data-ed-swatches] { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
[data-ed-colour] { transition: transform .14s ease, box-shadow .14s ease; }
[data-ed-colour]:hover { transform: scale(1.12); }
[data-ed-colour][aria-pressed="true"] { box-shadow: 0 0 0 2px #FFFFFF, 0 0 0 4px #0EA5E9; }

/* The custom picker: a labeled row under the presets. The <label> is the
   hit target, so the whole strip opens the picker rather than a 22px circle.
   The three ::-*-swatch rules strip the platform's fat border off the native
   input so the chip matches the swatches above it. */
[data-ed-custom] {
  display: flex; align-items: center; gap: 9px; margin-top: 9px; padding: 7px 10px;
  border: 1px solid #DDE2E9; border-radius: 9px; cursor: pointer;
  font: 500 12px/1 'Space Grotesk', system-ui; color: #5A6478;
  transition: border-color .16s ease, color .16s ease;
}
[data-ed-custom]:hover { border-color: #0F172A; color: #0F172A; }
[data-ed-custom]:focus-within { border-color: #0EA5E9; box-shadow: inset 0 0 0 1px #0EA5E9; }
[data-ed-colour-custom] {
  flex: 0 0 auto; width: 22px; height: 22px; padding: 0;
  border: 1px solid rgba(15,23,42,.18); border-radius: 999px; background: #FFFFFF; cursor: pointer;
}
[data-ed-colour-custom]:focus-visible { outline: none; }
[data-ed-colour-custom]::-webkit-color-swatch-wrapper { padding: 0; }
[data-ed-colour-custom]::-webkit-color-swatch { border: 0; border-radius: 999px; }
[data-ed-colour-custom]::-moz-color-swatch { border: 0; border-radius: 999px; }

/* ---- the map's coverage tiers ---- */
[data-ed-tiers] { display: grid; gap: 6px; }
[data-ed-tier-btn] {
  display: flex; align-items: center; gap: 9px; width: 100%; padding: 9px 11px;
  border: 1px solid #DDE2E9; border-radius: 9px; background: #FFFFFF;
  font: 500 12px/1 'Space Grotesk', system-ui; color: #0F172A; text-align: left; cursor: pointer;
  transition: border-color .16s ease, box-shadow .16s ease;
}
[data-ed-tier-btn]:hover { border-color: #0F172A; }
[data-ed-tier-btn][aria-pressed="true"] { border-color: #0EA5E9; box-shadow: inset 0 0 0 1px #0EA5E9; }
[data-ed-tier-btn][disabled] { opacity: .45; cursor: not-allowed; }
[data-ed-tier-btn][disabled]:hover { border-color: #DDE2E9; }
[data-ed-tier-chip] { flex: 0 0 auto; width: 15px; height: 15px; border-radius: 4px; border: 1px solid rgba(15,23,42,.18); }

[data-ed-segs], [data-ed-pic-actions] { display: flex; gap: 6px; }
[data-ed-seg] {
  flex: 1 1 0; min-width: 0; padding: 8px 6px;
  border: 1px solid #DDE2E9; border-radius: 8px; background: #FFFFFF;
  font: 500 12px/1 'Space Grotesk', system-ui; color: #5A6478; cursor: pointer;
  transition: border-color .16s ease, background-color .16s ease, color .16s ease;
}
[data-ed-seg]:hover { border-color: #0F172A; color: #0F172A; }
[data-ed-seg][aria-pressed="true"] { border-color: #0EA5E9; background: #E9F6FE; color: #0369A1; }
[data-ed-seg][disabled] { opacity: .4; cursor: not-allowed; }

[data-ed-primary], [data-ed-danger] {
  display: block; width: 100%; margin-top: 10px; padding: 10px;
  border-radius: 9px; font: 600 13px/1 'Space Grotesk', system-ui; cursor: pointer;
  transition: background-color .16s ease, border-color .16s ease, color .16s ease;
}
[data-ed-primary] { border: 0; background: #0EA5E9; color: #FFFFFF; }
[data-ed-primary]:hover { background: #0284C7; }
[data-ed-danger] { border: 1px solid #DDE2E9; background: #FFFFFF; color: #5A6478; }
[data-ed-danger]:hover { border-color: #B4232A; color: #B4232A; }
[data-ed-danger][disabled] { opacity: .4; cursor: not-allowed; }
[data-ed-danger][disabled]:hover { border-color: #DDE2E9; color: #5A6478; }

[data-ed-gallery] { margin-top: 14px; border-top: 1px solid #E3E6EC; padding-top: 12px; }
[data-ed-gallery-head] { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
[data-ed-gallery-head] span { font: 500 10px/1 'JetBrains Mono', monospace; letter-spacing: .2em; color: #5A6478; }
[data-ed-gallery-close] { width: 26px; height: 26px; padding: 0; border: 1px solid #DDE2E9; border-radius: 999px; background: #FFFFFF; color: #5A6478; font-size: 15px; line-height: 1; cursor: pointer; }
[data-ed-gallery-close]:hover { border-color: #0F172A; color: #0F172A; }
[data-ed-gallery-grid] { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
[data-ed-gallery-grid] button { transition: outline-color .14s ease; outline: 2px solid transparent; outline-offset: 1px; }
[data-ed-gallery-grid] button:hover { outline-color: #0EA5E9; }

/* ---- the state picker ----
   A disclosure button and a list that scrolls inside the panel. See the
   renderer for why this is not a <select>. */
[data-ed-state-picker] { position: relative; }
[data-ed-state-toggle] {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; padding: 9px 10px; border: 1px solid #DDE2E9; border-radius: 8px;
  background: #FFFFFF; font: 400 13px/1.2 'Space Grotesk', system-ui; color: #0F172A;
  text-align: left; cursor: pointer; transition: border-color .16s ease, box-shadow .16s ease;
}
[data-ed-state-toggle]:hover { border-color: #0F172A; }
[data-ed-state-toggle][aria-expanded="true"] { border-color: #0EA5E9; box-shadow: inset 0 0 0 1px #0EA5E9; }
[data-ed-state-placeholder] { color: #8B94A7; }
[data-ed-state-current] { font-weight: 600; }
[data-ed-state-caret] {
  flex: 0 0 auto; width: 0; height: 0;
  border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid #8B94A7;
  transition: transform .16s ease;
}
[data-ed-state-toggle][aria-expanded="true"] [data-ed-state-caret] { transform: rotate(180deg); }
/* Capped and scrollable. Fifty states unrolled inside a bottom dock would
   push every other control off the screen, which is the failure the native
   picker had in a different costume. */
[data-ed-state-list] {
  max-height: 184px; margin-top: 6px; overflow-y: auto; -webkit-overflow-scrolling: touch;
  border: 1px solid #DDE2E9; border-radius: 8px; background: #FFFFFF;
}
[data-ed-state-list] > button {
  display: block; width: 100%; padding: 9px 10px;
  border: 0; border-bottom: 1px solid #F1F3F6; background: transparent;
  font: 400 13px/1.2 'Space Grotesk', system-ui; color: #0F172A; text-align: left; cursor: pointer;
}
[data-ed-state-list] > button:last-child { border-bottom: 0; }
[data-ed-state-list] > button:hover { background: #F1F7FC; }
[data-ed-state-list] > button[aria-selected="true"] { background: #E9F6FE; color: #0369A1; font-weight: 600; }

/* ---- the keyboard toggle ----
   Full width and stated plainly, because on a phone it is the way back out
   of the on-screen keyboard and has to be findable without a hunt. */
[data-ed-kb] {
  display: flex; align-items: center; justify-content: center; width: 100%;
  margin-top: 14px; padding: 9px 10px;
  border: 1px solid #DDE2E9; border-radius: 9px; background: #FFFFFF;
  font: 500 12px/1 'Space Grotesk', system-ui; color: #5A6478; cursor: pointer;
  transition: border-color .16s ease, background-color .16s ease, color .16s ease;
}
[data-ed-kb]:hover { border-color: #0F172A; color: #0F172A; }
[data-ed-kb][aria-pressed="true"] { border-color: #0EA5E9; background: #E9F6FE; color: #0369A1; }

@media (prefers-reduced-motion: reduce) {
  [data-ed-block], [data-ed-map] path, [data-ed-colour], [data-ed-custom],
  [data-ed-seg], [data-ed-pic], [data-ed-tier-btn], [data-ed-inspect],
  [data-ed-state-caret] { transition: none !important; }
  [data-ed-colour]:hover { transform: none; }
}

/* ==================================================================
   THE SPINE STAGE
   Dark band, so everything here is authored against #0B1220 rather than
   against the light surfaces the rest of the page uses.
   ================================================================== */
/* No top margin on any of the three. This was 44px back when the goblin was a
   band of his own and needed room under the section header; inside a tab strip
   it collapses over the strip's own bottom margin and makes that one tab 18px
   taller than the other two — the strip provides the gap now. */
[data-goblin], [data-sk], [data-pt] { margin-top: 0; }
[data-goblin-stage] {
  position: relative;
  /* Wider than it is tall: the panel sits beside him now, not above him, so the
     vertical room the first version needed is dead space. The driver re-applies
     this from its own config so the tuning page can change it live. */
  aspect-ratio: 2.3 / 1;
  border: 1px solid #1C2430;
  border-radius: 16px;
  background: radial-gradient(120% 90% at 50% 10%, #131C2B 0%, #0C1421 70%);
  overflow: hidden;
}
[data-goblin-canvas] { position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none; }

/* --- Shin'Kari ---------------------------------------------------------
   The driver re-applies its own ratio on load, so this is the pre-load box —
   it has to match, or the gate is a different shape from the stage behind it. */
[data-sk-stage] {
  position: relative;
  aspect-ratio: 2.3 / 1;
  border: 1px solid #1C2430;
  border-radius: 16px;
  background: radial-gradient(90% 120% at 50% 0%, #141E2E 0%, #0B1220 72%);
  /* clip, NOT hidden. An overflow:hidden box is a scroll container, so Chromium
     ends the wheel-scroll chain there — the page would sit dead under the panel
     even on the frames where the driver deliberately does not preventDefault.
     overflow:clip crops identically without creating the container. */
  overflow: clip;
  touch-action: pan-y;          /* the page still scrolls under a touch drag */
  cursor: ns-resize;
  outline-offset: 3px;
}
[data-sk-canvas] { position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none; }

/* The only thing that travels. Faint rungs, far enough apart to read as
   distance without turning into a moire when he moves fast. */
[data-sk-wall] {
  position: absolute; inset: -20% 0;
  pointer-events: none;
  background-image: repeating-linear-gradient(180deg,
    rgba(255,255,255,0.055) 0 2px, rgba(255,255,255,0) 2px 74px);
  background-position-y: 0;
  mask-image: linear-gradient(180deg, transparent 0, #000 18%, #000 82%, transparent 100%);
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 18%, #000 82%, transparent 100%);
}

[data-sk-start] {
  position: absolute; inset: 0; z-index: 3;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
  background: rgba(9, 14, 24, 0.55); -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
[data-sk-start][hidden] { display: none !important; }
[data-sk-go] {
  padding: 13px 24px; border: 1px solid #38BDF8; border-radius: 10px;
  background: rgba(56,189,248,0.14); color: #E6EBF3;
  font: 500 15px/1 'Space Grotesk', system-ui; cursor: pointer;
}
[data-sk-start-note] { font: 400 11px/1 'JetBrains Mono', monospace; color: #7C8AA0; }

/* Said once, dismissed by the first scroll. */
[data-sk-hint] {
  position: absolute; left: 50%; bottom: 22px; transform: translateX(-50%); z-index: 2;
  display: flex; align-items: center; gap: 8px;
  padding: 7px 13px; border: 1px solid rgba(255,255,255,0.14); border-radius: 999px;
  background: rgba(10,16,28,0.72); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  font: 400 11px/1 'JetBrains Mono', monospace; color: #C7D2E0;
  pointer-events: none; animation: skbob 2.2s ease-in-out infinite;
}
[data-sk-hint][hidden] { display: none !important; }
[data-sk-arrow] {
  width: 9px; height: 9px; border-left: 1.5px solid #38BDF8; border-top: 1.5px solid #38BDF8;
  transform: rotate(45deg); display: block;
}
@keyframes skbob { 0%, 100% { transform: translate(-50%, 0); } 50% { transform: translate(-50%, -5px); } }
@media (prefers-reduced-motion: reduce) { [data-sk-hint] { animation: none; } }

/* How far up the rope he is. */
[data-sk-meter] {
  position: absolute; right: 14px; top: 16px; bottom: 16px; width: 3px; z-index: 2;
  border-radius: 3px; background: rgba(255,255,255,0.07); pointer-events: none;
}
[data-sk-meter-fill] { position: absolute; left: 0; right: 0; bottom: 0; height: 50%; border-radius: 3px; background: #38BDF8; display: block; }

[data-sk-loading], [data-sk-fail] {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); z-index: 4;
  font: 400 12px/1.5 'JetBrains Mono', monospace; color: #9FB0C6; text-align: center; max-width: 80%;
}
[data-sk-loading][hidden], [data-sk-fail][hidden] { display: none !important; }
[data-sk-stage-label] {
  position: absolute; right: 14px; bottom: 12px;
  font: 500 10px/1 'JetBrains Mono', monospace; letter-spacing: 0.16em; color: #4C596B;
}
[data-sk-bar] { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin-top: 16px; min-height: 36px; flex-wrap: wrap; }
[data-sk-bar][hidden] { display: none !important; }
[data-sk-status] { margin: 0; font: 400 11px/1.5 'JetBrains Mono', monospace; color: #A7B0C0; }
[data-sk-actions] { display: flex; align-items: center; gap: 8px; }
[data-sk-btn] {
  padding: 8px 14px; border: 1px solid rgba(255,255,255,0.18); border-radius: 8px;
  background: rgba(255,255,255,0.04); color: #E6EBF3;
  font: 500 12px/1 'Space Grotesk', system-ui; cursor: pointer;
}
[data-sk-note] { margin: 18px 0 0; max-width: 700px; font: 400 13px/1.7 'Space Grotesk', system-ui; color: #6B7688; text-wrap: pretty; }

/* --- the palm tree ------------------------------------------------------ */
[data-pt-stage] {
  position: relative;
  aspect-ratio: 2.3 / 1;
  border: 1px solid #1C2430;
  border-radius: 16px;
  background: radial-gradient(80% 70% at 50% 100%, #16233A 0%, #0B1220 70%);
  overflow: clip;
  touch-action: none;          /* the whole point of this panel is the drag */
  cursor: grab;
  outline-offset: 3px;
}
[data-pt-stage][data-pt-held] { cursor: grabbing; }
[data-pt-canvas] { position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none; }
[data-pt-sky] {
  position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(38% 26% at 50% 88%, rgba(56,189,248,0.10) 0%, rgba(56,189,248,0) 70%);
}
[data-pt-start] {
  position: absolute; inset: 0; z-index: 3;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
  background: rgba(9, 14, 24, 0.55); -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
[data-pt-start][hidden] { display: none !important; }
[data-pt-go] {
  padding: 13px 24px; border: 1px solid #38BDF8; border-radius: 10px;
  background: rgba(56,189,248,0.14); color: #E6EBF3;
  font: 500 15px/1 'Space Grotesk', system-ui; cursor: pointer;
}
[data-pt-start-note] { font: 400 11px/1 'JetBrains Mono', monospace; color: #7C8AA0; }
[data-pt-hint] {
  position: absolute; left: 50%; top: 20px; transform: translateX(-50%); z-index: 2;
  padding: 7px 13px; border: 1px solid rgba(255,255,255,0.14); border-radius: 999px;
  background: rgba(10,16,28,0.72); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  font: 400 11px/1 'JetBrains Mono', monospace; color: #C7D2E0; pointer-events: none;
}
[data-pt-hint][hidden] { display: none !important; }
[data-pt-loading], [data-pt-fail] {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); z-index: 4;
  font: 400 12px/1.5 'JetBrains Mono', monospace; color: #9FB0C6; text-align: center; max-width: 80%;
}
[data-pt-loading][hidden], [data-pt-fail][hidden] { display: none !important; }
[data-pt-stage-label] {
  position: absolute; right: 14px; bottom: 12px;
  font: 500 10px/1 'JetBrains Mono', monospace; letter-spacing: 0.16em; color: #4C596B;
}
[data-pt-bar] { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin-top: 16px; min-height: 36px; flex-wrap: wrap; }
[data-pt-bar][hidden] { display: none !important; }
[data-pt-status] { margin: 0; font: 400 11px/1.5 'JetBrains Mono', monospace; color: #A7B0C0; }
[data-pt-actions] { display: flex; align-items: center; gap: 8px; }
[data-pt-btn] {
  padding: 8px 14px; border: 1px solid rgba(255,255,255,0.18); border-radius: 8px;
  background: rgba(255,255,255,0.04); color: #E6EBF3;
  font: 500 12px/1 'Space Grotesk', system-ui; cursor: pointer;
}

/* The panel he is after. Positioned in px by the driver once it starts; until
   then it sits where these percentages put it, so the still stage is composed
   rather than empty. */
[data-goblin-panel] {
  position: absolute; left: 6%; top: 30%; width: 26%; min-width: 180px;
  padding: 0 0 16px;
  border: 1px solid #E3E6EC; border-radius: 14px;
  background: #FFFFFF;
  overflow: hidden;
  box-shadow: 0 18px 50px rgba(0,0,0,.42);
}
[data-goblin-panel-thumb] { height: 96px; background: #E7EBF1; border-bottom: 1px solid #E3E6EC; }
[data-goblin-panel-lines] { padding: 16px 18px 0; }
[data-goblin-panel-lines] i { display: block; height: 9px; border-radius: 4px; background: #E7EBF1; }
[data-goblin-panel-lines] i + i { margin-top: 8px; width: 62%; background: #EEF1F5; }

[data-goblin-hit], [data-goblin-decoy-hit] { position: absolute; cursor: pointer; background: transparent; }
[data-goblin-hit][hidden], [data-goblin-decoy-hit][hidden] { display: none !important; }

/* [hidden] restated on both of these, and it is not belt-and-braces.
   An inline or authored display property outranks the UA stylesheet's
   rule for [hidden], so a flex container that starts hidden — or gets
   hidden later — stays on screen with the attribute set and the property
   reading true. The start gate sat over the stage for the whole demo before
   this line existed, and every DOM assertion about it passed. */
[data-goblin-start][hidden], [data-goblin-bar][hidden] { display: none !important; }

[data-goblin-start] {
  position: absolute; inset: 0; z-index: 3;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 13px;
  background: rgba(11,18,32,.55);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
[data-goblin-go] {
  padding: 14px 24px; border: 0; border-radius: 999px;
  background: #0EA5E9; color: #FFFFFF;
  font: 600 16px/1 'Space Grotesk', system-ui; cursor: pointer;
  box-shadow: 0 10px 30px rgba(14,165,233,.32);
  transition: background-color .16s ease, transform .16s ease;
}
[data-goblin-go]:hover { background: #0284C7; transform: scale(1.03); }
[data-goblin-start-note] { font: 400 10px/1 'JetBrains Mono', monospace; letter-spacing: .12em; color: #8B94A7; }

[data-goblin-loading], [data-goblin-fail] {
  position: absolute; left: 50%; top: 50%; z-index: 3; transform: translate(-50%,-50%);
  padding: 10px 15px; border-radius: 999px; background: rgba(11,18,32,.86);
  font: 400 11px/1.5 'JetBrains Mono', monospace; color: #A7B0C0; text-align: center; max-width: 80%;
}
[data-goblin-stage-label] {
  position: absolute; right: 14px; bottom: 12px; z-index: 2;
  font: 500 10px/1 'JetBrains Mono', monospace; letter-spacing: .2em; color: #3C4757;
}

[data-goblin-bar] {
  display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap;
  margin-top: 16px;
}
[data-goblin-status] { margin: 0; font: 400 11px/1.5 'JetBrains Mono', monospace; color: #A7B0C0; }
/* The paused note sits alongside whichever state span is live, so it needs its
   own gap — without it the two run together as one unreadable sentence. */
[data-goblin-paused] { margin-left: 10px; color: #F0B429; }
[data-goblin-paused][hidden] { display: none !important; }
[data-goblin-actions] { display: flex; align-items: center; gap: 10px; }
[data-goblin-score] { font: 500 10px/1 'JetBrains Mono', monospace; letter-spacing: .16em; color: #5A6478; margin-right: 6px; }
[data-goblin-score] b { color: #38BDF8; font-weight: 700; }
[data-goblin-btn] {
  padding: 9px 14px; border: 1px solid #24303F; border-radius: 9px;
  background: transparent; color: #A7B0C0;
  font: 500 12px/1 'Space Grotesk', system-ui; cursor: pointer;
  transition: border-color .16s ease, color .16s ease;
}
[data-goblin-btn]:hover { border-color: #38BDF8; color: #F4F7FB; }
[data-goblin-note] { margin: 22px 0 0; max-width: 700px; font: 400 13px/1.65 'Space Grotesk', system-ui; color: #5A6478; text-wrap: pretty; }

/* ---- the 360 turntable ----
   Spec from the export's STATES & SPECS board: idle is cursor:grab, dragging
   is cursor:grabbing with a 1px accent inner rule. :active stands in for
   "dragging" until the real viewer lands and can set the state itself. */
[data-turntable] { cursor: grab; }
[data-turntable]:active { cursor: grabbing; box-shadow: inset 0 0 0 1px rgba(14,165,233,0.55); }

/* The poster is frame 1 as a real <img>. It is what ships, and what a visitor
   sees with JS blocked; the canvases only replace it once the viewer has
   actually started. Same static-art-then-upgrade shape as the drifting grid. */
[data-spin] [data-spin-poster] { transition: opacity .3s ease; }
[data-spin][data-spin-on] [data-spin-poster] { opacity: 0; pointer-events: none; }
[data-spin]:not([data-spin-on]) [data-spin-load] { display: none; }

/* State readout: idle by default, DRAGGING while the pointer is down. */
[data-spin] [data-spin-state="drag"] { display: none; }
[data-spin][data-spin-dragging] [data-spin-state="idle"] { display: none; }
[data-spin][data-spin-dragging] [data-spin-state="drag"] { display: inline; }

/* One stage per knot, stacked. The swap slides the outgoing one out to the
   right while the incoming enters from the left — 130% so neither is ever
   half-visible at the edge mid-transition. */
[data-spin] .spin-stage {
  position: absolute; inset: 0; display: grid; place-items: center;
  transition: transform .8s cubic-bezier(.16,1,.3,1), opacity .8s cubic-bezier(.16,1,.3,1);
}
[data-spin] .spin-stage.no-anim { transition: none; }
[data-spin] .spin-stage.is-off-l { transform: translateX(-130%); opacity: 0; }
[data-spin] .spin-stage.is-off-r { transform: translateX(130%); opacity: 0; }
[data-spin] .spin-stage canvas { width: 100%; height: 100%; object-fit: contain; touch-action: pan-y; }
[data-spin] [data-spin-load] { transition: opacity .4s ease; }

@media (prefers-reduced-motion: reduce) {
  [data-spin] .spin-stage { transition: none; }
}

/* ---- tabs ----
   The export drew selected and unselected as two hardcoded branches. Here the
   skin hangs off aria-selected, which main.js already maintains. */
/* The strip carries data-scroll-x for the phone, but the scroll itself is
   switched on at the breakpoint: on desktop an overflow:auto box would clip the
   selected tab's underline, which sits 1px below the strip's content edge. */
[data-c="tab-strip"] { overflow-x: visible; }
[data-c="tab"] [data-tab-label] { font: 500 15px/1 'Space Grotesk', system-ui; color: #5A6478; }
/* The dark variant. Same component, same behavior in main.js — only the two
   text colors and the rule under the strip differ. */
[data-c="tab"][data-c-dark] [data-tab-label] { color: #8B94A7; }
[data-c="tab"][data-c-dark]:hover [data-tab-label] { color: #E6EBF3; }
[data-c="tab"][data-c-dark][aria-selected="true"] [data-tab-label] { font-weight: 600; color: #F4F7FB; }
/* THE THREE NOTES RESERVE THE SAME BOX. The stages already match; if the
   paragraph under them does not, switching tab still slides everything below
   the section up and down. The tallest of the three sets the floor. */
[data-goblin-bar], [data-sk-bar], [data-pt-bar] { min-height: 36px; }
[data-goblin-note], [data-sk-note], [data-pt-note] {
  margin: 18px 0 0; max-width: 700px; min-height: 66px;
  font: 400 13px/1.7 'Space Grotesk', system-ui; color: #6B7688; text-wrap: pretty;
}
[data-c="tab"]:hover [data-tab-label] { color: #0F172A; }
[data-c="tab"][aria-selected="true"] [data-tab-label] { font-weight: 600; color: #0F172A; }
[data-c="tab"] [data-tab-underline] { display: none; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; background: #0EA5E9; }
[data-c="tab"][aria-selected="true"] [data-tab-underline] { display: block; }

/* ---- the editable demo block ---- */
[data-editable]:hover { outline-color: #0EA5E9; }
[data-editable]:focus-visible { outline: 2px solid #0EA5E9; }

/* ---- FAQ rows ----
   One sign is hidden at a time, so the row still reads +/– with JS blocked. */
[data-c="faq-row"] > summary:hover { border-bottom-color: #0F172A; background: #FBFCFD; }
[data-c="faq-row"] [data-sign-open] { display: none; }
[data-c="faq-row"][open] [data-sign-open] { display: inline; }
[data-c="faq-row"][open] [data-sign-closed] { display: none; }

/* ---- pricing ---- */
/* ---- tier cards ---- */
[data-c="tier-card"] { transition: border-color .18s ease, box-shadow .18s ease; }
[data-c="tier-card"]:hover { border-color: #0EA5E9; box-shadow: 0 10px 24px rgba(15,23,42,0.06); }

/* The tier CTA hovers to ink, not to accent — a card full of accent already has
   one accent button in it and a second would flatten the recommended tier.
   site.css's shared btn-secondary rule hovers to accent, so this overrides it
   inside a tier card only, where the export asks for #0F172A. */
[data-c="tier-card"] [data-c="btn-secondary"]:hover { border-color: #0F172A; color: #0F172A; }

/* The excluded rows' "Not included" text is visually hidden inline (SR_ONLY at
   the top of this module) rather than by a class here, so it is correct on a
   --only build that has not regenerated this stylesheet. */

/* ---- work ---- */
/* ---- the compare toggle ----
   Skins hang off aria-selected, which main.js maintains, instead of the
   export's two hardcoded sc-if branches. Color only: the mono 10px/0.2em face
   is inline on the label, so a stale pages.css cannot leave these tabs looking
   like the showcase strip's 15px Space Grotesk ones. */
[data-compare] [data-c="tab"] [data-tab-label] { color: #5A6478; }
[data-compare] [data-c="tab"]:hover [data-tab-label] { color: #0F172A; }
[data-compare] [data-c="tab"][aria-selected="true"] [data-tab-label] { color: #0F172A; }

/* The 180ms cross-state fade the export animates the BEFORE fill with. Declared
   here rather than in site.css: this page is the only thing that uses it. */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  [data-compare-fill] { animation: none !important; }
}

/* ---- the slideshow inside each state ----
   The transition lives here and not on the inline style so main.js can turn it
   off for the duration of a drag by setting a single attribute on the panel:
   during a drag the slides must follow the finger exactly, and a 460ms ease on
   transform would make them lag behind it. */
[data-slideshow] [data-slide] {
  transition: opacity 460ms cubic-bezier(.4,0,.2,1), transform 460ms cubic-bezier(.4,0,.2,1);
  will-change: opacity, transform;
}
[data-slideshow][data-slide-drag] [data-slide] { transition: none; }
[data-slideshow]:active { cursor: grabbing; }

/* Light, as asked: legible on a white hero and on a dark one, and not competing
   with the screenshot underneath until someone reaches for it. */
[data-slideshow] [data-slide-nav] {
  opacity: 0;
  box-shadow: 0 1px 3px rgba(15,23,42,.14);
  transition: opacity 200ms ease, background-color 160ms ease;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
/* OUT OF THE WAY UNTIL SOMEONE REACHES FOR THEM. These sit ON the screenshot,
   and the screenshot is the thing the row exists to show — two permanent
   buttons over it are two permanent holes in the work. They come back on
   hover, on keyboard focus, and (data-slide-touch, set by main.js) while a
   finger is on the picture.

   Never display:none on the way out: a control removed from the tree
   cannot be tabbed to, and the keyboard route is the only one a screen-reader
   user has. Opacity keeps it focusable, and :focus-within brings it back the
   instant it is tabbed to. */
[data-slideshow]:hover [data-slide-nav],
[data-slideshow]:focus-within [data-slide-nav],
[data-slideshow][data-slide-touch] [data-slide-nav] { opacity: .82; }
[data-slideshow] [data-slide-nav]:hover,
[data-slideshow] [data-slide-nav]:focus-visible { opacity: 1; background: rgba(255,255,255,.86); }

/* The pips ride on a translucent slate pill rather than directly on the
   screenshot. Every capture behind them is a website, and websites are mostly
   white at the bottom of the fold — white pips on white is no indicator at all,
   and dark pips would then disappear on the two captures that are not. The pill
   makes the contrast the component's own problem instead of the content's. */
[data-slide-dots] {
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}
[data-slide-dots] [data-slide-dot] {
  display: block; width: 5px; height: 5px; border-radius: 999px;
  background: rgba(255,255,255,.5);
  transition: width 220ms cubic-bezier(.4,0,.2,1), background-color 220ms ease;
}
[data-slide-dots] [data-slide-dot-on] { width: 16px; background: #FFFFFF; }

/* A phone cannot hover, so touching the picture is what reveals them — main.js
   sets data-slide-touch on pointerdown and clears it shortly after the finger
   leaves. The rule above covers it; this block used to pin them permanently
   visible on every touch device, which is the thing being fixed. */

@media (prefers-reduced-motion: reduce) {
  [data-slideshow] [data-slide],
  [data-slide-dots] [data-slide-dot] { transition: none !important; }
}

/* ---- the cost breakdown ----
   Scoped to [data-cost-dialog] throughout, because a <dialog> is in the top
   layer and inherits nothing useful from where it happens to sit in the DOM. */
[data-cost-open]:hover span:first-child,
[data-cost-open]:focus-visible span:first-child { text-decoration: underline; text-underline-offset: 3px; }

[data-cost-dialog] {
  width: min(620px, calc(100vw - 40px));
  max-height: min(84vh, 760px);
  padding: 0;
  border: 0;
  border-radius: 18px;
  background: #FFFFFF;
  color: #0F172A;
  box-shadow: 0 24px 70px rgba(15,23,42,.28);
  overflow: hidden;
}
[data-cost-dialog]::backdrop {
  background: rgba(15,23,42,.55);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
}
[data-cost-dialog][open] { display: flex; flex-direction: column; }

[data-cost-head] {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 20px;
  flex: none;
  padding: 22px 24px 18px;
  border-bottom: 1px solid #E6EAF0;
}
[data-cost-head] h3 {
  margin: 0;
  font: 700 20px/1.25 'Space Grotesk', system-ui;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
[data-cost-close] {
  flex: none;
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  border: 1px solid #E6EAF0; border-radius: 999px;
  background: #FFFFFF; color: #5A6478; cursor: pointer;
  transition: background-color 160ms ease, color 160ms ease;
}
[data-cost-close]:hover { background: #F1F4F8; color: #0F172A; }

/* The one scrolling region. The header stays put so the project's name is
   still on screen when someone is halfway down the second table. */
[data-cost-body] { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 4px 24px 24px; }

[data-cost-table] { width: 100%; border-collapse: collapse; margin-top: 20px; }
[data-cost-table] caption {
  text-align: left; padding-bottom: 6px;
  font: 500 10px/1.6 'JetBrains Mono', monospace; letter-spacing: .2em; color: #5A6478;
  text-transform: uppercase;
}
[data-cost-table] th {
  font: 500 10px/1.6 'JetBrains Mono', monospace; letter-spacing: .16em; color: #5A6478;
  text-align: left; padding: 7px 0; border-bottom: 1px solid #E6EAF0; font-weight: 500;
}
[data-cost-table] td { padding: 11px 0; border-bottom: 1px solid #F1F4F8; vertical-align: top; }
[data-cost-item] { font: 400 14px/1.4 'Space Grotesk', system-ui; padding-right: 18px; }
[data-cost-note] { display: block; margin-top: 3px; font: 400 10px/1.6 'JetBrains Mono', monospace; color: #8B94A7; }
/* tabular-nums is what keeps the decimal points in a column. */
[data-cost-amount] {
  text-align: right; white-space: nowrap; padding-left: 16px;
  font: 400 13px/1.4 'JetBrains Mono', monospace; font-variant-numeric: tabular-nums;
}
[data-cost-table] tfoot th {
  border-bottom: 0; border-top: 1px solid #DDE2E9; padding-top: 11px;
  font: 600 13px/1.4 'Space Grotesk', system-ui; letter-spacing: 0; text-transform: none; color: #0F172A;
}
[data-cost-table] tfoot td {
  border-bottom: 0; border-top: 1px solid #DDE2E9; padding-top: 11px;
  font: 700 15px/1.4 'JetBrains Mono', monospace; color: #0369A1;
}

[data-cost-saved] {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  margin: 24px 0 0; padding: 15px 17px;
  border-radius: 12px; background: #F0F9FF;
  font: 500 13px/1.4 'Space Grotesk', system-ui; color: #0F172A;
}
[data-cost-saved] b {
  font: 700 22px/1 'JetBrains Mono', monospace; font-variant-numeric: tabular-nums;
  letter-spacing: -.02em; color: #0369A1; white-space: nowrap;
}
[data-cost-footnote] { margin: 16px 0 0; font: 400 13px/1.65 'Space Grotesk', system-ui; color: #8B94A7; text-wrap: pretty; }

/* ---- the live embed ----
   The scrim lifts and the button grows very slightly on hover: enough to read
   as pressable over a screenshot, not enough to look like a video player. */
[data-embed] [data-embed-start] { transition: background-color 200ms ease, background 200ms ease; }
[data-embed] [data-embed-start] [data-embed-cta] { transition: transform 200ms cubic-bezier(.4,0,.2,1), background-color 200ms ease; }
/* The hint sits on its own slate ground rather than on a text-shadow. Every
   frame behind it is a map — pale in the middle, dark at the header — so white
   text with a shadow is legible on some states and invisible on others. */
[data-embed] [data-embed-hint] { -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }
[data-embed] [data-embed-start]:hover [data-embed-cta],
[data-embed] [data-embed-start]:focus-visible [data-embed-cta] { transform: scale(1.04); background: #0284C7; }
[data-embed] iframe { display: block; width: 100%; height: 100%; border: 0; }
@media (prefers-reduced-motion: reduce) {
  [data-embed] [data-embed-start] [data-embed-cta] { transition: none !important; }
  [data-embed] [data-embed-start]:hover [data-embed-cta] { transform: none; }
}

/* ---- contact ---- */
/* WHY SO MUCH !important IN A FILE THAT IS OTHERWISE ORDINARY CSS
   The export writes the control skin as an inline style attribute —
   border:1px solid #DDE2E9;background:#FFFFFF — and CONVENTIONS.md rule 3 says
   keep those verbatim. Inline declarations beat every author rule regardless of
   specificity, so a state that has to REPAINT something the inline style
   already painted (a border color, a background, a display) can only do it
   with !important. Rules that paint a property the inline style never mentions
   are left alone. That is the whole pattern: !important here marks "the export
   painted this inline", not "I could not work out the specificity". */

/* ---- fields ----
   Focus rings come from site.css (input/select/textarea already have
   :focus-visible there). The hover border is the export's style-hover, and the
   filled/valid board's #CFD5DF border is simply what a control with content in
   it looks like — :not(:placeholder-shown) is that state, for free. */
[data-c="contact-form"] input:hover,
[data-c="contact-form"] select:hover,
[data-c="contact-form"] textarea:hover { border-color: #CFD5DF !important; }
[data-c="contact-form"] input:not(:placeholder-shown),
[data-c="contact-form"] textarea:not(:placeholder-shown),
[data-c="contact-form"] select:required:valid { border-color: #CFD5DF !important; }

/* ---- inline error ----
   Empty container, no reserved space, no hidden attribute to keep in sync.
   Writing text into it is the whole reveal; emptying it is the whole undo. */
[data-c="contact-form"] .f-err:empty { display: none; }

/* The error skin. aria-invalid is what the handler sets; :user-invalid is the
   browser doing the same thing on its own, after the visitor has actually
   interacted — so a field turns red with JS blocked too, and never before it
   has been touched. [data-c="field-error"] is the export's own name for this
   state and is honoured if a handler prefers to flip the wrapper instead.

   Spelled out per element type on purpose: these have to outrank the filled
   rule above, which is 0,2,1 because of its :not(). A bare
   [data-c="contact-form"] :user-invalid is 0,2,0 and would lose to it — a
   filled-but-invalid field would then read as valid, which is the one thing
   this rule exists to prevent. */
[data-c="contact-form"] input:user-invalid,
[data-c="contact-form"] select:user-invalid,
[data-c="contact-form"] textarea:user-invalid,
[data-c="contact-form"] input[aria-invalid="true"],
[data-c="contact-form"] select[aria-invalid="true"],
[data-c="contact-form"] textarea[aria-invalid="true"],
[data-c="field-error"] input,
[data-c="field-error"] select,
[data-c="field-error"] textarea { border-color: #B42318 !important; background: #FEF6F5 !important; }

/* ---- error summary ----
   The box ships with the hidden attribute, so this display rule is what
   reveals it; hidden is a UA-stylesheet display:none and an author rule beats
   it without needing !important. One of the two words is always hidden. */
[data-c="contact-form"][data-state="error"] [data-c="form-error-summary"] { display: block; }
[data-c="form-error-summary"] [data-err-word="one"] { display: none; }
[data-c="form-error-summary"][data-count="1"] [data-err-word="one"] { display: inline; }
[data-c="form-error-summary"][data-count="1"] [data-err-word="many"] { display: none; }

/* ---- pending ----
   Label swap plus the disabled skin from the 05 board. No spinner and no new
   color: #CBD5E1 on #F7F8FA is the disabled pair the design system already
   publishes. The button's accent background is inline, hence !important. */
[data-c="contact-form"][data-state="pending"] [data-btn-idle] { display: none; }
[data-c="contact-form"][data-state="pending"] [data-btn-pending] { display: inline; }
[data-c="contact-form"] [data-form-submit]:disabled,
[data-c="contact-form"] [data-form-submit]:disabled:hover {
  background: #CBD5E1 !important; color: #F7F8FA !important; cursor: not-allowed !important;
}

/* ---- success replaces the form ----
   form-grid is inline display:flex, so hiding it needs !important; the success
   panel has no inline display and only needs to outrank its hidden attribute.

   TWO WAYS IN, AND :target IS THE ONE THAT ACTUALLY RUNS TODAY.

   api/contact.js answers an ordinary form POST with a 303 to
   /contact?sent=1#sent. Nothing read that. A query string cannot change a
   static page, and there is no submit handler in main.js — so EVERY
   submission, not just the JavaScript-disabled ones, landed back on an
   untouched contact form with no confirmation that anything had been sent.

   :target fixes it with no script at all: the fragment in that redirect makes
   #sent the target, the panel shows, and the general-sibling rule hides the
   field grid that now follows it in the DOM. That reordering is the reason the
   panel is emitted before the grid rather than after it.

   The [data-state="success"] pair below is the hook for the fetch handler
   described in this file's header, which was never built. It costs two rules
   and means that handler can be added later without touching this block. */
[data-c="form-success"]:target { display: block; }
[data-c="form-success"]:target ~ [data-c="form-grid"] { display: none !important; }
[data-c="contact-form"][data-state="success"] [data-c="form-grid"] { display: none !important; }
[data-c="contact-form"][data-state="success"] [data-c="form-success"] { display: block; }
[data-c="form-success"]:focus-visible { outline: 2px solid #0EA5E9; outline-offset: 6px; }

/* ---- sidebar ---- */
[data-side="direct"] [data-c="mono-number"]:hover { color: #0EA5E9 !important; }

/* ---- faq ---- */
/* ---- rows ----
   One sign is hidden at a time, so the row still reads +/– with JS blocked. */
[data-page="faq"] [data-c="faq-row"] > summary:hover { border-bottom-color: #0F172A; background: #FBFCFD; }
[data-page="faq"] [data-c="faq-row"] [data-sign-open] { display: none; }
[data-page="faq"] [data-c="faq-row"][open] [data-sign-open] { display: inline; }
[data-page="faq"] [data-c="faq-row"][open] [data-sign-closed] { display: none; }

/* ---- the group list ----
   The rail is desktop-only; the "Jump to a section" disclosure is phone-only
   and is switched on in exports.mobile. */
[data-toc="jump"] { display: none; }
[data-toc="jump"] [data-c="faq-row"] > summary:hover { background: transparent; }

[data-toc-link] { transition: color .18s ease, border-left-color .18s ease; }
[data-toc="rail"] [data-toc-link]:hover { color: #0F172A; border-left-color: #0F172A; }
[data-toc="jump"] [data-toc-link]:hover { color: #0F172A; }

/* The "you are here" skin. One rule, driven by aria-current, so a scroll
   observer that moves the attribute and the no-JS :target fallback below light
   up exactly the same thing. */
[data-toc-link][aria-current="true"] { color: #0F172A; }
[data-toc="rail"] [data-toc-link][aria-current="true"] { border-left-color: #0EA5E9; }

/* No-JS fallback for "mark the group in view". A jump puts the group in
   :target, so the mark follows the reader as far as their clicks go. The reset
   rule is listed first and is deliberately less specific than the five that
   follow (an id inside :has() outranks it), so the rendered aria-current mark
   steps aside as soon as the URL names a group. Scrolling past a group without
   clicking still needs the observer noted at the foot of this file. */
[data-page="faq"]:not(.toc-spy):has([data-faq-group]:target) [data-toc-link][aria-current="true"] { color: #5A6478; }
[data-page="faq"]:not(.toc-spy):has([data-faq-group]:target) [data-toc="rail"] [data-toc-link][aria-current="true"] { border-left-color: #DDE2E9; }
[data-page="faq"]:not(.toc-spy):has(#owning-it:target) [data-toc-link="owning-it"],
[data-page="faq"]:not(.toc-spy):has(#prices-and-process:target) [data-toc-link="prices-and-process"],
[data-page="faq"]:not(.toc-spy):has(#moving-off-a-platform:target) [data-toc-link="moving-off-a-platform"],
[data-page="faq"]:not(.toc-spy):has(#apps:target) [data-toc-link="apps"],
[data-page="faq"]:not(.toc-spy):has(#after-its-live:target) [data-toc-link="after-its-live"] { color: #0F172A; }
[data-page="faq"]:not(.toc-spy):has(#owning-it:target) [data-toc="rail"] [data-toc-link="owning-it"],
[data-page="faq"]:not(.toc-spy):has(#prices-and-process:target) [data-toc="rail"] [data-toc-link="prices-and-process"],
[data-page="faq"]:not(.toc-spy):has(#moving-off-a-platform:target) [data-toc="rail"] [data-toc-link="moving-off-a-platform"],
[data-page="faq"]:not(.toc-spy):has(#apps:target) [data-toc="rail"] [data-toc-link="apps"],
[data-page="faq"]:not(.toc-spy):has(#after-its-live:target) [data-toc="rail"] [data-toc-link="after-its-live"] { border-left-color: #0EA5E9; }

/* A linked row is scrolled to by the browser; give it somewhere to land that
   is not flush under the sticky header. */
[data-page="faq"] [data-c="faq-row"]:target,
[data-page="faq"] [data-faq-group]:target { scroll-margin-top: 40px; }

/* ---- privacy ---- */
/* ---- the drifting grid, off (SPECS: "GRID DRIFT — OFF ON THIS PAGE") ---- */
body[data-page="privacy"] #warp { display: none !important; }
body[data-page="privacy"] [data-c="band-light"]::before,
body[data-page="privacy"] [data-c="site-footer"]::before { display: none !important; }

/* ---- contents list ----
   The rail is desktop-only; the "Jump to a section" row is phone-only and is
   switched on in exports.mobile. */
[data-page="privacy"] [data-legal-toc="jump"] { display: none; }

[data-page="privacy"] [data-legal-toc-link] { transition: color .18s ease, border-left-color .18s ease; }
[data-page="privacy"] [data-legal-toc="rail"] [data-legal-toc-link]:hover { color: #0F172A; border-left-color: #0F172A; }
[data-page="privacy"] [data-legal-toc="jump"] [data-legal-toc-link]:hover { color: #0F172A; }

/* The "you are here" skin. The export drew the first row and the rest as two
   hardcoded variants; here one rule hangs off aria-current, so a scroll observer
   that moves the attribute lights up exactly what the renderer marked. */
[data-page="privacy"] [data-legal-toc-link][aria-current="true"] { color: #0F172A; }
[data-page="privacy"] [data-legal-toc-link][aria-current="true"] [data-c="mono-number"] { color: #0EA5E9; }
[data-page="privacy"] [data-legal-toc="rail"] [data-legal-toc-link][aria-current="true"] { border-left-color: #0EA5E9; }

/* ---- the phone's disclosure row ----
   Both signs ship and [open] picks which is visible, so the row still reads +/–
   with JS blocked. Same trick as the FAQ rows; scoped here so the two pages do
   not depend on faq.js's copy of it.

   The background:transparent is not redundant: home.js exports an UNSCOPED
   [data-c="faq-row"] > summary:hover rule that tints the row #FBFCFD, and these
   two boards draw the hover as a rule-colour change only. */
[data-page="privacy"] [data-legal-toc="jump"] > [data-c="faq-row"] > summary:hover { border-bottom-color: #0F172A; background: transparent; }
[data-page="privacy"] [data-c="faq-row"] [data-sign-open] { display: none; }
[data-page="privacy"] [data-c="faq-row"][open] [data-sign-open] { display: inline; }
[data-page="privacy"] [data-c="faq-row"][open] [data-sign-closed] { display: none; }

/* A linked clause is scrolled to by the browser; give it somewhere to land that
   is not flush against the top of the viewport. */
[data-page="privacy"] [data-legal-section]:target { scroll-margin-top: 40px; }

/* ---- terms ---- */
/* ---- the drifting grid, off (SPECS: "GRID DRIFT — OFF ON THIS PAGE") ---- */
body[data-page="terms"] #warp { display: none !important; }
body[data-page="terms"] [data-c="band-light"]::before,
body[data-page="terms"] [data-c="site-footer"]::before { display: none !important; }

/* ---- contents list ----
   The rail is desktop-only; the "Jump to a section" row is phone-only and is
   switched on in exports.mobile. */
[data-page="terms"] [data-legal-toc="jump"] { display: none; }

[data-page="terms"] [data-legal-toc-link] { transition: color .18s ease, border-left-color .18s ease; }
[data-page="terms"] [data-legal-toc="rail"] [data-legal-toc-link]:hover { color: #0F172A; border-left-color: #0F172A; }
[data-page="terms"] [data-legal-toc="jump"] [data-legal-toc-link]:hover { color: #0F172A; }

/* The "you are here" skin. The export drew the first row and the rest as two
   hardcoded variants; here one rule hangs off aria-current, so a scroll observer
   that moves the attribute lights up exactly what the renderer marked. */
[data-page="terms"] [data-legal-toc-link][aria-current="true"] { color: #0F172A; }
[data-page="terms"] [data-legal-toc-link][aria-current="true"] [data-c="mono-number"] { color: #0EA5E9; }
[data-page="terms"] [data-legal-toc="rail"] [data-legal-toc-link][aria-current="true"] { border-left-color: #0EA5E9; }

/* ---- the phone's disclosure row ----
   Both signs ship and [open] picks which is visible, so the row still reads +/–
   with JS blocked. Same trick as the FAQ rows; scoped here so the two pages do
   not depend on faq.js's copy of it.

   The background:transparent is not redundant: home.js exports an UNSCOPED
   [data-c="faq-row"] > summary:hover rule that tints the row #FBFCFD, and these
   two boards draw the hover as a rule-colour change only. */
[data-page="terms"] [data-legal-toc="jump"] > [data-c="faq-row"] > summary:hover { border-bottom-color: #0F172A; background: transparent; }
[data-page="terms"] [data-c="faq-row"] [data-sign-open] { display: none; }
[data-page="terms"] [data-c="faq-row"][open] [data-sign-open] { display: inline; }
[data-page="terms"] [data-c="faq-row"][open] [data-sign-closed] { display: none; }

/* A linked clause is scrolled to by the browser; give it somewhere to land that
   is not flush against the top of the viewport. */
[data-page="terms"] [data-legal-section]:target { scroll-margin-top: 40px; }

/* ---- festival ---- */
/* one language at a time; English wins when nothing is set (no JavaScript) */
html:not([data-lang="es"]) [data-l="es"] { display: none !important; }
html[data-lang="es"] [data-l="en"] { display: none !important; }

[data-fest-lang] { display: inline-flex; padding: 4px; border: 1px solid #334155; border-radius: 999px; }
[data-fest-lang] button { min-width: 52px; height: 44px; border: 0; border-radius: 999px; background: transparent; color: #A7B0C0; font: 500 13px/1 'JetBrains Mono', monospace; letter-spacing: 0.12em; cursor: pointer; }
[data-fest-lang] button[aria-pressed="true"] { background: #F4F7FB; color: #0F172A; }
[data-fest-lang] button:focus-visible { outline: 2px solid #38BDF8; outline-offset: 2px; }

[data-c="btn-outline-dark"]:hover { border-color: #38BDF8 !important; }
[data-fest-card] [data-c="link-arrow"]:hover { color: #0EA5E9 !important; }

/* the success panel: shown by the script, or by #sent as :target after a no-JS post */
#sent { display: none; }
#sent[data-show], #sent:target { display: block; }
#sent:target + form, [data-fest-form][hidden] { display: none !important; }
#sent:focus { outline: none; }

[data-fest-form] input:focus-visible, [data-fest-form] textarea:focus-visible { outline: 2px solid #0EA5E9; outline-offset: 1px; border-color: #0EA5E9 !important; }
[data-fest-form] label:has(input[type="radio"]:checked) { border-color: #0EA5E9 !important; background: #E9F6FE !important; }
[data-fest-form] [data-bad] input, [data-fest-form] [data-bad] textarea { border-color: #B42318 !important; background: #FEF6F5 !important; }
[data-fest-form] .f-err:empty, [data-fest-status]:empty { display: none; }
[data-fest-form] button[disabled] { opacity: 0.7; cursor: progress; }

/* ---- pay ---- */
[data-pay][data-state="nojs"] [data-panel="nojs"],
[data-pay][data-state="loading"] [data-panel="loading"],
[data-pay][data-state="missing"] [data-panel="missing"],
[data-pay][data-state="notfound"] [data-panel="notfound"],
[data-pay][data-state="draft"] [data-panel="draft"],
[data-pay][data-state="void"] [data-panel="void"],
[data-pay][data-state="processing"] [data-panel="processing"],
[data-pay][data-state="verify"] [data-panel="verify"],
[data-pay][data-state="paid"] [data-panel="paid"],
[data-pay][data-state="error"] [data-panel="error"] { display: block; }

[data-pay][data-state="open"] [data-pay-invoice],
[data-pay][data-state="processing"] [data-pay-invoice],
[data-pay][data-state="verify"] [data-pay-invoice],
[data-pay][data-state="paid"] [data-pay-invoice] { display: block; }
[data-pay][data-state="open"] [data-pay-form] { display: block; }

/* In the paid state the thank-you card comes first. order works because the
   column is a flex container. */
[data-pay][data-state="paid"] [data-panel="paid"] { order: -1; }

/* loading: a quiet pulse on the heading, off under reduced motion */
@media (prefers-reduced-motion: no-preference) {
  [data-panel="loading"] h2 { animation: pay-pulse 1.4s ease-in-out infinite; }
}
@keyframes pay-pulse { 50% { opacity: .45; } }

/* the button: hover, pending and disabled, same pairs as the contact form */
[data-pay-submit]:hover:not(:disabled), [data-pay-retry]:hover { background: #0284C7 !important; }
[data-pay-form][data-state="pending"] [data-btn-idle] { display: none; }
[data-pay-form][data-state="pending"] [data-btn-pending] { display: inline; }
[data-pay-submit]:disabled, [data-pay-submit]:disabled:hover {
  background: #CBD5E1 !important; color: #F7F8FA !important; cursor: not-allowed !important;
}
/* A row with an inline display (the ALREADY PAID row is display:flex) ignores
   the hidden attribute, because inline styles outrank the UA's display:none.
   This puts hidden back in charge for the optional rows only. */
[data-pay] [data-row][hidden], [data-pay] [data-f="more"][hidden] { display: none !important; }
[data-pay-error]:not(:empty) { display: block; }
[data-pay-error]:empty { display: none; }

[data-pay] [data-c="pay-panel"]:focus-visible { outline: 2px solid #0EA5E9; outline-offset: 6px; }
[data-pay-side="questions"] [data-c="mono-number"]:hover { color: #0EA5E9 !important; }
