/* Production article components. The renderer owns the markup; these classes
   define static, accessible defaults before any component enhancement runs. */

.article-figure,
.article-gallery,
.article-callout,
.article-related,
.interactive-component,
.code-example,
.data-table,
.unsupported-node {
  margin-top: 2em;
  margin-bottom: 2em;
}

.article-figure,
.article-gallery,
.wide-component {
  max-width: var(--wide-measure);
}

/* Interactive components fill their reading column (never inset). In an article the
   column rule already centres them at --content-measure; but a feed entry (an inline
   note renders its widget on the feed) leaves the <figure>'s UA side margins in place,
   which would inset the widget by 40px. Reset them so the widget fills the column on
   both desktop and mobile. A widget that ever needs to be wider than the column would
   be a separate, opt-in width — not the default. */
.post-content > .interactive-component {
  margin-left: 0;
  margin-right: 0;
}

.article-figure img {
  width: 100%;
  height: auto;
  background: var(--hairline);
}

.article-figure figcaption,
.article-gallery figcaption,
.data-caption,
.component-caption {
  max-width: var(--content-measure);
  margin: 10px auto 0;
  color: var(--ink-soft);
  font-size: 14px;
  line-height: 1.55;
}

/* Uppercase is this design's register for LABELS the system chooses (the rubrics,
   .component-label). A credit is words the author wrote — a name, a domain — so it is
   set in natural case, like the other authored micro-type (.post-meta, .post-via).
   Uppercasing it would flatten the capitalisation of a person's name, which is the one
   thing an attribution has to get right. */
.credit,
.source-note {
  display: block;
  margin-top: 4px;
  color: var(--ink-faint);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.05em;
}

/* A figure's attribution sits at the bottom right, under the image and under any
   caption — the print convention: the caption reads as prose on the left, the credit
   is a quiet marginal note on the right. It shares the figcaption's measure, so it
   aligns with the caption's edge rather than the (possibly wider) image's. Type matches
   the linkblog's "via" (.post-via) — the same gesture in a different place. */
.article-figure .credit {
  text-align: right;
  font-size: 11px;
  letter-spacing: 0.02em;
}

/* Underlined only faintly: enough that the link is findable without colour alone
   (which .post-via, riding beside a title, can get away with and a standalone line
   can't), quiet enough to stay a marginal note. */
.article-figure .credit a {
  color: inherit;
  text-decoration-color: var(--rule);
  text-underline-offset: 2px;
}

.article-figure .credit a:hover {
  color: var(--accent);
  text-decoration-color: currentColor;
}

/* caption-position="side": caption set beside the image on a wide figure. Falls
   back to the stacked default below the side breakpoint (mobile.css owns that). */
.article-figure--caption-side {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  column-gap: 24px;
  align-items: start;
}

.article-figure--caption-side figcaption {
  margin-top: 0;
}

.article-gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px;
}

.article-gallery-grid img {
  width: 100%;
  height: auto;
  background: var(--hairline);
}

.article-callout {
  padding: 18px 0 18px 22px;
  border-left: 2px solid var(--accent);
}

.article-callout[data-kind="method"] {
  border-left-color: var(--ink-faint);
}

.article-callout .component-label {
  display: block;
  margin-bottom: 8px;
  color: var(--accent);
  font-weight: 500;
}

.article-callout[data-kind="method"] .component-label {
  color: var(--ink-faint);
}

.article-callout > :last-child {
  margin-bottom: 0;
}

.article-related {
  padding-top: 16px;
  padding-bottom: 16px;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--hairline);
}

.article-related a {
  display: inline;
  color: var(--ink);
  font-family: var(--font-head);
  font-size: 20px;
  line-height: 1.35;
  text-decoration: none;
}

.article-related a:hover {
  color: var(--accent);
}

.article-related .component-label {
  display: block;
  margin-bottom: 8px;
  color: var(--ink-faint);
}

.interactive-component {
  position: relative;
}

/* The reserved stage height. `--stage-h` is emitted per component by the renderer
 * at its measured enhanced height (Thread 4), so the fallback box already occupies
 * the space the live widget will and nothing shifts when it mounts. Components that
 * fill the floor (the canvas sketches) emit no var and fall back to 420px. */
.interactive-component-stage {
  position: relative;
  display: grid;
  min-height: var(--stage-h, 420px);
  overflow: hidden;
  border: 1px solid var(--rule);
  border-radius: var(--radius-panel);
  background: var(--surface);
}

.interactive-component-stage > [data-map-mount],
.interactive-component-stage > [data-component-mount] {
  min-height: var(--stage-h, 420px);
}

/* Loading & failure presentation for JS readers. The static mount doubles as the
 * no-JS fallback: mandatory, and kept verbatim for readers WITHOUT JavaScript (the
 * `.js` class is set by an inline <head> script, so these rules never touch a no-JS
 * reader). For a JS reader the goals are: the verbose "needs JavaScript" prose /
 * placeholder SVG must never flash, real content should appear as soon as the
 * component mounts it (NOT gated on a slow "fully ready" signal — that would hide a
 * map while it streams tiles, and break libraries that must render into a visible
 * container), and a component that can't render should degrade to a quiet blank
 * space rather than a clunky box or error message.
 *
 * (1) Hide the static fallback's CONTENTS — never the mount element itself. Hiding
 * the mount (visibility:hidden) would stop a library like MapLibre, which renders
 * into that container, from ever finishing its load. Hiding only the children keeps
 * the container live while the verbose no-JS prose / placeholder SVG stays invisible.
 * The component removes this class the moment it renders real content (revealing it);
 * a component that fails before rendering keeps the class, so its fallback/error
 * contents stay hidden — i.e. blank. */
.js .interactive-component-fallback > * {
  visibility: hidden;
}

/* (2) Keep the stage CHROME quiet until the host marks the figure ready (successful
 * enhancement). Until then there is no border/surface and no focus control, so a
 * still-loading or failed interactive reads as empty space, not a placeholder box.
 *
 * Pre-ready the stage is pinned to a FIXED height (the reserved `--stage-h`), not
 * just floored by min-height, for two reasons (Thread 4): a hidden fallback that is
 * *taller* than the live widget — a chart's no-JS data table can be — must not
 * inflate the box (overflow is hidden, so the hidden table is simply clipped); and a
 * component whose enhanced stylesheet zeroes the stage min-height (the charts, to hug
 * their content) must not let a not-yet-enhanced sibling collapse. Once the host sets
 * data-interactive-ready the fixed height is released to min-height, and because the
 * reserved height equals the measured enhanced height, the settle is a no-op — no
 * shift. The value mirrors the min-height rule above (same var, same fallbacks). */
.js .interactive-component:not([data-interactive-ready]) .interactive-component-stage {
  height: var(--stage-h, 420px);
  border-color: transparent;
  background: transparent;
}

.js .interactive-component:not([data-interactive-ready]) .figure-frame__bar {
  visibility: hidden;
}

/* (3) Enhancement failed (a manifest 404, an unavailable capability, a component that
 * bailed without rendering). The mount still holds the no-JS static fallback — the
 * component never swapped in live content — so RESTORE that fallback rather than leave
 * the blank reserved box rule (2) would otherwise keep. Reveal its contents and let the
 * stage take its natural height with overflow released, so a fallback taller than the
 * reserved space (a chart's no-JS data table) is not clipped. The figure is NOT marked
 * ready, so the chrome stays quiet: this reads exactly as a no-JS reader's view, which
 * is the point. Placed after rule (2) — equal specificity, so source order wins. */
.js .interactive-component[data-map-status="error"] .interactive-component-stage {
  height: auto;
  overflow: visible;
}
.js .interactive-component[data-map-status="error"] .interactive-component-fallback > * {
  visibility: visible;
}

.interactive-component .component-caption p {
  margin: 0 0 0.65em;
  color: var(--ink-soft);
  font-size: 14px;
  line-height: 1.55;
}

.interactive-component .component-caption > :last-child {
  margin-bottom: 0;
}

/* The expand/restore (fullscreen) control and the expanded-frame styling live in
 * the figure-frame capability's frame.css — the host renders no control over the
 * component. See src/components/capabilities/figure-frame/v1/frame.css. */

.interactive-component-fallback,
.interactive-component-error {
  display: grid;
  align-content: center;
  gap: 8px;
  min-height: var(--stage-h, 420px);
  padding: 24px;
  color: var(--ink-soft);
  line-height: 1.5;
}

.interactive-component-fallback__label {
  margin: 0;
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.interactive-component-fallback p,
.interactive-component-error {
  margin: 0;
}

.interactive-component-error {
  color: #9f241b;
}

.code-example {
  border: 1px solid var(--hairline);
  border-radius: var(--radius-panel);
  background: var(--surface);
  overflow: hidden;
}

.code-example-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--hairline);
  color: var(--ink-faint);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.code-example pre {
  margin: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
}

.data-table {
  overflow-x: auto;
}

.data-table table {
  min-width: 560px;
}

.diagnostic-list {
  display: grid;
  gap: 8px;
  margin: 16px 0;
  padding: 0;
  list-style: none;
}

.diagnostic {
  padding: 10px 0;
  border-top: 1px solid var(--hairline);
  color: var(--ink-soft);
}

.diagnostic strong {
  color: var(--ink);
}

.diagnostic[data-severity="error"] strong {
  color: #9f241b;
}

.mode-dark .diagnostic[data-severity="error"] strong {
  color: #e87a6d;
}

.unsupported-node {
  padding: 16px 18px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-panel);
  background: var(--surface);
}

.unsupported-node .component-label {
  display: block;
  margin-bottom: 8px;
  color: #9f241b;
}

.unsupported-node pre {
  margin: 0;
  white-space: pre-wrap;
}

@media (max-width: 767px) {
  .interactive-component-stage,
  .interactive-component-stage > [data-map-mount],
  .interactive-component-stage > [data-component-mount],
  .interactive-component-fallback,
  .interactive-component-error {
    min-height: var(--stage-h-mobile, var(--stage-h, 340px));
  }

  /* The pre-ready fixed height (see rule (2)) at the mobile reservation. */
  .js .interactive-component:not([data-interactive-ready]) .interactive-component-stage {
    height: var(--stage-h-mobile, var(--stage-h, 340px));
  }

  /* Error state at mobile: mirror rule (3). The pre-ready rule just above re-pins the
     stage to the mobile reservation for EVERY not-ready component — including a failed
     one, whose restored fallback (a chart's no-JS data table, taller than the reserved
     space) would then be re-clipped and overflow onto the caption below. Release it to
     natural height with overflow visible, exactly as the desktop error rule does. Placed
     after the pin above (equal specificity) so source order wins. */
  .js .interactive-component[data-map-status="error"] .interactive-component-stage {
    height: auto;
    overflow: visible;
  }

}

@media print {
  .interactive-component-stage {
    min-height: auto;
  }

  /* A widget that never enhanced (no JS at print time, a failed capability) otherwise
     prints as a blank clipped box: rule (1) hides the fallback's contents and rule (2)
     pins the stage to the reserved --stage-h with overflow hidden. On paper there is no
     enhancement coming, so reveal the static fallback and release the reserved height so
     nothing is clipped — the no-JS view is the correct thing to print. */
  .js .interactive-component-fallback > * {
    visibility: visible;
  }

  .js .interactive-component:not([data-interactive-ready]) .interactive-component-stage {
    height: auto;
    overflow: visible;
  }

  .interactive-component-fallback,
  .interactive-component-error {
    min-height: auto;
  }
}
