/* =========================================================================
   Context Layer — diagram styles
   Editorial / technical, light mode, Lightdash violet accent.
   ========================================================================= */

/* Britti Sans (trial, Pangram Pangram) — self-hosted from /fonts/ */
@font-face {
  font-family: "Britti Sans";
  src: url("/fonts/BrittiSans-Semibold.otf") format("opentype");
  font-weight: 500 700;
  font-style: normal;
  font-display: swap;
}

:root {
  /* paper & ink */
  --paper:        #F8FAFB;
  --paper-edge:   oklch(95% 0.006 80);
  --card:         oklch(100% 0 0);
  --rule:         oklch(91% 0.012 280);
  --rule-soft:    oklch(94% 0.008 280);
  --card-stroke:  #5E4CFF;  /* brand violet outline for outer cards */
  --ink:          oklch(22% 0.022 285);
  --ink-soft:     oklch(38% 0.025 285);
  --muted:        oklch(54% 0.020 285);
  --label:        oklch(58% 0.025 285);

  /* brand — locked to the brand hex #5E4CFF used in the new icons */
  --violet:        #5E4CFF;
  --violet-strong: #4B3BE6;
  --violet-tint:   oklch(96% 0.025 290);

  /* type */
  --display: "Britti Sans", "Söhne", "Inter", system-ui, -apple-system, sans-serif;
  --sans:    "Inter", system-ui, -apple-system, sans-serif;
  /* Pixel face — used ONLY for literal code/identifiers inside the
     feedback-loop card (the YAML pull-request diff and governed metric
     names). Editorial copy and labels stay on --sans. */
  --pixel:   "Silkscreen", ui-monospace, "SF Mono", Menlo, monospace;

  /* scale */
  --s-2:  4px;
  --s-1:  8px;
  --s0:   12px;
  --s1:   16px;
  --s2:   20px;
  --s3:   24px;
  --s4:   32px;
  --s5:   40px;
  --s6:   56px;
  --s7:   72px;

  --shadow-card:
    0 1px 0 oklch(0% 0 0 / 0.02),
    0 1px 2px oklch(0% 0 0 / 0.025);

  --radius-card: 14px;
  --radius-sub:  10px;
  --radius-tile: 7px;

  /* Square-pixel dashed border (rounded rect). Uniform px-sized dashes —
     drawn as an SVG background so the dash size doesn't stretch with the
     element. Used for the warehouse chips, platform subcards, and the
     feedback-loop "house". rx matches --radius-sub (10px). */
  --pixel-dash: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' fill='none' preserveAspectRatio='none'%3E%3Crect width='100%25' height='100%25' rx='10' ry='10' stroke='%23bdbbcb' stroke-width='4' stroke-dasharray='2 3'/%3E%3C/svg%3E");
}

*,
*::before,
*::after { box-sizing: border-box; }

html, body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img { display: block; max-width: 100%; height: auto; }

/* =========================================================================
   Layout shell
   ========================================================================= */

.diagram {
  position: relative;
  max-width: 1180px;  /* hug the four columns (~1114px content + padding) */
  margin: 0 auto;
  padding: clamp(32px, 5vw, 64px) clamp(16px, 3vw, 32px) clamp(40px, 6vw, 80px);
  isolation: isolate;
  /* Centre the diagram vertically in the viewport. Done here (not on body)
     so `.diagram` stays a normal block child whose width still resolves to
     min(viewport, max-width) — the JS board-scaler measures clientWidth and
     would mis-read it if `.diagram` became a shrink-to-fit flex item.
     min-height (not fixed height) lets it grow and anchor to the top if the
     content ever exceeds the viewport, instead of clipping. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* The desktop board scales as a single unit (JS-set transform) when the
     viewport is narrower than its design width, so text never reflows or
     truncates before the mobile breakpoint. Clip the unscaled layout-box
     overflow so it can't produce a stray horizontal scrollbar. */
  overflow-x: clip;
}

/* =========================================================================
   Header
   ========================================================================= */

/* Top-left page title. */
.intro {
  margin-bottom: clamp(22px, 3vw, 40px);
}
.intro__title {
  font-family: var(--display);
  font-weight: 600;
  font-size: clamp(26px, 3.7vw, 39px);
  line-height: 1.08;
  letter-spacing: -0.022em;
  color: var(--ink);
  margin: 0;
}
/* Second sentence sits inline on the same line, in brand violet. */
.intro__accent { color: var(--violet); }

.diagram__header {
  text-align: center;
  margin-bottom: clamp(36px, 5vw, 56px);
}
.diagram__title {
  font-family: var(--display);
  font-weight: 600;
  font-size: clamp(24px, 3.6vw, 40px);
  line-height: 1.12;
  letter-spacing: -0.022em;
  margin: 0;
}
.diagram__title span { display: inline; }
.diagram__title .accent { color: var(--violet); }
@media (max-width: 560px) {
  .diagram__title span { display: block; }
}

/* =========================================================================
   Board grid
   ========================================================================= */

.board {
  position: relative;
  display: grid;
  /* Wide platform column in the middle (so "Business logic · internal docs"
     sits on one line and the feedback lanes breathe). The consumption column
     is reduced a touch, and the source column is set to one consumer-card
     width (≈ half the consumption column) so the warehouse card matches a
     single consumer card. Both consumer cards stay equal via .consumers. */
  /* Source matches the Platform's hugged width; both Platform and Consumption
     hug their content so the board saves space and the columns read even. */
  grid-template-columns: max-content max-content max-content max-content;
  /* Equal horizontal + vertical gaps: warehouse↔platform matches the
     Context Layer↔Feedback loop gap. */
  column-gap: var(--s2);
  row-gap: var(--s2);
  align-items: start;       /* every column / card hugs its own content */
}

/* Desktop placement: source and consumers span both rows (full height);
   the platform column splits into the Context Layer (row 1) and the
   Feedback loop (row 2) stacked beneath it. The Feedback loop is LAST in
   the DOM so that on mobile it naturally falls below the consumption layer
   — here we pin it back up into column 2. */
@media (min-width: 921px) {
  /* Four columns across in one row, every card stretched to equal height. */
  .column--source    { grid-column: 1; grid-row: 1; align-self: stretch; }
  .column--platform  { grid-column: 2; grid-row: 1; align-self: stretch; }
  .column--consumers { grid-column: 3; grid-row: 1; align-self: stretch; }
  .column--feedback  { grid-column: 4; grid-row: 1; align-self: stretch; }
  /* Subcards hug their content (no vertical stretch), so the frame stays as
     compact as the Context Layer card. */
  .column--consumers .consumers {
    grid-auto-rows: auto;
  }
}
/* Three-column desktop down to ~920px, then single-column mobile. */
@media (max-width: 920px) {
  .board { grid-template-columns: 1fr; gap: 32px; }
  /* On mobile, the warehouse card collapses to its natural height —
     no need to stretch it to match the platform column. */
  .source-stack {
    display: flex;
    flex-direction: column;
    gap: var(--s1);
    flex: 0 1 auto;
  }
  .card--source {
    padding: var(--s1);
    align-content: start;
  }
  .source-stack > .card--source:last-child {
    flex: 0 1 auto;
  }
}

/* =========================================================================
   Connectors (JS-drawn paths + pulses)
   ========================================================================= */

.connectors {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 0;
  overflow: visible;
  opacity: 0;
  transition: opacity 500ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* Dot overlay sits ABOVE the cards (z-index > .column's 1) so the
   junction dots float on top of the chips rather than being clipped
   behind them. Same fade-in timing as the line layer. */
.connectors-overlay {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 2;
  overflow: visible;
  opacity: 0;
  transition: opacity 500ms cubic-bezier(0.22, 1, 0.36, 1);
}
.connectors-overlay[data-ready="true"] { opacity: 1; }

/* Each junction dot stays hidden until the connector reaches it: JS then
   pops the solid core in first, and its glow (halo) follows. Both start
   hidden; the appear animations are set per-dot in JS (guarded against
   resize replays). */
.connectors-overlay .dots .dot-core,
.connectors-overlay .dots .dot-halo { opacity: 0; }
@keyframes cl-dot-appear { from { opacity: 0; } to { opacity: 1; } }

/* Subtle breathe on the translucent halo (transform + fill-opacity only, so
   it composes with the opacity fade-in). JS sets the actual animation with
   the per-dot delay; this just carries the transform settings. */
.dot-halo {
  transform-origin: center;
  transform-box: fill-box;
}
@keyframes dot-halo-pulse {
  0%, 100% { transform: scale(1);    fill-opacity: 0.36; }
  50%      { transform: scale(1.12); fill-opacity: 0.52; }
}
@media (prefers-reduced-motion: reduce) {
  .dot-halo { animation: none; }
}
.connectors[data-ready="true"] {
  opacity: 1;
}
.connectors .lines path {
  fill: none;
  stroke: var(--card-stroke);   /* same violet as the card outlines */
  stroke-width: 1;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Hide the unused pulse paths and junction dots; the static lines stay. */
.connectors .pulses,
.connectors-overlay .dots { display: none; }

/* =========================================================================
   Flow cycle animation
   ONE bright gradient sweeps the card purple strokes left → right; a soft
   blurred dot runs the solid return leg; the Context Layer flourishes as the
   cycle closes on it. Shared ~4.5s timeline (sweep is SMIL in the SVG defs).
   ========================================================================= */

/* The sweep rides the card strokes: SVG outline rects (one per card) stroked
   with the animated #sweep gradient, sitting on top of the violet borders. */
/* The sweep rides BOTH the card outlines and the forward connector lines, so
   the one wave drifts continuously across the cards (connectors bridge the
   gaps). Solid borders/lines sit underneath; this layer is the moving light. */
.connectors-overlay .sweep rect,
.connectors-overlay .sweep path {
  fill: none;
  stroke: url(#sweep);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 2px rgba(94, 76, 255, 0.7));
}
/* The return leg uses the right→left, time-offset gradient. */
.connectors-overlay .sweep-return {
  fill: none;
  stroke: url(#sweep-return);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 2px rgba(94, 76, 255, 0.7));
}

/* Closing leg: the gradient continues up through the Context Layer's outline
   (bottom→top) as the cycle returns to it — same look as the other sweeps,
   replacing the old flourish glow. */
.connectors-overlay .sweep-ctx {
  fill: none;
  stroke: url(#sweep-ctx);
  stroke-width: 4;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 4px rgba(94, 76, 255, 0.85));
}

/* Arrow nodes glow as the sweep reaches them (per-arrow delay set in JS). */
.flow-on .flow-arrow { animation: arrowFlow 12s ease-in-out infinite; }
@keyframes arrowFlow {
  0%, 7%, 100% { filter: none; }
  2.5%         { filter: drop-shadow(0 0 3px var(--violet))
                         drop-shadow(0 0 5px rgba(94, 76, 255, 0.6)); }
}

@media (prefers-reduced-motion: reduce) {
  .flow-arrow { animation: none; }
  .connectors-overlay .sweep rect,
  .connectors-overlay .sweep path,
  .connectors-overlay .sweep-return,
  .connectors-overlay .sweep-ctx { stroke: none; }
}

/* Animated highlight that traces each card's purple outline, sequenced with
   the connector pulses into one shared flow (see setupFlowAnimations). */
.connectors-overlay .card-outlines path {
  fill: none;
  /* Lighter than the violet card border so the sweep reads as a bright pulse
     ON the border (same-colour would be invisible where it overlaps, leaving
     only the inward glow — which looked like an inner offset line). The soft
     glow gives the band a fluid, flowing presence as it passes card → card. */
  stroke: #c4baff;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 2px color-mix(in oklch, var(--violet) 42%, transparent));
  opacity: 0;        /* keyframes fade each card's sweep in during its window */
}
.connectors .pulses path {
  fill: none;
  stroke: var(--violet);
  stroke-width: 1.25;
  stroke-linecap: round;
  filter: drop-shadow(0 0 6px color-mix(in oklch, var(--violet) 32%, transparent))
          drop-shadow(0 0 2px color-mix(in oklch, var(--violet) 28%, transparent));
  /* Stay hidden until the keyframes explicitly fade the pulse in.
     Without this, the stroke-dasharray pattern is visible as a
     static violet line during the animation-delay period. */
  opacity: 0;
}

/* =========================================================================
   Column / label
   ========================================================================= */

.column {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);          /* tight spacing between label and content */
  position: relative;
  z-index: 1;
  min-height: 0;
}
.column__label {
  margin: 0;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--label);
}
/* "Auto-improve" label above the feedback loop — mobile only (on desktop the
   feedback card sits inside the Platform column, so no label is shown). */
@media (min-width: 921px) {
  .column__label--feedback { display: none; }
}

/* Source column: stretches to match the platform card. The warehouse
   card grows to fill the column so its bottom sits flush with the
   Lightdash Context Layer card. Icon+title pin to the top and the chips
   list pins to the bottom, so top and bottom padding both equal the
   card's intrinsic padding. */
.column--source { align-self: stretch; }
.source-stack {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  flex: 1;
  min-height: 0;
}
.source-stack > .card--source:last-child {
  flex: 1;               /* fill the column so the warehouse matches card heights */
  align-content: start;
}
.card--source {
  padding: var(--s1);
  align-content: start;
}

/* =========================================================================
   Card — shared
   ========================================================================= */

.card {
  background: var(--card);
  border: 1px solid var(--card-stroke);
  border-radius: var(--radius-card);
  padding: var(--s1);
  box-shadow: var(--shadow-card);
  /* Stacked layout (matches the PDF): icon on its own row, title below
     it, then the sub / chips. Single left-aligned column. The platform
     and feedback cards override this with their own flex head. */
  display: grid;
  grid-template-columns: 1fr;
  justify-items: start;
  align-content: center;    /* vertically center content block; even padding top/bottom */
  row-gap: var(--s-2);
  position: relative;
}
.card > .card__icon {
  /* Tighter gap below the icon to the title. */
  margin-bottom: var(--s-2);
}
.card > .card__title {
  margin: 0;
  line-height: 1.25;
}
.card > .card__sub {
  margin: 0;
}
.card > .chips {
  /* Title→chips gap mirrors the Lightdash Context Layer card's
     head→subgrid gap (var(--s1) = 16px). Card row-gap contributes
     var(--s-2) = 4px, so 12px margin completes the total. */
  margin: var(--s0) 0 0;
  /* justify-self (not align-self) is the inline axis — stretch the chip
     list full-width against the card's justify-items:start. */
  justify-self: stretch;
}

.card__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 33.6px;        /* 28px + 20% */
  height: 33.6px;
  overflow: hidden;
}
.card__icon img { width: 33.6px; height: 33.6px; }

/* The new Agents/Apps SVGs provide their own white tile via a masked
   group; no extra CSS background needed. Keeping the class as a hook in
   case future glyph-only SVGs land here. */
.card__icon--tile {
  background: transparent;
}

.card__title {
  font-family: var(--sans);
  font-weight: 600;
  /* Uniform with the subcard titles (Agents, Apps, Semantics…). */
  font-size: clamp(11px, calc(5px + 1.6vw), 14px);
  letter-spacing: -0.005em;
  margin: 0;
  color: var(--ink);
}
.card__title--lg {
  font-family: var(--display);
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.1;
}
.card__sub {
  margin: 0;
  font-size: 13px;
  color: var(--muted);
}

/* =========================================================================
   Source-of-truth cards (chips inside warehouse)
   ========================================================================= */

.chips {
  list-style: none;
  margin: var(--s-1) 0 0;
  padding: 0;
  /* Vertical stack of warehouse chips, each filling the card width evenly. */
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-1);
}
.chips li {
  /* Text matches the subcard sub ("Metrics · joins", "Business logic …"):
     12.5px, regular weight, muted colour — centered in the chip. */
  font-family: var(--sans);
  font-size: 12.5px;
  font-weight: 400;
  line-height: 1.45;
  color: var(--muted);
  padding: var(--s-1) var(--s0);   /* tighter top/bottom than the s0 box */
  border: 1px solid var(--rule);
  border-radius: var(--radius-sub);
  text-align: center;
  white-space: nowrap;
  /* Smooth the weight/colour shift on hover. */
  transition: color 0.15s ease, font-weight 0.15s ease;
}
/* Hover: chip text lifts to the title's weight/ink (like the card titles). */
.chips li:hover {
  font-weight: 600;
  color: var(--ink);
}

/* Mobile: warehouse chips stack in a single vertical list, text centred. */
@media (max-width: 920px) {
  .chips {
    grid-template-columns: 1fr;
  }
  .chips li {
    text-align: center;
  }
  /* Card-head titles read a little larger on mobile. */
  .card__head .card__title { font-size: 17px; }
  /* The sweep runs top→bottom through the stacked cards on mobile. */
  .connectors-overlay .sweep rect,
  .connectors-overlay .sweep path { stroke: url(#sweep-down); }
}

/* =========================================================================
   Platform card (parent)
   ========================================================================= */

.column--platform { align-self: stretch; }
.card--platform,
.card--feedback,
.card--consumption {
  display: flex;           /* override the grid layout used for other cards */
  flex-direction: column;
  /* Reset the inherited `.card { align-items: center }` so the head and
     the content block fill the card width instead of shrinking to their
     content and centering (most visible for the feedback lanes on mobile). */
  align-items: stretch;
  padding: var(--s1);
  gap: var(--s1);
}
/* Context Layer card hugs its content; the Feedback loop card below it
   absorbs any remaining column height so the animation stage breathes. */
/* Consumption + Context fill column 2 (they define its height); the warehouse
   and feedback hug their content and centre vertically in their full-height
   columns. */
.card--source,
.card--platform,
.card--consumption,
.card--feedback { flex: 1 1 auto; }
.card--platform .card__head,
.card--feedback .card__head,
.card--source .card__head,
.card--consumption .card__head {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--s-2);
  padding: 0;
  margin: 0;
  width: 100%;
  text-align: left;
}
.card--platform .card__head .card__icon,
.card--feedback .card__head .card__icon,
.card--source .card__head .card__icon,
.card--consumption .card__head .card__icon { margin-bottom: 0; }
.card--platform .card__title--lg {
  margin: 0;
  text-align: left;
  flex: 0 1 auto;
}

.subgrid {
  display: grid;
  /* minmax(0, 1fr) — without the explicit 0 min, `1fr` defaults to
     minmax(auto, 1fr) and the auto floor is the subcard's min-content
     width (long unbroken text like ".md · descriptions · ai_context").
     That overflows the platform card on narrow viewports. minmax(0, 1fr)
     lets the columns shrink so text wraps inside the card. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-auto-rows: auto;    /* subcards hug their content; no stretching */
  gap: var(--s-1);
  /* Fill the platform card's content width on every viewport, matching
     the warehouse chip column above. Pairs with the minmax(0, 1fr) cols
     so the subgrid spans full width without overflowing on narrow. */
  width: 100%;
}
/* Feedback loop subcards stack in a single vertical column; the Context Layer
   uses the base 2-column subgrid (2×2). */
.card--platform .subgrid,
.card--feedback .subgrid { grid-template-columns: minmax(0, 1fr); }

.subcard {
  border: 1px solid var(--rule);
  border-radius: var(--radius-sub);
  padding: var(--s0) var(--s0);
  /* Iconless subcards: title on row 1, sub on row 2 — flex column.
     2px gap (slightly tighter than var(--s-2)) per visual refinement. */
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.subcard > .subcard__title { margin: 0; }
.subcard > .subcard__sub   { margin: 0; }
.subcard__title {
  font-family: var(--sans);
  font-weight: 600;
  /* Smooth, viewport-driven shrink. clamp locks at 14px on wide
     screens, scales down with vw as the window narrows, and floors at
     11px to stay legible. No ellipsis — the title shrinks gradually
     instead of truncating. */
  font-size: clamp(11px, calc(5px + 1.6vw), 14px);
  /* Match the consumer card title's line-height so the visual gap
     between the title and the sub matches the consumer-card spacing. */
  line-height: 1.25;
  margin: 0;
  color: var(--ink);
  white-space: nowrap;
  min-width: 0;
}
.subcard__sub {
  margin: 0;
  font-size: 12.5px;
  color: var(--muted);
  /* Match the consumer card sub's inherited body line-height (1.45)
     for visual spacing consistency between the two card families. */
  line-height: 1.45;
  /* Allow long unbreakable tokens (e.g. ai_context, row-level) to break
     so the text never crosses the subcard's dashed border. */
  overflow-wrap: break-word;
}

/* Mobile-only loop-back card in the feedback column — stands in for the
   return arc (which only exists on desktop). Filled violet, lighter violet
   outline, white text. */
.subcard--loop { display: none; }
@media (max-width: 920px) {
  .subcard--loop {
    display: flex;
    background: var(--violet);
    border-color: #9b8dff;
  }
  .subcard--loop .subcard__title,
  .subcard--loop .subcard__sub { color: #fff; }
}

/* =========================================================================
   Consumers (2 cols × 3 rows, equal heights)
   ========================================================================= */

.consumers {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-auto-rows: auto;
  gap: var(--s-1);
}

.card--consumer {
  /* Grey-outlined subcard inside the consumption frame (icon/title/sub block
     pinned top-left), matching the Context Layer subcards exactly. */
  display: flex;
  flex-direction: column;
  justify-content: flex-start;   /* anchor the block to the top */
  align-items: flex-start;       /* keep it left-aligned */
  gap: 2px;                      /* same title→sub gap as .subcard */
  padding: var(--s0);
  align-self: stretch;
  height: 100%;
  border-color: var(--rule);     /* grey hairline, not the violet card stroke */
  border-radius: var(--radius-sub);
  box-shadow: none;
}
.card--consumer .card__title { font-size: clamp(11px, calc(5px + 1.6vw), 14px); }   /* match the Context Layer subcard titles */
/* Icon tile sits just above each consumer title. */
.card--consumer .card__icon { display: block; margin-bottom: -3px; line-height: 0; }
.card--consumer .card__icon img { width: 24px; height: 24px; border-radius: 4px; }
.card--consumer .card__sub { font-size: 12.5px; }

/* Accent variant: solid brand violet fill, white type */
.card--accent {
  background: #5E4CFF;
  border-color: transparent;
  color: oklch(99% 0.005 285);
}
.card--accent .card__title { color: oklch(99% 0.005 285); }
.card--accent .card__sub   { color: oklch(94% 0.020 285); }

/* =========================================================================
   Feedback loop card — sits below the Context Layer card in the platform
   column. Three labelled lanes (Metrics used → Pull request → Governed
   context) carry a looping animation: incoming metrics become a YAML pull
   request, get approved, and land as governed context. The list grows, so
   the layer keeps improving itself.
   ========================================================================= */

.floop {
  display: grid;
  /* PR lane holds the code diff; the governed lane holds the longest
     identifiers (median_profit, repeat_buyers) so both get a little extra
     over the metrics lane. minmax(0,…) lets lanes shrink gracefully. */
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.5fr) minmax(0, 1.1fr);
  gap: var(--s1);
  flex: 1 1 auto;          /* fill the card's remaining height */
  min-height: 150px;
  align-items: stretch;
  /* Same outline as the Context Layer subcards: a hairline rule, no fill. */
  border: 1px solid var(--rule);
  border-radius: var(--radius-sub);
  padding: var(--s1);
}
.floop__col {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  min-width: 0;
}
/* The PR lane is one connected unit: a status header bar sitting directly on
   top of the dark .yml window, split by a coloured divider, all inside a
   single rounded, clipped frame. */
.floop__col--pr {
  gap: 0;
  border-radius: var(--radius-sub);
  overflow: hidden;
  /* Grey hairline housing the whole PR window (header + .yml body). No fill —
     the header and dark body each fill their own half; a container background
     would bleed a dark fringe at the header's rounded top corners. */
  border: 1px solid var(--rule);
}

/* Lane labels reuse the editorial small-caps treatment of .column__label */
.floop__label {
  margin: 0;
  font-family: var(--sans);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);   /* same ink as the card titles */
  white-space: nowrap;
}
/* Live status pill — sits above the code block where the "Pull request"
   label used to be, spanning the PR lane. Steps through the loop's phases;
   colour shifts by state: violet (working) → amber (gap found) → green
   (improved). Editorial small-caps treatment, not pixel. */
.floop__status {
  align-self: stretch;        /* span the full PR-lane width */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  padding: 7px 10px;
  /* Fixed height so the header (and the whole PR window) never collapses while
     the status text is empty between type-outs. */
  min-height: 30px;
  /* header bar of the connected PR window — divider sits below it, kept to
     the same 1px hairline as every other line in the diagram */
  border-radius: 0;
  border-bottom: 1px solid var(--rule);
  /* White header with dark (title-ink) text by default; only the "not in
     semantics" warn state turns amber. */
  color: var(--ink);
  background: var(--card);
  transition: color .35s ease, background-color .35s ease, border-color .35s ease;
}
.floop__status-text { overflow: hidden; text-overflow: ellipsis; }
/* Fixed-width slot for the "Listening" dots so they fill in place without
   pushing the word. */
.fl-dots { display: inline-block; width: 1.4em; text-align: left; }
.floop__status[data-state="warn"] { color: #b8791f; background: #fbeed1; border-bottom-color: #e6c98f; }
.floop__status[data-state="ok"]   { color: var(--ink); background: var(--card); }
.floop__status-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
  animation: fl-status-pulse 1s ease-in-out infinite;
}
@keyframes fl-status-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
@media (prefers-reduced-motion: reduce) {
  .floop__status-dot { animation: none; }
}

/* ---- Metrics used lane (incoming natural-language questions) ---- */
.floop__metrics,
.floop__governed {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  min-width: 0;
  /* Reserve room for the full 4 chips so the lane — and therefore the whole
     feedback-loop section — stays one fixed height for the entire loop, even
     as the metrics list collapses and the governed list grows. (Sized for the
     10px pixel chips.) */
  min-height: 122px;
}
.fl-metric {
  position: relative;        /* so the drifting chip can lift above siblings */
  font-family: var(--pixel);
  font-size: 10px;
  letter-spacing: 0.01em;
  text-align: center;
  color: var(--ink-soft);
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius-tile);
  padding: 5px 8px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity .4s ease, transform .4s ease,
              border-color .35s ease, color .35s ease;
}
.fl-metric.in { opacity: 1; transform: translateY(0); }
/* Flagged: this metric isn't in the semantic layer yet — amber highlight
   with a brief attention pulse before it drifts into the .yml. */
.fl-metric.active {
  border-color: #e0962b;
  color: #a9701f;
  background: #fdf3df;
  animation: fl-flag 1.3s ease-in-out 1;
}
/* A double flash — the amber fill brightens and a halo pulses twice — to draw
   the eye to the flagged metric before it drags across. */
@keyframes fl-flag {
  0%, 100% { background: #fdf3df; box-shadow: 0 0 0 0 rgba(224, 150, 43, 0); }
  18%, 54% { background: #f6d489; box-shadow: 0 0 0 4px rgba(224, 150, 43, 0.28); }
  36%, 72% { background: #fdf3df; box-shadow: 0 0 0 0 rgba(224, 150, 43, 0); }
}
.fl-metric.resolved { opacity: .4; }

/* ---- Pull request lane (the literal YAML diff — pixel face) ---- */
.floop__pr {
  position: relative;
  flex: 1 1 auto;
  /* Holds the full 4-line diff (10px pixel font) up front, so the box doesn't
     grow as the lines type in. */
  min-height: 104px;
  background: #36394A;
  /* No own border/radius — the .floop__col--pr frame provides the outline. */
  padding: 10px 11px;
  overflow: hidden;
  transition: box-shadow .35s ease;
}
.floop__pr.is-approved {
  box-shadow: inset 0 0 0 2px color-mix(in oklch, #1fa971 40%, transparent);
}
.floop__code {
  display: block;
  font-family: var(--pixel);
  font-size: 10px;
  line-height: 1.95;
  letter-spacing: 0.01em;
  color: #d4d4e2;
  white-space: pre;
}
.floop__code .ln {
  display: block;
  overflow: hidden;
  text-overflow: clip;
  white-space: nowrap;
}
.fl-code__file { color: #f2f2f7; }
.fl-code__add  { color: #3fbf8e; }
.fl-code__name { color: #e0962b; }

/* APPROVED stamp slams over the diff once the PR is merged */
.floop__stamp {
  position: absolute;
  /* Fills the .yml body exactly; the .floop__col--pr frame clips the rounded
     corners, so no dark slivers and the header stays uncovered. */
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-family: var(--pixel);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: #fff;
  background: #1fa971;
  border-radius: 0;
  opacity: 0;
  transform: scale(1);
  pointer-events: none;
  /* Fades back to the dark .yml when un-stamped (the slam-in opacity is driven
     by the animation, so this transition only governs the fade-OUT). */
  transition: opacity .7s ease;
}
.floop__stamp-icon { width: 15px; height: 15px; flex: 0 0 auto; }
.floop__stamp.is-stamped {
  opacity: 1;     /* held by the class so removing it fades via the transition */
  animation: fl-stamp .45s cubic-bezier(.22, 0.9, 0.3, 1);
}
@keyframes fl-stamp {
  0%   { opacity: 0; transform: scale(1.18); }
  100% { opacity: 1; transform: scale(1); }
}

/* ---- Governed context lane (green ✓ rows — pixel identifiers) ---- */
.fl-gov {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  font-family: var(--pixel);
  font-size: 10px;
  letter-spacing: 0.01em;
  color: #1c9b6a;
  background: #e8f7f0;
  border: 1px solid #c6efdd;
  border-radius: var(--radius-tile);
  padding: 5px 8px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  opacity: 0;
  transform: translateX(6px);
  transition: opacity .45s ease, transform .45s ease;
}
.fl-gov.in { opacity: 1; transform: translateX(0); }
.fl-gov__tick { color: #1fa971; flex: 0 0 auto; }
.fl-gov.fresh { animation: fl-fresh .7s ease; }
@keyframes fl-fresh {
  0%   { background: #bfeed7; }
  40%  { background: #a7e6c8; }
  100% { background: #e8f7f0; }
}

@media (prefers-reduced-motion: reduce) {
  .fl-metric, .fl-gov { transition: none; }
  .floop__stamp.is-stamped { animation: none; opacity: 1; transform: scale(1); }
  .fl-gov.fresh { animation: none; }
}

/* On the single-column mobile board the three lanes stack vertically. */
@media (max-width: 920px) {
  .floop {
    grid-template-columns: 1fr;
    gap: var(--s1);
    min-height: 0;
  }
  /* Left-aligned lane labels at the consumer-card sub size (12.5px), inked
     to match the card titles (e.g. "Embedding") rather than the muted grey. */
  .floop__label {
    text-align: left;
    font-size: 12.5px;
    color: var(--ink);
  }
  /* Centre the chip text within the now full-width metrics + governed lanes,
     and make the chips a touch bigger — reusing the existing 10px size (the
     status pill) rather than introducing a new one, with slightly more
     padding. The .yml diff matches the same 10px on mobile. */
  .fl-metric,
  .fl-gov,
  .floop__code { font-size: 10px; }
  .fl-metric,
  .fl-gov { padding: 10px 12px; }
  /* Keep the .yml box roughly SQUARE on mobile (a wide-thin slab looks awkward
     full-width) — height tracks width via aspect-ratio, capped so wide tablets
     don't balloon. The diff is centred vertically within it. */
  .floop__pr {
    flex: 0 0 auto;
    min-height: 0;
    aspect-ratio: 1 / 1;
    max-height: 300px;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }
  .fl-metric { text-align: center; }
  .fl-gov { justify-content: center; }
  /* Lay the metrics + governed chips out in a 2×2 grid on mobile rather than
     a single tall column — so reserve only 2 rows, not the desktop column's 4. */
  .floop__metrics,
  .floop__governed {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--s-1);
    min-height: 92px;
  }
}

/* =========================================================================
   Entrance motion
   ========================================================================= */

@media (prefers-reduced-motion: no-preference) {
  .diagram__title,
  .column {
    opacity: 0;
    transform: translateY(8px);
    animation: rise 700ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
  }
  .diagram__title { animation-delay: 40ms; }
  .column--source     { animation-delay: 160ms; }
  .column--platform   { animation-delay: 240ms; }
  .column--consumers  { animation-delay: 320ms; }
  .column--feedback   { animation-delay: 400ms; }
}
@keyframes rise { to { opacity: 1; transform: translateY(0); } }
