/* Nixie shared component layer. Load after the surface's existing stylesheet.
   Keep tokens aligned with DESIGN.md; prefer intrinsic width for content. */
:root {
  --nx-surface: #ffffff;
  --nx-ground: #fdfaf7;
  --nx-text: #4a3f42;
  --nx-muted: #6f6368;
  --nx-border: #ebe0dd;
  --nx-accent: #94505d;
  --nx-sage-surface: #e7efe9;
  --nx-sage-text: #4a6553;
  --nx-radius: 8px;
  --nx-space-1: 4px;
  --nx-space-2: 8px;
  --nx-space-3: 12px;
  --nx-space-4: 16px;
  --nx-space-6: 24px;
  --nx-content-measure: 65ch;
}
/* Content hugs its contents, but can shrink and wrap in a narrow parent. */
.nx-fit, .nx-content-card, .nx-stat {
  box-sizing: border-box;
  inline-size: fit-content;
  max-inline-size: 100%;
  min-inline-size: 0;
  justify-self: start;
  align-self: start;
  overflow-wrap: anywhere;
}
.nx-fit-row { display: flex; flex-wrap: wrap; align-items: start; gap: var(--nx-space-4); }
.nx-fit-row > * { flex: 0 1 auto; min-inline-size: 0; max-inline-size: 100%; }
.nx-content-card { background: var(--nx-surface); color: var(--nx-text); border: 1px solid var(--nx-border); border-radius: var(--nx-radius); padding: var(--nx-space-4); }
.nx-content-card > :first-child { margin-block-start: 0; }
.nx-content-card > :last-child { margin-block-end: 0; }
.nx-content-card p { max-inline-size: var(--nx-content-measure); }
/* An inline statistic, never an empty full-width dashboard tile. */
.nx-stat { display: inline-flex; align-items: baseline; gap: var(--nx-space-2); color: var(--nx-text); font-variant-numeric: tabular-nums; white-space: normal; }
.nx-stat__value { font-size: 1.25em; font-weight: 750; line-height: 1.25; }
.nx-stat__label { color: var(--nx-muted); font-size: .875em; line-height: 1.4; }
/* The same sandbox notice is used in the site footer, account entry and studio. */
.nx-sandbox-banner { box-sizing: border-box; display: flex; flex-wrap: wrap; align-items: center; gap: 12px 24px; inline-size: fit-content; max-inline-size: 100%; padding: 18px 22px; background: var(--nx-sage-surface); color: var(--nx-sage-text); border-radius: var(--nx-radius); font-family: inherit; font-size: 14px; line-height: 1.5; }
.nx-sandbox-banner__copy { min-inline-size: 0; max-inline-size: var(--nx-content-measure); }
.nx-sandbox-banner__copy strong { display: block; font-weight: 750; }
.nx-sandbox-banner__copy > span { display: block; font-size: .9em; margin-block-start: 3px; }
.nx-sandbox-banner__action { display: inline-flex; flex: 0 0 auto; align-items: center; justify-content: center; max-inline-size: 100%; min-block-size: 42px; padding: 8px 14px; color: var(--nx-sage-text); border: 1px solid currentColor; border-radius: 5px; text-decoration: none; font-size: 13px; line-height: 1.3; font-weight: 700; background: transparent; }
.nx-sandbox-banner__action:hover { background: #dce8df; }
.nx-sandbox-banner__action:focus-visible { outline: 2px solid var(--nx-accent); outline-offset: 3px; }
.nx-sandbox-banner--compact { padding: 10px 26px; font-size: 12px; gap: 8px 18px; }
/* Explicit exception: a status ribbon spans the work area it describes. */
.nx-sandbox-banner--wide { inline-size: 100%; border-radius: 0; justify-content: space-between; }
@media (max-width:600px) { .nx-sandbox-banner { padding: 16px; gap: 12px; } .nx-sandbox-banner--compact { padding: 10px 16px; font-size: 11px; gap: 4px 12px; } }

/* ==========================================================================
   Loading. Nixie never uses a spinner; see DESIGN.md. There are exactly two
   answers here: the squiggle, for a whole page or view that is on its way, and
   a content placeholder, for a region whose shape is already known.
   ========================================================================== */

/* The squiggle. A hand-drawn water line across the top of the viewport, 10px
   down from the edge, spanning its full width. It is drawn, not filled: the
   dash travels along the path, so the wave writes itself across the screen and
   recedes, and at no point does it claim to know how far along a load is. */
.nx-loading-line {
  position: fixed;
  inset-block-start: 10px;
  inset-inline: 0;
  z-index: 2147483000; /* above studio sheets, dialogs and toasts */
  block-size: 16px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .28s ease;
}
.nx-loading-line.is-loading { opacity: 1; }
/* assets/loading.js draws the wave to the viewport's own width in whole
   periods, so this fills its container at very close to 1:1 and a period keeps
   its drawn size on a phone as on a desktop. */
.nx-loading-line__wave { display: block; inline-size: 100%; block-size: 100%; }
.nx-loading-line__wave path {
  fill: none;
  stroke: var(--nx-loading-ink, #94505d);
  stroke-width: 2; /* path units are CSS pixels: a 2px drawn line */
  stroke-linecap: round;
  /* pathLength is normalised to 1000 in the markup, so these are portions of
     the whole line regardless of how many periods the viewport holds.
     The drawn segment is most of the line, not a short dart: the wave should
     read as crossing the content block it sits above, which is what the
     reader is actually waiting for. */
  stroke-dasharray: 880 1000;
  stroke-dashoffset: 1880;
}
/* Juice. The stroke is the brand mark, so it is allowed a little soul: it
   enters briskly, eases through the middle where the reader can read it, and
   releases on the way out, rather than crossing at one constant rate. The
   trailing width taper and the settle below give the line follow-through —
   the non-essential detail that makes a mark feel drawn by a hand rather
   than stepped by a timer. Slower than the old 1.9s, because the line now
   travels the whole width and a fast crossing reads as a nervous tic. */
.nx-loading-line.is-loading .nx-loading-line__wave path {
  animation: nx-loading-draw 2.9s cubic-bezier(.62,.03,.31,1) infinite,
             nx-loading-breathe 2.9s ease-in-out infinite;
}
@keyframes nx-loading-draw {
  from { stroke-dashoffset: 1880; }
  to   { stroke-dashoffset: -880; }
}
/* Secondary motion: the stroke thickens as the wave arrives and thins as it
   leaves, the way pressure changes across a drawn line. */
@keyframes nx-loading-breathe {
  0%   { stroke-width: 1.4; opacity: .55; }
  22%  { stroke-width: 2.4; opacity: 1; }
  70%  { stroke-width: 2.2; opacity: 1; }
  100% { stroke-width: 1.4; opacity: .5; }
}
/* Reduced motion: the same squiggle, drawn once and left there. The line still
   says "this page is coming"; it simply does not travel to say it. */
@media (prefers-reduced-motion: reduce) {
  .nx-loading-line__wave path { stroke-dasharray: none; stroke-dashoffset: 0; opacity: .55; stroke-width: 2; }
  .nx-loading-line.is-loading .nx-loading-line__wave path { animation: none; }
  .nx-loading-line { transition: none; }
}

/* ==========================================================================
   The drawn frame. The same hand as the squiggle, but shaped like the thing
   that is coming rather than laid across the top of it. A view that knows it
   is about to be a card, a table or a panel draws that outline in one
   continuous rose stroke, then lets a few short interior lines arrive after
   it — the rhythm of the rows the region is about to hold.

   This is the squiggle's second form, for loading an INTERFACE. The
   horizontal line says "this page is on its way". The drawn frame says "this
   region is about to be *this shape*", which is the more useful statement
   once the reader is already inside a surface. It is still drawn, never
   filled, and it still reports no fraction: the outline completes and the
   interior lines settle, and that is the whole vocabulary.
   ========================================================================== */
.nx-draw {
  display: block;
  inline-size: 100%;
  max-inline-size: 100%;
  block-size: auto;
  overflow: visible;
}
.nx-draw__frame,
.nx-draw__line {
  fill: none;
  stroke: var(--nx-loading-ink, #94505d);
  stroke-linecap: round;
  stroke-linejoin: round;
  /* The drawing stretches to the region it describes, but the stroke must not
     stretch with it, or the outline thickens on one axis and the hand is lost. */
  vector-effect: non-scaling-stroke;
}
/* The outline. One stroke, drawn from the top-left and closing on itself. */
.nx-draw__frame {
  stroke-width: 2;
  /* pathLength="1000" is set on the element in markup, so the dash below is a
     portion of the whole outline whatever the region's real dimensions are. */
  stroke-dasharray: 1000;
  stroke-dashoffset: 1000;
  animation: nx-draw-frame 1.5s cubic-bezier(.65,0,.35,1) forwards;
}
@keyframes nx-draw-frame {
  from { stroke-dashoffset: 1000; }
  to   { stroke-dashoffset: 0; }
}
/* The interior lines. Impact, not content: they arrive after the outline has
   closed, each a beat behind the last, and they are shorter and lighter than
   the frame so they read as the suggestion of rows rather than fake records.
   The stagger is the juice — the region assembles instead of appearing. */
.nx-draw__line {
  stroke-width: 1.6;
  opacity: 0;
  stroke-dasharray: 1000;
  stroke-dashoffset: 1000;
  animation: nx-draw-line .85s cubic-bezier(.4,0,.2,1) forwards;
  animation-delay: calc(1.15s + var(--nx-draw-index, 0) * .16s);
}
@keyframes nx-draw-line {
  from { stroke-dashoffset: 1000; opacity: 0; }
  60%  { opacity: .75; }
  to   { stroke-dashoffset: 0; opacity: .62; }
}
/* While the region is still waiting, the whole drawing breathes once it has
   settled, so a long wait does not end in a static picture that looks broken. */
.nx-draw.is-waiting { animation: nx-draw-settle 2.6s ease-in-out 2.4s infinite; }
@keyframes nx-draw-settle {
  0%, 100% { opacity: 1; }
  50%      { opacity: .58; }
}
/* Reduced motion: the same drawing, complete and still. The shape is the
   message, and the shape survives without any of the drawing. */
@media (prefers-reduced-motion: reduce) {
  .nx-draw__frame,
  .nx-draw__line { animation: none; stroke-dashoffset: 0; opacity: 1; }
  .nx-draw__line { opacity: .6; }
  .nx-draw.is-waiting { animation: none; }
}

/* ==========================================================================
   The traced mark. The squiggle's third form: the same wave, closed around
   the Nixie mark instead of laid across the top of a page.

   It exists for the one moment where there is nothing else to describe. On
   the sandbox entry the visitor has asked for a studio and the product is
   fetching it; there is no page on its way that the reader is already
   looking at, and no region whose shape is known, so the horizontal line has
   nothing to sit above and the drawn frame has nothing to outline. What is on
   screen is the mark, and the line runs around it.

   It obeys the same rule as the other two: drawn, never filled, and reporting
   no fraction. The dash travels around the closed path, so a stroke is always
   arriving somewhere on the ring and leaving somewhere else, and the ring is
   never completed into a static border.
   ========================================================================== */
.nx-trace {
  position: relative;
  display: block;
  inline-size: 150px;
  block-size: 150px;
  max-inline-size: 100%;
}
/* The mark is centred and sized as its own share of the viewBox, set inline
   by assets/loading.js, so the gap between the mark and the wave holds
   whatever size the container is given. */
.nx-trace__mark {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  translate: -50% -50%;
}
/* The wave overflows its box by design: the crests sit outside the padded
   viewBox at the extremes of the breathe animation. */
.nx-trace__wave {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  overflow: visible;
}
.nx-trace__line {
  fill: none;
  stroke: var(--nx-loading-ink, #94505d);
  stroke-linecap: round;
  stroke-width: 1.5;
  /* pathLength is normalised to 1000 in the markup. The drawn segment is a
     little over half the ring: enough that the line reads as running around
     the mark, not so much that it closes into a frame around it. */
  stroke-dasharray: 560 1000;
  stroke-dashoffset: 1560;
  /* Slower than the horizontal line at 2.9s. This one circles rather than
     crosses, so the reader's eye is following it round rather than watching
     it pass, and the same pace would read as hurried. */
  animation: nx-trace-draw 3.6s cubic-bezier(.62,.03,.31,1) infinite,
             nx-trace-breathe 3.6s ease-in-out infinite;
}
@keyframes nx-trace-draw {
  from { stroke-dashoffset: 1560; }
  to   { stroke-dashoffset: -560; }
}
/* The same pressure change as the horizontal line, at the lighter weight this
   form is drawn in. */
@keyframes nx-trace-breathe {
  0%   { stroke-width: 1.1; opacity: .5; }
  22%  { stroke-width: 1.9; opacity: 1; }
  70%  { stroke-width: 1.8; opacity: 1; }
  100% { stroke-width: 1.1; opacity: .45; }
}
/* Reduced motion: the whole ring, drawn once and left around the mark. The
   line still says the studio is coming; it simply does not travel to say it. */
@media (prefers-reduced-motion: reduce) {
  .nx-trace__line { animation: none; stroke-dasharray: none; stroke-dashoffset: 0; stroke-width: 1.5; opacity: .6; }
}

/* The verification widget while it is verifying on its own. Turnstile draws
   its own spinner beside "Verifying…", inside a cross-origin frame where it
   cannot be restyled, and a rotating shape is banned on every Nixie surface.
   assets/verification.js therefore keeps the widget unpainted until it is
   clear that it actually wants the visitor, and reveals it then. The box keeps
   its geometry throughout, because Turnstile needs somewhere to lay out and
   because revealing it must not shift what is around it. */
.verification.is-quiet { visibility: hidden; }

/* Content placeholders. A region that knows its own shape says so while it
   waits: table rows keep their rhythm, a card keeps its measure. The shimmer
   is a slow wash across the placeholder, not a rotating object. */
.nx-placeholder {
  display: block;
  block-size: 1em;
  border-radius: var(--nx-radius-sm, 4px);
  background: var(--nx-placeholder, #ece2de);
  background-image: linear-gradient(90deg, transparent 0%, var(--nx-placeholder-sheen, rgb(255 255 255 / 60%)) 50%, transparent 100%);
  background-size: 220% 100%;
  background-repeat: no-repeat;
  animation: nx-placeholder-wash 1.6s ease-in-out infinite;
}
@keyframes nx-placeholder-wash {
  from { background-position: 180% 0; }
  to   { background-position: -80% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .nx-placeholder { animation: none; background-image: none; }
}
/* A stack of lines standing in for a paragraph or a record. The last line is
   short, the way a real last line is. */
.nx-placeholder-lines { display: flex; flex-direction: column; gap: var(--nx-space-3); margin: 0; padding: 0; list-style: none; }
.nx-placeholder-lines > * { inline-size: min(100%, 600px); }
.nx-placeholder-lines > :nth-child(2) { inline-size: min(80%, 470px); }
.nx-placeholder-lines > :nth-child(3) { inline-size: min(65%, 360px); }
.nx-placeholder-lines > :nth-child(4) { inline-size: min(72%, 410px); }
/* A table waiting for rows keeps the row rhythm it is about to have, so the
   records do not jump into place on arrival. */
.nx-placeholder-rows { display: flex; flex-direction: column; gap: var(--nx-space-4); margin: 0; padding: var(--nx-space-2) 0; list-style: none; }
.nx-placeholder-rows > * { display: flex; gap: var(--nx-space-4); align-items: center; }
.nx-placeholder-rows .nx-placeholder { flex: 1 1 0; min-inline-size: 0; }
.nx-placeholder-rows .nx-placeholder--narrow { flex: 0 1 90px; }
/* The caption under a placeholder names what is coming, in the surface's own
   words. It is a statement, never the word "Loading" beside a moving shape. */
.nx-placeholder-note { color: var(--nx-muted); font-size: .8125em; margin: var(--nx-space-3) 0 0; }
