/**
 * colors.css
 * -----------------------------------------------------------------------
 * Styles for /colors only — the Color Library. Loaded by that one page and
 * by nothing else, the same arrangement palettes.css has.
 *
 * Reuses only the site-wide primitives from styles.css (--action / --paper /
 * --card / --sans / --grid-line / --text / --text-muted, .wrap,
 * .sr-only, .empty-state, .toast). It defines no new site-level token and
 * overrides nothing in :root, so loading it cannot change any other page.
 *
 * RELATIONSHIP TO palettes.css
 *
 * Related, not shared. The two pages are siblings and are meant to look it,
 * so the radius, the easing curve and the grid breakpoints are the same
 * values — but they are declared here rather than imported, because a
 * palette card and a colour card are different components and coupling their
 * stylesheets would mean every future change to one had to be checked
 * against the other. The duplication is four values; the coupling would be
 * permanent.
 *
 * Local decisions worth knowing before editing:
 *   - --color-radius is scoped to this page, exactly as --palette-radius is
 *     scoped to /palettes. The global --radius (3px) is untouched.
 *   - The HEX label's colour, the copied-state veil and the containment ring
 *     are all inline custom properties emitted per card by
 *     src/client/colors.js from a WCAG contrast calculation. Nothing in this
 *     file assigns a colour per swatch, and nothing should: 300 hand-written
 *     rules is the failure mode this replaces.
 *   - The plate is a fixed ASPECT RATIO, not a fixed height, so the colour
 *     block keeps its proportion at every column count instead of going
 *     letterbox on mobile.
 * -----------------------------------------------------------------------
 */

.colors-page {
  --color-radius: 10px;
  --color-ease: cubic-bezier(0.2, 0.6, 0.3, 1);
}

/* .guides-main ships 44px of padding-top for the guide grid, which stacks
   with this page's own hero padding. Same 28px trim /palettes makes. */
.colors-page .guides-main {
  padding-top: 28px;
}

/* ---------- header band (hero + filters) ---------- */
/* One grouped band closed by a single hairline, so title, description and
   controls read as one unit attached to the grid below — the same shape
   .palettes-head gives /palettes. */
.colors-head {
  border-bottom: 1px solid var(--grid-line);
  margin-bottom: 26px;
}

/* Page-intro scale from styles.css (--intro-*), matching .palettes-hero. */
.colors-hero {
  padding: var(--intro-space-top) 0 clamp(32px, 3vw + 16px, 64px);
}
.colors-hero h1 {
  font-size: var(--intro-title-hero);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--text);
  overflow-wrap: break-word;
  text-wrap: balance;
}
.colors-hero p {
  max-width: 800px;
  margin-top: var(--intro-title-gap);
  color: var(--text-muted);
  font-size: var(--intro-dek);
  line-height: 1.45;
}

/* ---------- category filters ---------- */
/* Light-gray pills, a colour dot on the left, the name on the right.
   Wraps rather than scrolls: on a phone the whole vocabulary stays visible,
   which a horizontal scroller hides. */
.colors-filters {
  --color-chip-bg: #f1f2f4;
  --color-chip-bg-hover: #e5e7eb;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding-bottom: 24px;
}

/* From 641px up (the site's own 640px phone break) the twelve categories sit
   as three rows of four — warm, cool, neutral, which is CATEGORY_ORDER read
   four at a time — while every pill keeps its own content width.
   Still a wrapping flex row; the rows come from forced line breaks, counted
   by position rather than by name, so they follow the data order:
     - "All" (first child, not a colour) takes its own row above the three:
       its 100% end margin leaves nothing else room on that line. A margin,
       not a border box, so it adds no scrollable overflow.
     - the container's ::before / ::after are zero-height, full-width flex
       items slotted in by `order` after the 4th and 8th colour. Visual
       order still equals DOM order, so Tab order is unchanged.
   Row spacing is the pills' own bottom margin, not row-gap: row-gap would
   also open a gap around each empty break line and double the spacing. */
@media (min-width: 641px) {
  .colors-filters {
    gap: 0 12px;
    padding-bottom: 12px;
  }
  .colors-filters > .colors-filter {
    margin-bottom: 12px;
  }
  .colors-filters > .colors-filter:first-child {
    margin-inline-end: 100%;
  }
  .colors-filters::before,
  .colors-filters::after {
    content: "";
    flex-basis: 100%;
    height: 0;
  }
  .colors-filters::before {
    order: 1;
  }
  .colors-filters > .colors-filter:nth-child(n + 6) {
    order: 2;
  }
  .colors-filters::after {
    order: 3;
  }
  .colors-filters > .colors-filter:nth-child(n + 10) {
    order: 4;
  }
}

.colors-filter {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 42px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--color-chip-bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: 14px;
  /* Held at one weight in every state on purpose: bumping the weight on the
     active chip would change its width and reflow the row on each switch. */
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.005em;
  padding: 9px 18px 9px 14px;
  cursor: pointer;
  transition:
    color 0.16s var(--color-ease),
    background-color 0.16s var(--color-ease),
    border-color 0.16s var(--color-ease);
}
/* "All" carries no dot, so it gets even padding on both sides. */
.colors-filter:not(:has(.colors-filter__dot)) {
  padding-left: 18px;
}
.colors-filter:hover {
  background: var(--color-chip-bg-hover);
}

/* The selected chip is marked by FILL and TEXT WEIGHT CONTRAST, not by hue:
   solid --action background, white label. A viewer who cannot distinguish
   the chip colours still sees which one is inverted — and aria-pressed
   carries the same fact to assistive tech, so the state is never
   colour-only. Declared after :hover so an active chip stays inverted
   while hovered. */
.colors-filter[aria-pressed="true"],
.colors-filter[aria-pressed="true"]:hover {
  background: var(--action);
  border-color: var(--action);
  color: #fff;
}

/* A sample of the category, emitted inline by src/client/colors.js from the
   data itself. Decorative and aria-hidden — the chip's text is its name. */
.colors-filter__dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  flex: none;
  background: var(--dot, #ccc);
  /* The hairline is what keeps the White dot visible on the gray pill. */
  box-shadow: inset 0 0 0 1px rgba(15, 42, 82, 0.18);
}
/* On the blue active fill a Blue or Violet dot would vanish; a white ring
   keeps every dot legible there. */
.colors-filter[aria-pressed="true"] .colors-filter__dot {
  box-shadow:
    inset 0 0 0 1px rgba(15, 42, 82, 0.18),
    0 0 0 2px #fff;
}

/* ---------- the count line ---------- */
.colors-count {
  font-family: var(--sans);
  font-size: 13px;
  color: var(--text-muted);
  padding-bottom: 18px;
}

/* ---------- grid ---------- */
/* minmax(0, 1fr) rather than 1fr so a column can never be widened past its
   share by its own content — that is what guarantees no horizontal overflow
   at any width, and it is why a long colour name shortens the name instead
   of stretching the grid.
   The breakpoints are /palettes' own (959px, 459px) rather than new numbers;
   only the column counts differ, because one wide colour block needs fewer
   columns than a four-strip palette plate does. */
.colors-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 28px 22px;
  padding-bottom: 32px;
}

@media (max-width: 959px) {
  .colors-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 24px 18px;
  }
}

@media (max-width: 459px) {
  .colors-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 18px;
  }
}

/* Reserve a screen of space while colors-data.json loads. The grid is
   empty until colors.js renders it, so without this the footer is drawn
   inside the first screen and then pushed off it — a large layout shift
   (CLS ~0.47 on mobile). If the load fails, the error message shows
   straight away instead of below an empty screen. */
.colors-grid:empty {
  min-height: 100vh;
}

.colors-grid:empty:has(~ .empty-state[data-visible="true"]) {
  min-height: 0;
}

/* ---------- card ---------- */
.color-card {
  min-width: 0;
}
/* [hidden] is how the filter hides a card. Grid items ignore the UA's
   `display:none` for [hidden] in no browser — but stating it here means the
   rule survives any future `display` set on .color-card. */
.color-card[hidden] {
  display: none;
}

/* ---------- colour plate ---------- */
/* The whole plate is the copy target. A fixed aspect ratio rather than a
   fixed height, so the block keeps its proportions from a 1-column phone to
   a 4-column desktop. */
.color-card__plate {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 5 / 2;
  min-height: 120px;
  border: 0;
  padding: 0;
  border-radius: var(--color-radius, 10px);
  background: var(--color-hex);
  cursor: pointer;
  /* Containment ring, emitted per card: a hairline for most colours and a
     stronger one for near-whites, which would otherwise have no visible edge
     against the page. Always painted, so the two cases have identical
     geometry. */
  box-shadow: inset 0 0 0 1px var(--color-ring, rgba(15, 42, 82, 0.06));
  transition: transform 0.18s var(--color-ease);
}

/* Guarded on a fine pointer: iOS Safari applies :hover on tap and leaves it
   applied until you tap elsewhere, which would strand one card lifted. */
@media (hover: hover) and (pointer: fine) {
  .color-card__plate:hover {
    transform: translateY(-2px);
  }
}
.color-card__plate:focus-visible {
  /* Inset, so the ring is drawn on the colour rather than in the grid gap
     where an adjacent card would clip it. */
  outline-offset: -4px;
}

/* The HEX, centred on the colour. Hover-revealed (and focus-revealed for
   keyboard users), hidden the rest of the time — the card stays exactly as
   still as before; opacity is the only thing changing.
   --color-label is computed per card from a WCAG contrast ratio, so it is
   near-black on a light swatch and near-white on a dark one.
   NOT a monospace face — the site has one family, var(--sans), and
   .mono in styles.css resolves to it too. Tabular figures give the alignment
   a monospace face would have been used for. */
.color-card__hex {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--sans);
  font-size: clamp(15px, 1.5vw, 19px);
  font-weight: 700;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  color: var(--color-label);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.16s var(--color-ease);
}
.color-card:hover .color-card__hex,
.color-card:focus-within .color-card__hex {
  opacity: 1;
}

/* ---------- copied state ---------- */
/* The confirmation lives inside the plate that was clicked, not in the
   site-wide toast — the toast is reserved for the failure case, where there
   is nothing to confirm inside the card. Everything here is opacity on an
   absolutely positioned overlay, so nothing in the grid can move.
   MUST stay declared after .color-card__hex — equal specificity, so source
   order is what lets the copied state hide the HEX underneath it. */
.color-card__copied {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border-radius: inherit;
  background: var(--color-veil, rgba(15, 42, 82, 0.45));
  color: var(--color-label);
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: 1;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s var(--color-ease);
}
.color-card__copied svg {
  width: 16px;
  height: 16px;
  flex: none;
  opacity: 0.9;
}
.color-card__plate[data-copied="true"] .color-card__copied {
  opacity: 1;
  transition: opacity 0.12s var(--color-ease);
}
.color-card__plate[data-copied="true"] .color-card__hex {
  opacity: 0;
}

/* ---------- name ---------- */
/* Shrinks below its own content width (min-width:0 on the card) and clips
   with an ellipsis rather than wrapping, so a long colour name can never
   grow one card taller than its row. */
.color-card__name {
  margin-top: 10px;
  padding: 0 2px;
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 500;
  color: var(--text);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* ---------- save ---------- */
/* Save to the account (docs/SAVED.md): drawn by colors.js only once Saved
   is launched, so nothing here matches before then. The name and a
   bookmark share one row, and the name still shrinks and clips first. The
   6px top margin keeps the name where it sat alone: the 24px row centres
   it about 4px down. The bookmark, its colours and its states are
   .palette-save-btn's on /palettes, declared here for the reason the
   header gives. saved.js sets aria-pressed, and aria-busy while a request
   runs. 24px minimum so the target stays usable beside the name. */
.color-card__foot {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
}
.color-card__foot .color-card__name {
  flex: 1 1 auto;
  min-width: 0;
  margin-top: 0;
}
.color-save-btn {
  appearance: none;
  background: none;
  border: 0;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  min-height: 24px;
  padding: 2px 3px;
  cursor: pointer;
  color: var(--text-muted);
  transition:
    color 0.16s var(--color-ease),
    opacity 0.16s var(--color-ease);
}
.color-save-btn svg {
  width: 18px;
  height: 18px;
  transition:
    transform 0.16s var(--color-ease),
    fill 0.16s var(--color-ease);
}
/* Hover guarded on a fine pointer, as the plate's lift is: a tap would
   otherwise leave an unsaved bookmark blue until the next tap elsewhere. */
@media (hover: hover) and (pointer: fine) {
  .color-save-btn:hover {
    color: var(--action);
  }
  .color-save-btn:hover svg {
    transform: scale(1.08);
  }
}
.color-save-btn:focus-visible {
  color: var(--action);
}
.color-save-btn:active svg {
  transform: scale(0.9);
}
/* Saved: filled in the action blue, hovered or not. */
.color-save-btn[aria-pressed="true"] {
  color: var(--action);
}
.color-save-btn[aria-pressed="true"] svg {
  fill: currentColor;
}
.color-save-btn[aria-busy="true"] {
  cursor: progress;
  opacity: 0.55;
}

/* ---------- reduced motion ---------- */
/* styles.css already forces every transition-duration to 0.001ms site-wide.
   The lift is switched off outright rather than sped up: a 2px jump is a
   bigger jolt than the movement it replaces. The copy feedback is pure
   opacity and stays — it never moved anything. */
@media (prefers-reduced-motion: reduce) {
  .color-card__plate:hover {
    transform: none;
  }
  .color-save-btn:hover svg,
  .color-save-btn:active svg {
    transform: none;
  }
}

/* Empty/error state reuses the site-wide .empty-state component from
   styles.css (display:none by default, [data-visible="true"] shows it) —
   nothing to add here. */
