/*
  The one stylesheet of the site.

  ---------------------------------------------------------------------------------------
  THE DESIGN, IN FIVE SENTENCES

  1. Cool paper, graphite ink, one signal colour. The neutrals are not grey: they are
     tinted toward the brand hue, so nothing on the page is dead. Saturation is spent
     almost entirely on what the visitor can act on.
  2. Type carries the identity, not ornament. A tight, large sans for headings, and a mono
     microvoice (uppercase, letterspaced, small) for labels, statuses and ids - this is a
     catalog whose primary reader is an agent hitting a JSON API, and the chrome says so
     without a single decorative image.
  3. Every colour, radius, shadow, duration and easing curve is a token, and re-skinning the
     whole site is one line: --h. The only literals below :root are white and black as mix
     partners, and the two light values inside the inverted ink card - which is fixed in both
     colour schemes on purpose, so a token that flips with the scheme would be wrong there.
  4. Motion is a system of two durations and three curves, used only for arrival, hover
     and page change. Nothing loops except a pulse that means "still working".
  5. The content is asset artwork. The frame stays quiet so the artwork does not have to
     compete with it - tinted canvases behind assets, hairline borders, soft shadows.

  ---------------------------------------------------------------------------------------
  WHY OKLCH

  Colours are written in oklch and mixed in oklab. Two reasons that matter here: a ramp
  built by stepping L in oklch has perceptually even steps (the same ramp in hex does not),
  and a tint of the accent mixed in oklab does not swing through grey the way an sRGB mix
  does. Support is Chrome 111 / Safari 15.4 / Firefox 113 - the same floor color-mix()
  already imposed on this file long before this rewrite.

  Every hue derives from --h. Changing it moves the buttons, the focus rings, the brand
  mark, the shadows and the neutrals together, which is the point: a brand colour that
  only lands on buttons reads as a sticker, not as a theme.

  The markup lives in .html files and is never generated in JavaScript, so the classes
  below are the whole contract between the two.
*/

:root {
  /* Let the browser theme scrollbars and form controls with us, not against us. */
  color-scheme: light dark;

  /*
    The two hues the whole site is built from. --h is the brand (215 = cyan - and 215,
    not the 195 a first guess lands on: 195 renders as teal/petrol; the colours everyone
    calls cyan sit at oklch hue 210-223, verified against reference values);
    --h-tint tints the neutrals toward it at a chroma low enough to still read as paper.

    A warning before you turn --h somewhere hot: the chroma values below are tuned to the
    sRGB gamut of THIS hue. Cyan holds far less chroma at mid lightness than orange or
    red do (about 0.10 vs 0.155 at L 0.53) - a hue swap that keeps the old chroma does
    not error, the browser just clamps it, and every accent quietly lands on the same
    clipped colour. Re-derive the chroma ceilings when you move the hue.
  */
  --h: 215;
  --h-tint: 210;

  --bg: oklch(0.982 0.008 var(--h-tint));
  --surface: oklch(0.999 0.002 var(--h-tint));
  --surface-2: oklch(0.963 0.009 var(--h-tint));
  --sunken: oklch(0.943 0.012 var(--h-tint));
  --fg: oklch(0.215 0.014 var(--h-tint));
  --muted: oklch(0.505 0.016 var(--h-tint));
  --line: oklch(0.902 0.011 var(--h-tint));
  --line-strong: oklch(0.835 0.015 var(--h-tint));

  /*
    --accent is dark enough to be link text on --surface (5.0:1, computed, not assumed);
    --accent-hot is the lighter, bluer end used only as the second stop of a gradient,
    never alone as text.
  */
  --accent: oklch(0.530 0.092 var(--h));
  --accent-hot: oklch(0.685 0.118 calc(var(--h) + 22));
  --accent-fg: oklch(0.995 0.008 var(--h-tint));
  --accent-soft: oklch(0.957 0.030 var(--h));
  --accent-line: oklch(0.855 0.075 var(--h));

  /*
    The state colours are pushed away from the brand on purpose. --ok moved from 165 to
    150 (unambiguous green) when the brand became cyan: at the low chroma a status dot
    carries, teal-green and cyan were a squint apart. Danger stays at hue 20 and is only
    ever drawn as an outline or a tint, never as a filled call to action.
  */
  --ok: oklch(0.505 0.115 150);
  --warn: oklch(0.555 0.120 85);
  --info: oklch(0.505 0.140 255);
  --danger: oklch(0.525 0.185 20);

  --ring: color-mix(in oklab, var(--accent) 26%, transparent);

  /* The dark card that carries the agent instructions - inverted in both schemes. */
  --ink: oklch(0.235 0.020 var(--h-tint));
  --ink-fg: oklch(0.895 0.012 var(--h-tint));
  --ink-line: oklch(0.330 0.022 var(--h-tint));
  --ink-accent: oklch(0.800 0.130 calc(var(--h) + 10));

  --r-xs: 7px;
  --r-sm: 10px;
  --r-md: 14px;
  --r-lg: 20px;
  --r-xl: 28px;
  --r-pill: 999px;

  /*
    Shadows are tinted with the neutral hue rather than pure black. A black shadow on
    tinted paper reads as dirt; one that shares the paper's hue reads as depth.
  */
  --shade: oklch(0.30 0.045 var(--h-tint));
  /*
    The 1px highlight along the top edge of a raised surface. It is a token because it is the
    one thing that must not survive into dark mode at full strength: a bright white edge on a
    light card reads as bevelled, the same edge on a dark card reads as cheap.
  */
  --edge: oklch(1 0 0 / 45%);
  --shadow-sm: 0 1px 2px color-mix(in oklab, var(--shade) 7%, transparent);
  --shadow: 0 1px 2px color-mix(in oklab, var(--shade) 6%, transparent),
    0 10px 24px -16px color-mix(in oklab, var(--shade) 26%, transparent);
  --shadow-lg: 0 2px 4px color-mix(in oklab, var(--shade) 7%, transparent),
    0 26px 50px -24px color-mix(in oklab, var(--shade) 32%, transparent);
  --glow: 0 8px 24px -12px color-mix(in oklab, var(--accent) 45%, transparent);

  --dur-1: 120ms;
  --dur-2: 200ms;
  --dur-3: 340ms;
  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring: cubic-bezier(0.34, 1.4, 0.55, 1);

  --font-sans: system-ui, -apple-system, "Segoe UI Variable Text", "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Menlo, Consolas,
    "Liberation Mono", monospace;

  --measure: 74rem;
  --gutter: 1.25rem;
  /*
    A single form field holds a sentence, not a paragraph. .form-row is a flex column with the
    default align-items: stretch, and the input rule sets no width - so a field that is the only
    thing in its row stretches to the full content width (--measure, 74rem). That made the editor
    and account tabs read far wider than the catalogue below them ever does, and wider than the
    grid-grouped fields on the same page. Bounded here, and in a .form-grid (where a column sizes
    the field already) lifted back to full.
  */
  --field: 38rem;
  /*
    One height for every control that can stand next to another one. A field takes its height
    from the inherited line-height (1.62), a .btn declares its own (1.2) - so a button beside an
    input came out about 10px shorter, and in a row aligned on its bottom edge that reads as the
    button sitting too low rather than as two different heights. Pinned here so the two cannot
    drift apart again; buttons that are deliberately not field-sized (the tab row) opt out.
  */
  --control: 2.9rem;

  /* Film grain. Kept this low it is not visible as texture, only as "not flat". */
  --grain: 0.035;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: oklch(0.168 0.012 var(--h-tint));
    --surface: oklch(0.213 0.013 var(--h-tint));
    --surface-2: oklch(0.253 0.015 var(--h-tint));
    --sunken: oklch(0.148 0.010 var(--h-tint));
    --fg: oklch(0.958 0.008 var(--h-tint));
    --muted: oklch(0.720 0.016 var(--h-tint));
    --line: oklch(0.295 0.014 var(--h-tint));
    --line-strong: oklch(0.400 0.018 var(--h-tint));

    /*
      The accent inverts its role in the dark: it becomes the light thing on a dark ground,
      so it has to gain lightness (and give up a little chroma) or it turns to mud.
    */
    --accent: oklch(0.780 0.130 calc(var(--h) + 8));
    --accent-hot: oklch(0.855 0.077 calc(var(--h) + 24));
    --accent-fg: oklch(0.185 0.020 var(--h-tint));
    --accent-soft: oklch(0.295 0.048 var(--h));
    --accent-line: oklch(0.420 0.070 var(--h));

    --ok: oklch(0.790 0.130 150);
    --warn: oklch(0.825 0.120 85);
    --info: oklch(0.770 0.105 255);
    --danger: oklch(0.740 0.145 22);

    /* Elevated above --surface, so the ink card still reads as a card here. */
    --ink: oklch(0.248 0.020 var(--h-tint));
    --ink-fg: oklch(0.912 0.012 var(--h-tint));
    --ink-line: oklch(0.365 0.022 var(--h-tint));

    --shade: oklch(0.02 0 0);
    --edge: oklch(1 0 0 / 6%);
    --shadow-sm: 0 1px 2px color-mix(in oklab, var(--shade) 45%, transparent);
    --shadow: 0 1px 2px color-mix(in oklab, var(--shade) 45%, transparent),
      0 10px 24px -16px color-mix(in oklab, var(--shade) 85%, transparent);
    --shadow-lg: 0 2px 4px color-mix(in oklab, var(--shade) 45%, transparent),
      0 26px 50px -24px color-mix(in oklab, var(--shade) 90%, transparent);

    --grain: 0.055;
  }
}

/* =====================================================================================
   BASE
   ===================================================================================== */

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

/*
  Reserve the scrollbar's track whether or not it is showing. Without it the viewport is
  17px narrower on a long page than on a short one, and every centred thing on the site -
  header .inner, main, the footer - jumps sideways by half of that on each navigation. The
  gutter is the standard cure and costs the width only once, always, instead of sometimes.
*/
html {
  scrollbar-gutter: stable;
}

/*
  The column is what pins the footer: main takes the leftover height (flex: 1), so on a
  page shorter than the viewport the footer sits on the bottom edge rather than floating
  under the content. min-height on the body alone never did that - it stretched the body,
  not the space between the last section and the footer.
*/
body {
  margin: 0;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  color: var(--fg);
  font: 16px/1.6 var(--font-sans);
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  /*
    Two very wide, very faint washes of the brand hue at the top corners. They are what
    keeps a tinted-paper page from looking like an unstyled document, and they are cheap:
    fixed attachment means one paint, never a repaint on scroll.
  */
  background:
    radial-gradient(
      70rem 34rem at 8% -14%,
      color-mix(in oklab, var(--accent) 13%, transparent),
      transparent 68%
    ),
    radial-gradient(
      52rem 28rem at 104% -8%,
      color-mix(in oklab, var(--accent-hot) 10%, transparent),
      transparent 66%
    ),
    var(--bg);
  background-attachment: fixed;
}

/*
  Film grain over the whole viewport at 3-6% opacity. It sits at z-index -1: above the page
  background, below every element, and unclickable. This is the one purely decorative layer
  in the file and it is 300 bytes of inline SVG - no image request.
*/
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--grain);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/*
  width: 100% is not redundant here, it is what makes every page the same width.

  main is a flex item of the body column. A flex item normally stretches across the cross
  axis - but stretch is switched off the moment a cross-axis margin is auto, and this rule
  needs `margin: 0 auto` to centre. Without an explicit width main therefore sized to its
  contents: the assets tab (a wide grid) filled the measure, the profile tab (one capped
  form column) shrank to about half of it, and each tab centred itself at its own width.
  The header and the footer kept their 74rem, so the page appeared to breathe in and out
  underneath a fixed chrome. This arrived with the sticky footer - before the body became
  a flex column, main was a block and `max-width` alone did the right thing.
*/
main {
  flex: 1 0 auto;
  width: 100%;
  max-width: var(--measure);
  margin: 0 auto;
  padding: 2.25rem var(--gutter) 5.5rem;
}

/*
  Home reads in one order and renders in another, and both orders are deliberate.

  Document order is what a fetch tool gets, and the shell is that reader's whole answer: the
  two things it can act on - the MCP connection and the REST endpoints - come first, before a
  search form it cannot type into and three containers JavaScript fills. Visual order is what
  a person gets, and a person on a catalog page wants the catalog, not the API reference.

  The cost is real and worth naming: focus follows the document, not the picture, so tabbing
  through home goes hero, MCP, REST, search, results. That is a WCAG 2.4.3 trade, taken with
  eyes open on a page whose primary reader is a machine. Nothing else on the site does this.

  The delays live here rather than on main's generic nth-child stagger for the same reason
  the order does: a stagger that counts document position would cascade in the reading order,
  not the seen one, and would read as the page assembling itself out of sequence.
*/
.home {
  display: flex;
  flex-direction: column;
}

.home > #hero {
  order: 1;
}

.home > #search {
  order: 2;
  transition-delay: 40ms;
}

.home > #results {
  order: 3;
  transition-delay: 80ms;
}

.home > #mcp {
  order: 4;
  transition-delay: 120ms;
}

.home > #rest {
  order: 5;
  transition-delay: 160ms;
}

/* The sticky header would otherwise cover the target of every in-page link. */
[id] {
  scroll-margin-top: 5.5rem;
}

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

::selection {
  background: color-mix(in oklab, var(--accent) 24%, transparent);
}

* {
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}

/* ---- Type ---------------------------------------------------------------------------- */

h1,
h2,
h3 {
  line-height: 1.15;
  text-wrap: balance;
}

h1 {
  font-size: clamp(2rem, 1.35rem + 2.2vw, 2.95rem);
  margin: 0 0 0.6rem;
  letter-spacing: -0.04em;
  font-weight: 680;
}

h2 {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  font-size: 1.3rem;
  margin: 3rem 0 1.1rem;
  letter-spacing: -0.025em;
  font-weight: 640;
}

/*
  The section marker: a short accent rule in front of every top-level heading. It is the
  cheapest way to make a long scrolling document read as chapters, and it costs no markup.
  Panels reset it below - a marker inside a card would compete with the card's own edge.
*/
h2::before {
  content: "";
  flex: none;
  width: 1.1rem;
  height: 3px;
  border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--accent), var(--accent-hot));
}

h3 {
  font-size: 1.05rem;
  margin: 0 0 0.4rem;
  letter-spacing: -0.015em;
  font-weight: 620;
}

p {
  margin: 0 0 1rem;
}

a {
  color: var(--accent);
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  transition: color var(--dur-1) var(--ease);
}

a:hover {
  color: color-mix(in oklab, var(--accent) 72%, var(--fg));
}

.lead {
  font-size: 1.09rem;
  line-height: 1.62;
  color: var(--muted);
  max-width: 46rem;
  text-wrap: pretty;
}

.note,
.hint {
  color: var(--muted);
  font-size: 0.9rem;
}

/*
  The mono microvoice. Labels, statuses and counts - the words that name a thing rather
  than say it - are set small, uppercase and letterspaced. Identifiers (slugs, pack ids)
  get the same family further down but are never uppercased: they are real values that
  someone will type back, and shouting them would misrepresent them.
*/
.eyebrow,
.badge,
.stat span,
.field dt {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.055em;
  text-transform: uppercase;
}

/*
  Inline code is a route, a slug or an identifier - by construction one long word with no
  break opportunity in it. Left alone it does not wrap, it pushes: on a phone a single
  `/api/recommend?project={description}&count={n}` in a paragraph made the whole document
  wider than the viewport, and everything that measures itself against the viewport - the
  sticky header above all - then ended in the middle of the page. Breaking mid-token is the
  only alternative to that, and it is the lesser evil for a value that is read, not retyped.
*/
code {
  font-family: var(--font-mono);
  font-size: 0.86em;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-xs);
  padding: 0.1em 0.36em;
  overflow-wrap: anywhere;
}

pre {
  position: relative;
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 2.35rem 1.15rem 1.15rem;
  overflow-x: auto;
  box-shadow: var(--shadow-sm);
  line-height: 1.65;
}

/*
  Three dots in the corner of every code block. The whole promise of this site is that a
  machine drives it from a terminal, and this is that promise stated in the furniture.
*/
pre::before {
  content: "";
  position: absolute;
  top: 1rem;
  left: 1.15rem;
  width: 2.5rem;
  height: 0.5rem;
  background:
    radial-gradient(circle at 0.25rem 50%, var(--line-strong) 0 0.25rem, transparent 0),
    radial-gradient(circle at 1.25rem 50%, var(--line-strong) 0 0.25rem, transparent 0),
    radial-gradient(circle at 2.25rem 50%, var(--line-strong) 0 0.25rem, transparent 0);
}

/*
  Inside a block the opposite is right: the block scrolls (overflow-x above), and a command
  line broken mid-flag reads as two commands. A copied line must survive the copy.
*/
pre code {
  background: none;
  border: none;
  padding: 0;
  font-size: 0.85rem;
  overflow-wrap: normal;
}

[hidden] {
  display: none !important;
}

/* =====================================================================================
   MOTION

   Arrival, hover, page change. Nothing else moves, and nothing loops except the pulse
   that means "a request is still out".
   ===================================================================================== */

@keyframes enter-fade {
  from {
    opacity: 0;
  }
}

@keyframes enter-rise {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
}

@keyframes enter-pop {
  from {
    opacity: 0;
    transform: translateY(9px) scale(0.985);
  }
}

@keyframes soft-pulse {
  50% {
    opacity: 0.45;
  }
}

@keyframes page-out {
  to {
    opacity: 0;
  }
}

@keyframes page-in {
  from {
    opacity: 0;
    transform: translateY(12px) scale(0.995);
  }
}

@keyframes header-lift {
  to {
    box-shadow: var(--shadow);
    border-bottom-color: var(--line-strong);
  }
}

/*
  Staggered arrival for anything that comes in as a list.

  The fill mode is backwards, never both: an animation left in force after it ends
  overrides ordinary declarations, and the hover lift of these very cards is an ordinary
  declaration. Backwards is also the only reason a fill mode is wanted here - an item
  waiting for its delay has to be invisible, not visible and then briefly gone.

  The delay is capped at the ninth child. A pack of a thousand assets must not trickle in
  for half a minute, and past about nine the stagger has already done its work.
*/
:is(.pack-grid, .grid, .rows, .upload-list, .icon-picker) > * {
  animation: enter-pop var(--dur-3) var(--ease-out) backwards;
}

:is(.pack-grid, .grid, .rows, .upload-list, .icon-picker) > :nth-child(2) {
  animation-delay: 25ms;
}

:is(.pack-grid, .grid, .rows, .upload-list, .icon-picker) > :nth-child(3) {
  animation-delay: 50ms;
}

:is(.pack-grid, .grid, .rows, .upload-list, .icon-picker) > :nth-child(4) {
  animation-delay: 75ms;
}

:is(.pack-grid, .grid, .rows, .upload-list, .icon-picker) > :nth-child(5) {
  animation-delay: 100ms;
}

:is(.pack-grid, .grid, .rows, .upload-list, .icon-picker) > :nth-child(6) {
  animation-delay: 125ms;
}

:is(.pack-grid, .grid, .rows, .upload-list, .icon-picker) > :nth-child(7) {
  animation-delay: 150ms;
}

:is(.pack-grid, .grid, .rows, .upload-list, .icon-picker) > :nth-child(8) {
  animation-delay: 175ms;
}

:is(.pack-grid, .grid, .rows, .upload-list, .icon-picker) > :nth-child(n + 9) {
  animation-delay: 200ms;
}

/*
  A tab switch is a reveal, not an arrival.

  The tabs work by toggling [hidden] on the panel wrappers, so the one that opens goes from
  display:none back into flow - and that restarts every entry animation inside it. Every click
  on a tab therefore replayed the rise of that panel's lists and notices, which reads as the
  page reloading under you rather than as a section coming forward. The page's own arrival is
  already animated once, by the view transition on main; the sections inside it are not new.

  Named rather than blanket, because animation is not only used for arrival here: the pulse that
  means "a request is still out" lives inside these panels too, and it has to keep running.
*/
[data-panel] :is(.pack-grid, .grid, .rows, .upload-list, .icon-picker) > *,
[data-panel] :is(.notice, .intake-result) {
  animation-name: none;
}

/*
  Page transitions. router.js swaps main inside document.startViewTransition, so the
  browser snapshots the old and the new page and crossfades them for us.

  Header and footer get their own transition names, which lifts them out of the root
  snapshot: they stay put while the content underneath them changes. Without this the
  whole viewport - sticky header included - would fade, and every navigation would look
  like a full page load, which is the thing the SPA exists not to look like.
*/
header.site {
  view-transition-name: site-header;
}

footer.site {
  view-transition-name: site-footer;
}

::view-transition-old(root) {
  animation: page-out var(--dur-2) var(--ease) both;
}

::view-transition-new(root) {
  animation: page-in var(--dur-3) var(--ease-out) both;
}

/*
  Where the View Transition API is missing, router.js marks the document and we animate the
  incoming content ourselves. Same feel, one element deep instead of a snapshot.
*/
.no-view-transitions main > * {
  animation: enter-rise var(--dur-3) var(--ease-out) backwards;
}

/*
  The cold arrival: the first paint of home, which is the one load that has no previous page
  to come from. Armed by boot.js before that paint and lifted a frame
  after the fonts are ready.

  Opacity alone, and deliberately no transform: a fade is composited, so it cannot contend
  with the font swap, the logo decode and the first layout - the contention that made the
  cold load stutter when it was still driven through a view transition. A transform would
  put it straight back on the main thread.

  A transition rather than an animation, for one reason that matters more than the effect: a
  transition that never gets its cue leaves the content where it already is. An entry
  animation with a fill mode holds the content invisible until something ends it, and the
  home markup is the whole answer for a reader without JavaScript - it may be late, never
  absent. Hence also no `all`: only opacity may ever be held back here.

  The delays are unscoped on purpose. They have to be in force at the moment .cold goes
  away, which is when the class is no longer there to carry them.
*/
main > *,
.home > * {
  transition: opacity var(--dur-3) var(--ease-out);
}

main > :nth-child(2) {
  transition-delay: 40ms;
}

main > :nth-child(3) {
  transition-delay: 80ms;
}

main > :nth-child(4) {
  transition-delay: 120ms;
}

main > :nth-child(n + 5) {
  transition-delay: 160ms;
}

.cold main > *,
.cold .home > * {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }

  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation-duration: 1ms !important;
  }
}

/* =====================================================================================
   HEADER, NAV, ACCOUNT MENU, FOOTER
   ===================================================================================== */

header.site {
  position: sticky;
  top: 0;
  z-index: 20;
  background: color-mix(in oklab, var(--surface) 72%, transparent);
  backdrop-filter: blur(16px) saturate(1.5);
  border-bottom: 1px solid var(--line);
  transition: border-color var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}

/*
  The header content is held to the same measure as main. Before this it ran edge to edge
  while the page below it was centred, so on a wide screen the brand and the content it
  belongs to were a hand's width apart.
*/
header.site > .inner {
  max-width: var(--measure);
  margin: 0 auto;
  padding: 0.7rem var(--gutter);
  display: flex;
  align-items: center;
  gap: 1.1rem;
  min-height: 4rem;
}

/*
  The header's shadow is driven by the scroll position itself - no listener, no layout read
  per frame. Where scroll-driven animation is missing the header simply keeps its hairline
  border, which is what it looks like at the top of the page anyway.
*/
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    header.site {
      animation: header-lift linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 5rem;
    }
  }
}

/*
  The brand is the full logo lockup (mark plus wordmark) - no chip, no text beside it.

  The logo is white on transparency, so it cannot simply be painted: its own pixels are the
  wrong colour in both schemes. The drawing is used as a MASK on the link and the colour comes
  from the link's background, which is why the token can be an accent rather than a filter
  guess - filter can darken or invert a bitmap, it cannot tint it to an arbitrary hue.
  The img stays in the markup (alt text, and its intrinsic width/height reserve the box before
  the PNG arrives so the nav does not jump on first paint); it is only made transparent, not
  removed, because it is what gives the link its size.
*/
.brand {
  display: inline-flex;
  align-items: center;
  background-color: var(--fg);
  -webkit-mask: url("/brand/logo.png") no-repeat center / contain;
  mask: url("/brand/logo.png") no-repeat center / contain;
  text-decoration: none;
  transition: background-color var(--dur-1) var(--ease);
}

.brand img {
  display: block;
  height: 1.75rem;
  width: auto;
  opacity: 0;
}

/*
  Hover goes to the page's ink, which is the requested white in the dark scheme (--fg is
  L 0.958 there) and ink on the light one - a literal white on the light header would erase
  the logo instead of highlighting it. One token, no scheme override, and in both cases it is
  the colour the logo carried before it was tinted.
*/
.brand:hover {
  background-color: var(--accent);
}

nav.main {
  display: flex;
  gap: 0.2rem;
  margin-right: auto;
  flex-wrap: wrap;
}

/*
  .nav-link carries the same shape outside nav.main - the sign in link sits beside the
  nav, not in it (the nav takes the auto margin that pushes everything after it right).
*/
nav.main a,
.nav-link {
  position: relative;
  color: var(--muted);
  text-decoration: none;
  font-size: 0.93rem;
  font-weight: 520;
  padding: 0.4rem 0.75rem;
  border-radius: var(--r-pill);
  transition: color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease);
}

nav.main a:hover,
.nav-link:hover {
  color: var(--fg);
  background: var(--surface-2);
}

/*
  On a phone the header has one line's worth of room and four things that want it: the
  lockup, two nav words, the account control. Nothing here may be dropped - a nav that
  wraps turns the bar into two rows and pushes the page down on every route - so each part
  gives up a little instead: the gutter, the gap, the logo's height, the pill padding.
  Below roughly 20rem of usable width the wrap returns, and that is the honest outcome.
*/
@media (max-width: 30rem) {
  header.site > .inner {
    gap: 0.5rem;
    padding-inline: 0.9rem;
  }

  .brand img {
    height: 1.4rem;
  }

  nav.main {
    gap: 0;
  }

  nav.main a,
  .nav-link {
    padding-inline: 0.5rem;
    font-size: 0.88rem;
  }
}

nav.main a[aria-current="page"] {
  color: color-mix(in oklab, var(--accent) 85%, var(--fg));
  background: var(--accent-soft);
  font-weight: 580;
}

footer.site {
  border-top: 1px solid var(--line);
  background: color-mix(in oklab, var(--surface) 55%, transparent);
  color: var(--muted);
  font-size: 0.9rem;
}

footer.site .inner {
  max-width: var(--measure);
  margin: 0 auto;
  padding: 2rem var(--gutter) 2.5rem;
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2.5rem;
  align-items: baseline;
  justify-content: space-between;
}

footer.site p {
  margin: 0;
  max-width: 42rem;
}

footer.site nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.25rem;
}

footer.site nav a {
  color: var(--muted);
  text-decoration: none;
  font-size: 0.8rem;
}

footer.site nav a:hover {
  color: var(--accent);
}

/* ---- Account menu -------------------------------------------------------------------- */

.account-menu {
  position: relative;
}

.account-menu > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.25rem 0.5rem 0.25rem 0.25rem;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  cursor: pointer;
  list-style: none;
  color: var(--muted);
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}

.account-menu > summary::-webkit-details-marker {
  display: none;
}

.account-menu > summary:hover,
.account-menu[open] > summary {
  background: var(--surface-2);
  border-color: var(--line);
}

.account-menu > summary > span[aria-hidden="true"]:last-child {
  display: inline-block;
  font-size: 0.7rem;
  transition: transform var(--dur-2) var(--ease);
}

.account-menu[open] > summary > span[aria-hidden="true"]:last-child {
  transform: rotate(180deg);
}

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  background: linear-gradient(145deg, var(--accent-hot), var(--accent) 75%);
  color: var(--accent-fg);
  font-weight: 680;
  font-size: 0.88rem;
  box-shadow: var(--glow), inset 0 1px 0 oklch(1 0 0 / 25%);
}

.menu-panel {
  position: absolute;
  right: 0;
  top: calc(100% + 0.55rem);
  min-width: 15.5rem;
  padding: 0.4rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  display: grid;
  gap: 0.1rem;
  transform-origin: top right;
  animation: enter-pop var(--dur-2) var(--ease-out) backwards;
}

.menu-panel a,
.menu-panel button {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0.5rem 0.7rem;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--fg);
  font: inherit;
  font-size: 0.94rem;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}

.menu-panel a:hover,
.menu-panel button:hover {
  background: var(--accent-soft);
  color: color-mix(in oklab, var(--accent) 80%, var(--fg));
}

.menu-panel .who {
  display: grid;
  gap: 0.1rem;
  padding: 0.55rem 0.7rem 0.65rem;
  border-bottom: 1px solid var(--line);
  margin-bottom: 0.2rem;
}

.menu-panel .who span {
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  overflow-wrap: anywhere;
}

.menu-panel hr {
  border: 0;
  border-top: 1px solid var(--line);
  margin: 0.2rem 0;
}

/* =====================================================================================
   CONTROLS
   ===================================================================================== */

.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  font: inherit;
  font-size: 0.93rem;
  font-weight: 560;
  line-height: 1.2;
  padding: 0.58rem 1.05rem;
  min-height: var(--control);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--fg);
  cursor: pointer;
  text-decoration: none;
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease),
    background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease);
}

.btn:hover {
  background: var(--surface-2);
  border-color: color-mix(in oklab, var(--accent) 30%, var(--line-strong));
  box-shadow: var(--shadow);
}

.btn:active {
  transform: scale(0.98);
  box-shadow: var(--shadow-sm);
}

.btn:disabled {
  opacity: 0.5;
  box-shadow: none;
}

.btn:disabled:hover {
  transform: none;
  box-shadow: none;
  background: var(--surface);
  border-color: var(--line-strong);
}

/*
  A disabled control here always means "not allowed" - a locked pack, someone else's
  decision, a form that is not yours. Nothing in this app disables a button while a request
  is in flight, so the wait cursor this once carried was telling every visitor "in a moment"
  where it meant "no".
*/
button:disabled,
input:disabled,
select:disabled,
textarea:disabled,
fieldset:disabled {
  cursor: not-allowed;
}

.btn-primary {
  background: linear-gradient(180deg, var(--accent-hot), var(--accent) 85%);
  border-color: color-mix(in oklab, var(--accent) 72%, oklch(0 0 0));
  color: var(--accent-fg);
  box-shadow: var(--glow), inset 0 1px 0 oklch(1 0 0 / 22%);
}

.btn-primary:hover {
  background: linear-gradient(
    180deg,
    color-mix(in oklab, var(--accent-hot) 88%, oklch(1 0 0)),
    color-mix(in oklab, var(--accent) 92%, oklch(1 0 0)) 85%
  );
  border-color: color-mix(in oklab, var(--accent) 72%, oklch(0 0 0));
  box-shadow: 0 12px 30px -14px color-mix(in oklab, var(--accent) 60%, transparent),
    inset 0 1px 0 oklch(1 0 0 / 28%);
}

/*
  One sheen, on hover, on the primary button only. It is the single flourish in the file and
  it is deliberately alone: a page where everything glints has no emphasis left to give the
  one control that matters.
*/
.btn-primary::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 30%, oklch(1 0 0 / 28%) 50%, transparent 70%);
  transform: translateX(-120%);
  transition: transform var(--dur-3) var(--ease-out);
}

.btn-primary:hover::after {
  transform: translateX(120%);
}

.btn-primary:disabled:hover {
  background: var(--accent);
}

.btn-quiet {
  border-color: transparent;
  background: none;
  color: var(--muted);
  box-shadow: none;
}

.btn-quiet:hover {
  background: var(--surface-2);
  border-color: transparent;
  color: var(--fg);
  box-shadow: none;
}

.btn-danger {
  border-color: color-mix(in oklab, var(--danger) 40%, var(--line));
  color: var(--danger);
}

.btn-danger:hover {
  background: color-mix(in oklab, var(--danger) 10%, var(--surface));
  border-color: color-mix(in oklab, var(--danger) 58%, var(--line));
  color: var(--danger);
}

/* ---- Fields -------------------------------------------------------------------------- */

input,
select,
textarea {
  font: inherit;
  color: var(--fg);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  padding: 0.58rem 0.8rem;
  transition: border-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease),
    background-color var(--dur-1) var(--ease);
}

/*
  Pin the field to the shared control height. Single-line fields only: a textarea has its own
  min-height because it holds a paragraph, and the file input is laid out by its picker button.
*/
input:not([type="file"]),
select {
  min-height: var(--control);
}

input::placeholder,
textarea::placeholder {
  color: color-mix(in oklab, var(--muted) 65%, transparent);
}

input:hover:not(:disabled),
select:hover:not(:disabled),
textarea:hover:not(:disabled) {
  border-color: color-mix(in oklab, var(--accent) 32%, var(--line-strong));
}

input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--ring);
}

/*
  The keyboard-focus outline, shown only when the browser decides it should be
  (Tab navigation, not a mouse click). It is for controls that have no focus
  treatment of their own - buttons, links, the menu summary.

  Form fields are deliberately excluded. An input already announces focus in the
  :focus rule above - an accent border and a 3px ring, plus the caret - and form
  fields match :focus-visible even on a mouse click (the caret proves they have
  it). Layering this outline on top was a second, competing indicator that only
  made the field look boxed in. The ring is their indicator; this outline is for
  everything that doesn't have one.
*/
.btn:focus-visible,
a:focus-visible,
summary:focus-visible,
button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

textarea {
  min-height: 6rem;
  resize: vertical;
  line-height: 1.55;
}

/*
  The bound sits on the row, not on the input. A row is the one place that knows it holds a
  single field; putting max-width on input/select/textarea would also hit the ones inside the
  search bar, the asset cards and the file picker, each of which is sized by its own container.
*/
.form-row {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin-bottom: 1.1rem;
  max-width: var(--field);
}

.form-row label {
  font-size: 0.88rem;
  font-weight: 580;
}

.form-row label:has(> input, > textarea, > select) {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.form-inline {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: flex-end;
}

/*
  The grid carries the bottom margin its rows give up below. A .form-row spaces itself (1.1rem),
  but inside a grid that margin is dropped in favour of the gap - which left whatever follows the
  grid sitting straight against the last field: the Clear button under the style fields in the
  editor, and the same for the asset card's actions.
*/
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
  gap: 0.75rem 1rem;
  margin-bottom: 1.1rem;
}

/*
  In a grid the column already decides the width, and the tracks are narrower than --field
  anyway. Leaving the bound on would cap a wide row (.form-row-wide spans every column) at
  38rem in the middle of a full-width grid, which reads as a broken cell rather than a measure.
*/
.form-grid .form-row,
.form-inline .form-row {
  margin: 0;
  max-width: none;
}

.form-row-wide {
  grid-column: 1 / -1;
}

/*
  A form that is the whole panel, not a column inside it. The sentence measure on .form-row is
  right for a title or a tag line, but wrong where the panel is the page and the field is the
  only thing in it: the describe form left its right half empty and read as a left margin. A
  textarea holds a paragraph anyway, so it takes the panel; the options line then spans the
  same width, the optional field grows into the gap, and the submit button anchors the right
  edge - so the box reads as one block with two aligned edges.
*/
.form-stretch .form-row {
  max-width: none;
}

.form-stretch .form-grow {
  flex: 1 1 16rem;
}

.form-stretch .form-inline > .btn {
  margin-left: auto;
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  align-items: center;
}

/* ---- The search bar ------------------------------------------------------------------ */

/*
  One control, not four. The border and the focus ring belong to the bar; the input and the
  select inside it are transparent, so a visitor sees a single field with a button on the
  end rather than a row of separate widgets that happen to sit next to each other.
*/
.searchbar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 1.1rem 0 0.6rem;
  padding: 0.4rem 0.4rem 0.4rem 0.75rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-sm);
  transition: border-color var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}

.searchbar:focus-within {
  border-color: color-mix(in oklab, var(--accent) 45%, var(--line));
  box-shadow: var(--shadow), 0 0 0 4px var(--ring);
}

.searchbar input[type="search"] {
  flex: 1 1 18rem;
  min-width: 0;
}

.searchbar input[type="search"],
.searchbar select {
  border-color: transparent;
  background: transparent;
  box-shadow: none;
}

.searchbar select {
  border-radius: var(--r-pill);
  padding-inline: 0.6rem;
  color: var(--muted);
}

.searchbar select:hover:not(:disabled) {
  border-color: transparent;
  background: var(--surface-2);
}

.searchbar input[type="search"]:focus,
.searchbar select:focus {
  border-color: transparent;
  box-shadow: none;
}

.searchbar .btn {
  border-radius: var(--r-pill);
  padding-inline: 1.25rem;
}

/* =====================================================================================
   SURFACES
   ===================================================================================== */

.panel {
  padding: 1.4rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm), inset 0 1px 0 var(--edge);
  margin-bottom: 1.25rem;
}

.panel h2,
.panel h3 {
  margin: 0 0 0.6rem;
  font-size: 1.05rem;
}

/* Inside a card the section marker fights the card's own edge. */
.panel h2::before {
  display: none;
}

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

/* ---- Pack cards ---------------------------------------------------------------------- */

.pack-grid {
  display: grid;
  gap: 1.1rem;
  grid-template-columns: repeat(auto-fill, minmax(min(18.5rem, 100%), 1fr));
}

.pack-card {
  display: block;
  position: relative;
  padding: 0;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  color: inherit;
  text-decoration: none;
  transition: transform var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease),
    border-color var(--dur-2) var(--ease);
}

.pack-card::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 2px;
  z-index: 1;
  background: linear-gradient(90deg, var(--accent), var(--accent-hot) 55%, transparent);
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease);
}

.pack-card:hover {
  border-color: var(--accent-line);
  box-shadow: var(--shadow-lg);
}

/*
  The whole card is the link, so it needs no arrow badge in its corner: the accent bar above
  plus the lifted border already say "this goes somewhere", and the badge sat on top of the
  preview strip it was covering.
*/
.pack-card:hover::before {
  opacity: 1;
}

/*
  The preview strip is a canvas, not a row of loose images: a sunken band that the assets sit
  on, edge to edge, so a pack of pale line assets still reads as a pack of something.
*/
.pack-preview {
  /*
    Local to the strip rather than global: the two numbers only mean anything together, and
    the second scale below is the only other place either of them is read.
  */
  --thumb: 4rem;
  --lap: 1.4rem;
  display: flex;
  /* No gap: the assets overlap into a stack (see the negative margin below). */
  gap: 0;
  align-items: center;
  min-height: 6.6rem;
  padding: 1.15rem;
  background: linear-gradient(
    180deg,
    var(--sunken),
    color-mix(in oklab, var(--sunken) 55%, var(--surface))
  );
  border-bottom: 1px solid var(--line);
}

/*
  The three values dock.js writes per asset. They are declared here, not only there, for two
  reasons: the strip has to look right for the frame before the first measurement, and a
  reader without JavaScript gets an even row instead of an invisible one.
*/
.pack-preview img {
  --s: 1;
  --o: 1;
  --z: 0;
  flex: none;
  position: relative;
  z-index: var(--z);
  width: var(--thumb);
  height: var(--thumb);
  /*
    The frame around each asset, kept but slimmed: at 6px the mat was wider than the strokes
    of the artwork inside it, which reads as a plump chip rather than a framed icon. The tile
    keeps its size - only the artwork inside it grows.
  */
  padding: 3px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  opacity: var(--o);
  transform: scale(var(--s));
  /*
    Short and linear-ish: the pointer already supplies the easing. A spring here would lag a
    frame behind the mouse and read as a rubber band, not as weight.
  */
  transition:
    transform var(--dur-2) var(--ease-out),
    opacity var(--dur-2) var(--ease-out);
}

/*
  Reduced motion keeps the weighting - it is layout, not animation - and drops the travel
  between states.
*/
@media (prefers-reduced-motion: reduce) {
  .pack-preview img {
    transition: none;
  }
}

/*
  The stack. Which asset laps over which is not document order but --z, ranked by how close
  each one is to the focus - the largest has to be the whole one. flex: none above keeps them
  from squashing into ovals instead of clipping when the column gets very narrow.
*/
.pack-preview img + img {
  margin-left: calc(var(--lap) * -1);
}

/*
  The stack is the one row here that cannot reflow: six assets are six assets. At full size
  they need 6 x 4rem less 5 x 1.4rem = 17rem, which is wider than a phone column once the
  card's gutters come off - so the scale drops instead of the last asset being clipped.
*/
@media (max-width: 30rem) {
  .pack-preview {
    --thumb: 3rem;
    --lap: 1.2rem;
    min-height: 5.4rem;
    padding: 0.9rem;
  }
}


.pack-card > h3 {
  margin: 1.1rem 1.15rem 0.4rem;
}

.pack-card > p {
  margin: 0 1.15rem 0.5rem;
  color: var(--muted);
  font-size: 0.92rem;
  /*
    Three lines, then an ellipsis. A grid whose cards are all different heights because one
    creator wrote an essay is not a grid - the clamp stays, it just sits one line lower now
    that the card is taller.
  */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.pack-card > .meta {
  display: block;
  margin: 0.7rem 1.15rem 1.1rem;
  font-family: var(--font-mono);
  font-size: 0.76rem;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- Asset cards ---------------------------------------------------------------------- */

.grid {
  display: grid;
  gap: 0.85rem;
  grid-template-columns: repeat(auto-fill, minmax(min(9.5rem, 100%), 1fr));
}

.card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding: 0.9rem 0.7rem 1rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  color: inherit;
  text-decoration: none;
  text-align: center;
  transition: transform var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease),
    border-color var(--dur-2) var(--ease);
}

.card:hover {
  border-color: var(--accent-line);
  box-shadow: var(--shadow-lg);
}

/*
  The image carries its own canvas. A transparent asset on a white card is a drawing floating
  in nothing; on a tinted tile it is an object.
*/
.card img {
  width: 100%;
  max-width: 4.5rem;
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: contain;
  padding: 0.55rem;
  background: var(--sunken);
  border-radius: var(--r-md);
  transition: transform var(--dur-2) var(--ease-spring),
    background-color var(--dur-2) var(--ease);
}

.card:hover img {
  transform: scale(1.06);
  background: var(--accent-soft);
}

.card-name {
  font-size: 0.9rem;
  font-weight: 560;
  line-height: 1.3;
}

/* An identifier, in mono - but never uppercased: someone is going to type this back. */
.card-slug,
.upload-id,
.icon-tile code,
.row-main code {
  font-family: var(--font-mono);
  font-size: 0.74rem;
  color: var(--muted);
}

.card-slug {
  background: none;
  border: 0;
  padding: 0;
  overflow-wrap: anywhere;
}

/* ---- Chips, fields, badges ----------------------------------------------------------- */

.chip {
  display: inline-block;
  font-size: 0.8rem;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  padding: 0.14rem 0.65rem;
  margin: 0 0.25rem 0.3rem 0;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease);
}

.chip:hover {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: color-mix(in oklab, var(--accent) 80%, var(--fg));
}

/*
  The metadata table reads as one object rather than a stack of underlined rows: a card with
  hairline dividers, the label column in the mono microvoice.
*/
.fields {
  display: grid;
  gap: 0;
  margin: 1.5rem 0;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.field {
  display: grid;
  grid-template-columns: 11rem 1fr;
  gap: 0.9rem;
  padding: 0.8rem 1.15rem;
  border-bottom: 1px solid var(--line);
  transition: background-color var(--dur-1) var(--ease);
}

.field:last-child {
  border-bottom: 0;
}

.field:hover {
  background: color-mix(in oklab, var(--surface-2) 60%, transparent);
}

.field dt {
  color: var(--muted);
  padding-top: 0.2rem;
}

.field dd {
  margin: 0;
  overflow-wrap: anywhere;
}

@media (max-width: 40rem) {
  .field {
    grid-template-columns: 1fr;
    gap: 0.2rem;
  }
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--surface-2);
  color: var(--muted);
  font-weight: 560;
  white-space: nowrap;
}

/*
  A status is a dot plus a word. The dot does the colour work, which lets the word stay
  quiet - and lets a status pill sit beside a cyan call to action without the two
  competing for the same meaning.
*/
.badge::before {
  content: "";
  flex: none;
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 50%;
  background: currentcolor;
}

.badge[data-kind="primary"] {
  color: color-mix(in oklab, var(--accent) 85%, var(--fg));
  border-color: var(--accent-line);
  background: var(--accent-soft);
}

.badge[data-kind="completion"] {
  color: var(--muted);
}

.badge-draft {
  color: var(--muted);
}

.badge-pending,
.badge-ai_review,
.badge-ai_running {
  color: var(--warn);
  background: color-mix(in oklab, var(--warn) 13%, var(--surface));
  border-color: color-mix(in oklab, var(--warn) 32%, var(--line));
}

.badge-published {
  color: var(--ok);
  background: color-mix(in oklab, var(--ok) 12%, var(--surface));
  border-color: color-mix(in oklab, var(--ok) 32%, var(--line));
}

.badge-amended {
  color: var(--info);
  background: color-mix(in oklab, var(--info) 12%, var(--surface));
  border-color: color-mix(in oklab, var(--info) 32%, var(--line));
}

.badge-rejected,
.badge-ai_failed {
  color: var(--danger);
  background: color-mix(in oklab, var(--danger) 12%, var(--surface));
  border-color: color-mix(in oklab, var(--danger) 32%, var(--line));
}

/* A job in flight says so by breathing. */
.badge-ai_running::before {
  animation: soft-pulse 1.6s var(--ease) infinite;
}

/* ---- Rows ---------------------------------------------------------------------------- */

.rows {
  list-style: none;
  margin: 0 0 1.25rem;
  padding: 0;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.row {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.85rem;
  padding: 0.9rem 1.15rem;
  border-bottom: 1px solid var(--line);
  transition: background-color var(--dur-1) var(--ease);
}

/* The hover marker grows out of the middle of the row's left edge. */
.row::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.35rem;
  bottom: 0.35rem;
  width: 2px;
  border-radius: 0 var(--r-pill) var(--r-pill) 0;
  background: linear-gradient(180deg, var(--accent), var(--accent-hot));
  transform: scaleY(0);
  transition: transform var(--dur-2) var(--ease-out);
}

.row:hover {
  background: color-mix(in oklab, var(--surface-2) 70%, transparent);
}

.row:hover::before {
  transform: scaleY(1);
}

.row:last-child {
  border-bottom: 0;
}

.row img {
  width: 40px;
  height: 40px;
  padding: 0.3rem;
  background: var(--sunken);
  border-radius: var(--r-sm);
  flex: none;
}

.row-main {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  flex: 1 1 16rem;
  min-width: 0;
  text-decoration: none;
  color: inherit;
}

a.row-main:hover strong {
  color: var(--accent);
}

.row-meta {
  font-size: 0.85rem;
  color: var(--muted);
  overflow-wrap: anywhere;
}

/* ---- Notices and empty states -------------------------------------------------------- */

.notice {
  padding: 0.85rem 1.05rem;
  border: 1px solid var(--line);
  border-left: 3px solid var(--line-strong);
  border-radius: var(--r-md);
  background: var(--surface);
  margin: 0 0 1.1rem;
  font-size: 0.94rem;
  box-shadow: var(--shadow-sm);
  animation: enter-rise var(--dur-2) var(--ease-out) backwards;
}

.notice-ok {
  color: color-mix(in oklab, var(--ok) 88%, var(--fg));
  border-color: color-mix(in oklab, var(--ok) 30%, var(--line));
  border-left-color: var(--ok);
  background: color-mix(in oklab, var(--ok) 8%, var(--surface));
}

.notice-error {
  color: color-mix(in oklab, var(--danger) 88%, var(--fg));
  border-color: color-mix(in oklab, var(--danger) 30%, var(--line));
  border-left-color: var(--danger);
  background: color-mix(in oklab, var(--danger) 8%, var(--surface));
}

.empty {
  display: grid;
  gap: 0.5rem;
  justify-items: center;
  padding: 3rem 2rem;
  text-align: center;
  color: var(--muted);
  font-size: 0.94rem;
  background: color-mix(in oklab, var(--surface) 45%, transparent);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-lg);
}

.empty strong {
  color: var(--fg);
  font-size: 1.08rem;
  font-weight: 620;
  letter-spacing: -0.015em;
}

.grid:has(~ :not([hidden])),
.empty:has(~ :not([hidden])) {
  margin-bottom: 1.25rem;
}

/* ---- The two link lists on the home page --------------------------------------------- */

/*
  grid-auto-rows: 1fr is what makes the cards one size. With auto rows every row is as tall
  as its own tallest card, so a single wrapping description (or the one long /api/recommend
  path) made that row deeper than the rest and the list read as a ragged pile. Flexible rows
  in a grid of indefinite height all resolve to the tallest row's content, so every card in
  the section gets the same height - and a card that has room to spare keeps it at the bottom
  rather than centring its text.
*/
ul.tools,
ul.endpoints {
  list-style: none;
  padding: 0;
  margin: 0 0 1rem;
  display: grid;
  gap: 0.6rem;
  grid-template-columns: repeat(auto-fill, minmax(min(21rem, 100%), 1fr));
  grid-auto-rows: 1fr;
}

/*
  The endpoint list is a different shape from the tool list: one mono chip per card, no
  description underneath. At 21rem three columns fit, and the longest route
  (/api/recommend?project=...&count={n}) wrapped - which, with grid-auto-rows: 1fr, made
  *every* card in the section as tall as that one wrapped card. A wider minimum (two columns
  on the 74rem measure) lets each route stay on one line, and rows sized to their own content
  keep the section one line tall. Cards in the same row still match - that is the default
  stretch, and it is all this list needs.

  min(32rem, 100%) rather than a bare 32rem: a track minimum is a *minimum*, so on a phone
  (~24rem of viewport) the column stayed 32rem wide and the cards ran off the page - the wider
  the minimum, the worse the overflow, which is why this list broke and the others did not.
  min() caps the minimum at the container, so the track falls back to one full-width column.
*/
ul.endpoints {
  grid-template-columns: repeat(auto-fill, minmax(min(32rem, 100%), 1fr));
  grid-auto-rows: auto;
}

/*
  The chip sits on its own line, the description below it. Inline, a description that wrapped
  ran under the chip and started at a different x than the one above it - the second reason
  the list looked uneven. As a column the chip is always flush left and the text always
  starts on the same line, whether it wraps or not.
*/
ul.tools li,
ul.endpoints li {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.4rem;
  padding: 0.75rem 0.95rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-sm);
  font-size: 0.92rem;
  color: var(--muted);
  transition: transform var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease),
    box-shadow var(--dur-1) var(--ease);
}

ul.tools li:hover,
ul.endpoints li:hover {
  border-color: var(--accent-line);
  box-shadow: var(--shadow);
}

/*
  A route has no spaces after the verb, so it is one unbreakable word to the line breaker:
  even in a full-width column it would poke out of its own card on a narrow screen. anywhere
  lets it break; max-width keeps it inside the card's padding rather than the card's border box.
*/
/* The wrap itself is on the base code rule - only the colour and the cap belong here. */
ul.tools li code,
ul.endpoints li code {
  color: var(--fg);
  max-width: 100%;
}

/* =====================================================================================
   HOME
   ===================================================================================== */

.hero {
  display: grid;
  gap: 0 2.5rem;
  padding: 1.75rem 0 0.5rem;
}

.hero h1 {
  max-width: 24ch;
  margin-bottom: 0.9rem;
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  justify-self: start;
  margin: 0 0 1.1rem;
  padding: 0.35rem 0.8rem 0.35rem 0.6rem;
  font-weight: 560;
  color: color-mix(in oklab, var(--accent) 82%, var(--fg));
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-pill);
}

.eyebrow::before {
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 18%, transparent);
  animation: soft-pulse 2.4s var(--ease) infinite;
}

/*
  The agent instructions, as an inverted card in both colour schemes.

  This paragraph is the one part of the page written for a reader with a fetch tool, and it
  is the only place on the site where a URL is an instruction rather than a link. Giving it
  the look of a terminal states that in one glance, and pins the identity of the whole
  product: this catalog is driven by machines first.
*/
.hero .note {
  align-self: start;
  /* Stacked under the stats, it needs air of its own; beside them it does not. */
  margin-top: 1.1rem;
  padding: 1.35rem 1.4rem;
  color: var(--ink-fg);
  background: linear-gradient(
    160deg,
    color-mix(in oklab, var(--ink) 92%, var(--accent)),
    var(--ink) 55%
  );
  border: 1px solid var(--ink-line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  font-size: 0.88rem;
  line-height: 1.75;
}

.hero .note::before {
  content: "for agents";
  display: block;
  margin-bottom: 0.7rem;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-accent);
}

.hero .note code {
  color: oklch(0.97 0.01 var(--h-tint));
  background: oklch(1 0 0 / 9%);
  border-color: oklch(1 0 0 / 14%);
}

.hero .note a {
  color: var(--ink-accent);
}

.hero .note a:hover {
  color: oklch(0.9 0.076 var(--h));
}

/*
  Wide screens read the hero as two things at once: the claim, and the machine-readable way
  in. Below this width they stack in source order, which is also the order they matter in
  to a person.
*/
@media (min-width: 70rem) {
  .hero {
    grid-template-columns: minmax(0, 1.08fr) minmax(21rem, 0.92fr);
  }

  .hero > .eyebrow,
  .hero > h1,
  .hero > .lead,
  .hero > .stat-row {
    grid-column: 1;
  }

  .hero > .note {
    grid-column: 2;
    grid-row: 1 / span 4;
    margin-top: 0;
  }
}

/*
  min(8.5rem, 100%) rather than a bare 8.5rem, even though three 8.5rem tracks fit any phone
  once they are allowed to reflow. This row is a grid item of .hero, and a grid track sized
  auto may not go below its items' min-content contribution - for which the browser keeps the
  auto-repeat at its full count instead of collapsing it to one. The row therefore asked for
  3 x 8.5rem + gaps (about 27rem), the hero column grew to that, and the whole document came
  out roughly 20% wider than a 390px viewport: the lead was cut off and the sticky header, which
  measures itself against the viewport, ended in the middle of the page. Measured in Chromium at
  390px, before and after. The cap makes the contribution collapse, so the row reflows instead.
*/
.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(8.5rem, 100%), 1fr));
  gap: 0.7rem;
  margin: 1.9rem 0 0.5rem;
  max-width: 44rem;
}

.stat {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.95rem 1.1rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-sm), inset 0 1px 0 var(--edge);
  overflow: hidden;
  transition: transform var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease),
    border-color var(--dur-2) var(--ease);
}

/* A hairline of brand colour along the top edge, brightening on hover. */
.stat::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 2px;
  background: linear-gradient(90deg, var(--accent), transparent);
  opacity: 0.35;
  transition: opacity var(--dur-2) var(--ease);
}

.stat:hover {
  border-color: var(--accent-line);
  box-shadow: var(--shadow);
}

.stat:hover::before {
  opacity: 1;
}

.stat strong {
  font-size: 1.8rem;
  font-weight: 660;
  line-height: 1.1;
  letter-spacing: -0.035em;
  font-variant-numeric: tabular-nums;
}

.stat span {
  color: var(--muted);
}

/* =====================================================================================
   PACK AND ASSET PAGES
   ===================================================================================== */

.asset-detail {
  display: flex;
  flex-wrap: wrap;
  gap: 2.5rem;
  align-items: flex-start;
}

/*
  The checkerboard is not decoration: these files are mostly transparent, and on a flat
  white plate there is no way to tell whether the white in the drawing is the drawing or
  the page behind it.
*/
.asset-preview {
  position: relative;
  display: grid;
  place-items: center;
  padding: 2.5rem;
  background:
    conic-gradient(
        from 90deg at 50% 50%,
        var(--surface-2) 0 25%,
        var(--surface) 0 50%,
        var(--surface-2) 0 75%,
        var(--surface) 0
      )
      0 0 / 18px 18px;
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg);
}

/* A soft brand glow under the plate, so the artwork is the lit thing on the page. */
.asset-preview::after {
  content: "";
  position: absolute;
  inset: 15% 10% -8%;
  z-index: -1;
  border-radius: 50%;
  background: color-mix(in oklab, var(--accent) 28%, transparent);
  filter: blur(34px);
  opacity: 0.6;
}

.asset-preview img {
  width: 128px;
  height: 128px;
}

.asset-info {
  flex: 1 1 22rem;
  min-width: 0;
}

/* =====================================================================================
   ACCOUNT, UPLOADS, THE EDITOR
   ===================================================================================== */

.account-head {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  margin-bottom: 1.9rem;
}

.account-head .avatar {
  width: 3.6rem;
  height: 3.6rem;
  font-size: 1.45rem;
}

.account-head h1 {
  margin: 0;
}

.sub {
  margin: 0.25rem 0 0;
  color: var(--muted);
}

/*
  Tabs are a row of plain-voice labels on one hairline, not a boxed control. Two rules
  that look like taste but are not:

  - The labels keep the SAME weight in every state. A bolder active label is a wider
    active label, and the whole row would shuffle sideways on every tab change. The
    active state is said twice by other means instead - ink-dark text plus the accent
    bar - so it survives colourblindness without touching the metrics.
  - The bar sits at bottom: -1px, ON the row's hairline rather than above it. One pixel
    higher and the hairline shows through underneath, which reads as a rendering bug.
*/
.tabs {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  flex-wrap: wrap;
  margin: 0 0 1.9rem;
  border-bottom: 1px solid var(--line);
}

.tabs a {
  position: relative;
  color: var(--muted);
  text-decoration: none;
  font-size: 0.88rem;
  font-weight: 560;
  padding: 0.5rem 0.95rem 0.66rem;
  border-radius: var(--r-sm) var(--r-sm) 0 0;
  transition: color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease);
}

.tabs a::after {
  content: "";
  position: absolute;
  left: 0.95rem;
  right: 0.95rem;
  bottom: -1px;
  height: 2px;
  border-radius: var(--r-pill);
  background: var(--accent);
  transform: scaleX(0);
  transition: transform var(--dur-2) var(--ease-out);
}

.tabs a:hover {
  color: var(--fg);
  background: color-mix(in oklab, var(--surface-2) 70%, transparent);
}

.tabs a[aria-current="page"] {
  color: var(--fg);
}

.tabs a[aria-current="page"]::after {
  transform: scaleX(1);
}

.tabs .spacer {
  margin-left: auto;
}

/* The row's hairline is the baseline; anything that is not a tab keeps clear of it. */
.tabs .btn {
  font-size: 0.8rem;
  padding: 0.4rem 0.8rem;
  margin-bottom: 0.5rem;
  /* Opts out of the shared control height: it sits beside tab labels, not beside a field. */
  min-height: 0;
}

/* ---- Sign in ------------------------------------------------------------------------- */

.form-card {
  position: relative;
  max-width: 26rem;
  margin: 3.5rem auto;
  padding: 2.25rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}

.form-card::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 3px;
  background: linear-gradient(90deg, var(--accent), var(--accent-hot), transparent);
}

.form-card h1 {
  margin-top: 0;
  font-size: 1.9rem;
}

/*
  Every paragraph in this sheet carries its space below it, never above - so a block that
  is followed by prose has to supply its own. A form does not (the UA margin is zero),
  which is why the hint underneath sat flush against the submit button.
*/
.form-card form {
  margin-bottom: 1.4rem;
}

.form-card button[type="submit"] {
  width: 100%;
  margin-top: 0.5rem;
  padding-block: 0.7rem;
}

/* ---- The upload list ----------------------------------------------------------------- */

.upload-list {
  display: grid;
  gap: 0.65rem;
  margin-bottom: 1.25rem;
}

.upload-row {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto auto;
  grid-template-areas: "title status meta" "id status meta";
  gap: 0.15rem 0.9rem;
  align-items: center;
  padding: 1rem 1.2rem;
  color: inherit;
  text-decoration: none;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  transition: transform var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease),
    box-shadow var(--dur-1) var(--ease);
}

/*
  A row in a stack moves sideways, not up: it has neighbours above and below it, and lifting
  it would push into them.
*/
.upload-row::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  background: linear-gradient(180deg, var(--accent), var(--accent-hot));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-2) var(--ease-out);
}

.upload-row:hover {
  transform: translateX(3px);
  border-color: var(--accent-line);
  box-shadow: var(--shadow);
}

.upload-row:hover::before {
  transform: scaleX(1);
}

.upload-row .badge {
  grid-area: status;
}

.upload-title {
  grid-area: title;
  font-weight: 580;
}

.upload-id {
  grid-area: id;
}

.upload-meta {
  grid-area: meta;
  font-size: 0.84rem;
  color: var(--muted);
}

.upload-head {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  margin-bottom: 1.1rem;
}

.upload-head h1 {
  margin: 0 0 0.4rem;
}

.upload-head .sub {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0;
}

/*
  Breadcrumbs, in two shapes that share one voice.

  They were mono at 0.78rem. Mono is the identifier voice here - slugs, pack ids, routes:
  values somebody types back. A crumb is a navigation word ("Browse", "My uploads"), so it
  belongs to the sans voice, same reasoning that took the mono off the tabs.

  Shape one (default) is a path: links joined by a .sep, ending in .here, the page you are
  on. Shape two (.back) is not a path - the editor offers up to two *parallel* ways out (your
  uploads, the review queue), and a separator between them would claim a hierarchy that does
  not exist. Those are drawn as chips instead: two words floating side by side with nothing
  between them read as broken, a chip reads as a way out.

  No display is set on .crumbs a on purpose: the review crumb is toggled with [hidden], and
  a display here would out-specify the UA rule that makes hidden mean hidden.
*/
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem 0.5rem;
  margin: 0 0 1.1rem;
  font-size: 0.84rem;
  color: var(--muted);
}

.crumbs a {
  color: inherit;
  text-decoration: none;
  transition: color var(--dur-1) var(--ease);
}

.crumbs a:hover {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
}

/* Lighter than the words it divides: it is punctuation, not a step of its own. */
.crumbs .sep {
  color: var(--line-strong);
  user-select: none;
}

/* The page you are already on: ink, so the trail ends somewhere instead of trailing off. */
.crumbs .here {
  color: var(--fg);
}

.crumbs.back a {
  padding: 0.22rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--surface);
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
    background var(--dur-1) var(--ease);
}

.crumbs.back a:hover {
  color: var(--accent);
  border-color: var(--accent-line);
  background: var(--accent-soft);
  /* The chip is the affordance; an underline inside it would be the second one. */
  text-decoration: none;
}

/* ---- The dropzone -------------------------------------------------------------------- */

.dropzone {
  display: grid;
  gap: 0.6rem;
  justify-items: center;
  padding: 2.25rem 1.5rem;
  text-align: center;
  background: color-mix(in oklab, var(--sunken) 65%, transparent);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-lg);
  transition: border-color var(--dur-2) var(--ease), background-color var(--dur-2) var(--ease),
    transform var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease);
}

.dropzone::before {
  content: "+";
  display: grid;
  place-items: center;
  width: 2.6rem;
  height: 2.6rem;
  margin-bottom: 0.2rem;
  border-radius: 50%;
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  color: var(--accent);
  font-size: 1.5rem;
  font-weight: 500;
  line-height: 1;
  transition: transform var(--dur-2) var(--ease-spring);
}

.dropzone:hover {
  border-color: var(--accent-line);
}

.dropzone:hover::before {
  transform: scale(1.08);
}

.dropzone.dragging {
  border-color: var(--accent);
  border-style: solid;
  background: var(--accent-soft);
  transform: scale(1.006);
  box-shadow: var(--glow);
}

.dropzone.dragging::before {
  transform: scale(1.16);
}

.dropzone .hint {
  margin: 0;
}

/*
  The file input is the one native widget left on the page, and it was the only control that
  did not belong to the design. The field itself becomes invisible furniture and its own
  button gets the .btn treatment, so "use the button" in the hint next to it points at
  something that looks like every other button on the site.
*/
.dropzone input[type="file"] {
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 0.88rem;
  max-width: 100%;
}

.dropzone input[type="file"]::file-selector-button {
  margin-right: 0.7rem;
  padding: 0.5rem 0.95rem;
  font: inherit;
  font-size: 0.9rem;
  font-weight: 560;
  color: var(--fg);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
    box-shadow var(--dur-1) var(--ease);
}

.dropzone input[type="file"]::file-selector-button:hover {
  background: var(--surface-2);
  border-color: color-mix(in oklab, var(--accent) 30%, var(--line-strong));
  box-shadow: var(--shadow);
}

.intake-results {
  display: grid;
  gap: 0.35rem;
  margin: 1.1rem 0 0;
  padding: 0;
  list-style: none;
  font-size: 0.88rem;
}

.intake-result {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: baseline;
  animation: enter-fade var(--dur-2) var(--ease) backwards;
}

.intake-result::before {
  font-family: var(--font-mono);
  font-size: 0.8rem;
}

/* Escapes, not the glyphs themselves: this file is held to ASCII. */
.intake-ok {
  color: var(--ok);
}

.intake-ok::before {
  content: "\2713";
}

.intake-bad {
  color: var(--danger);
}

.intake-bad::before {
  content: "\00D7";
}

/* ---- The asset workbench -------------------------------------------------------------- */

.icon-workbench {
  display: grid;
  grid-template-columns: 34rem 1fr;
  gap: 1.35rem;
  align-items: start;
  margin-bottom: 1.25rem;
}

/*
  The picker is a sunken tray that scrolls on its own and stays put while the fields to its
  right are edited. Three columns is a size decision, not a count one: the drawing has to be
  big enough to recognise, and a column count tied to the width shrank it again every time
  the layout changed.
*/
.icon-picker {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.5rem;
  position: sticky;
  top: 5rem;
  max-height: calc(100vh - 6rem);
  overflow-y: auto;
  padding: 0.65rem;
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm),
    inset 0 1px 3px color-mix(in oklab, var(--shade) 8%, transparent);
}

.icon-tile {
  display: grid;
  justify-items: center;
  gap: 0.35rem;
  padding: 0.45rem 0.3rem 0.55rem;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  cursor: pointer;
  font: inherit;
  color: var(--muted);
  min-width: 0;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}

.icon-tile:hover {
  background: var(--surface);
  border-color: var(--line);
  color: var(--fg);
  box-shadow: var(--shadow-sm);
}

.icon-tile img {
  width: 100%;
  aspect-ratio: 1 / 1;
  height: auto;
  object-fit: contain;
  padding: 0.3rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  transition: transform var(--dur-2) var(--ease-spring);
}

.icon-tile:hover img {
  transform: scale(1.04);
}

.icon-tile code {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  background: none;
  border: 0;
  padding: 0;
}

.icon-tile.is-selected {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: color-mix(in oklab, var(--accent) 80%, var(--fg));
  box-shadow: 0 0 0 3px var(--ring);
}

.icon-tile.is-selected img {
  border-color: var(--accent-line);
}

/*
  The tile is the copy of an asset that stays visible after the selection moves on, so it has
  to carry the one thing its card would otherwise say alone: this asset still owes the server
  an edit.
*/
.icon-tile.is-dirty code::after {
  content: " *";
  color: var(--danger);
  font-weight: 700;
}

@media (max-width: 76rem) {
  .icon-workbench {
    grid-template-columns: 1fr;
  }

  .icon-picker {
    position: static;
    max-height: 16rem;
    grid-template-columns: repeat(auto-fill, minmax(min(6.5rem, 100%), 1fr));
  }
}

.icon-list {
  display: grid;
  gap: 1rem;
  min-width: 0;
}

.icon-card {
  display: grid;
  gap: 1.1rem;
  margin-bottom: 0;
  padding: 1.25rem;
}

.icon-card > * {
  margin: 0;
}

.icon-head {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--line);
}

.icon-head .js-delete {
  margin-left: auto;
  flex: none;
}

.icon-head img {
  flex: none;
  padding: 0.3rem;
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}

.icon-ident {
  display: grid;
  gap: 0.15rem;
  min-width: 0;
  overflow-wrap: anywhere;
}

.icon-facts {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--muted);
}

.icon-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: center;
}

/*
  One column for the asset fields. They are single-line inputs with long labels: at 40rem wide
  an input is no more useful than at 20, and the second column only pushed the drawing they
  describe further away.
*/
.form-grid.icon-fields {
  grid-template-columns: 1fr;
}

.icon-fields textarea {
  min-height: 3.5rem;
}

/*
  Mono, because most of what it says is data ("Saved 14:22:07") - but deliberately not
  uppercased like the other labels: its error branch carries the server's whole sentence,
  and a refusal shouted in capitals is harder to read, not more urgent. The error state
  goes back to the body face for the same reason.
*/
.save-status {
  align-self: center;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--muted);
}

.save-status.is-working {
  animation: soft-pulse 1.6s var(--ease) infinite;
}

.save-status.is-error {
  font-family: var(--font-sans);
  font-size: 0.86rem;
  color: var(--danger);
  font-weight: 640;
}

/* ---- Review history ------------------------------------------------------------------ */

/*
  A timeline, not a list: the rail and the dots say "these happened in order" before a single
  date is read.
*/
.history {
  position: relative;
  display: grid;
  gap: 0.2rem;
  margin: 0;
  padding: 0 0 0 1.5rem;
  list-style: none;
}

.history::before {
  content: "";
  position: absolute;
  left: 0.28rem;
  top: 1.1rem;
  bottom: 1.1rem;
  width: 1px;
  background: linear-gradient(180deg, var(--accent-line), var(--line));
}

.history li {
  position: relative;
  display: grid;
  gap: 0.2rem;
  padding: 0.75rem 0;
}

.history li::before {
  content: "";
  position: absolute;
  left: -1.5rem;
  top: 1.1rem;
  width: 0.65rem;
  height: 0.65rem;
  border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--line-strong);
}

/* The newest entry is the one that matters; it gets the brand colour. */
.history li:first-child::before {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 15%, transparent);
}

/* =====================================================================================
   UTILITY
   ===================================================================================== */

/*
  Set by boot.js when a deep link is served the home markup, and
  lifted by router.js once the right fragment is in. Visibility rather than display: the
  layout is already correct, only the wrong content must not be seen.
*/
.routing main {
  visibility: hidden;
}
