/* ==========================================================================
   components.css - the document grammar
   Requires tokens.css + base.css.

   Component families, in the order a page uses them:
     field / record / mrz / stamp / endorsement / btn / form / dialog /
     tabs / toast / empty / skeleton
   ========================================================================== */

/* --------------------------------------------------------------------------
   FIELD - a small caps label over its value. The atom of a data page.
   -------------------------------------------------------------------------- */

.field-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--space-md) var(--space-lg);
}

.field { min-width: 0; }

.field-value {
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  letter-spacing: var(--tracking-mono);
  line-height: var(--line-snug);
  color: var(--color-text);
  margin-top: var(--space-3xs);
  overflow-wrap: anywhere;
}

/* An empty field is still a field: the document prints dashes rather than dropping the row. */
.field-value--empty { color: var(--color-text-disabled); }
.field-value--empty::after { content: "\2014\2014\2014"; }

/* Prose values (a title, a description) stay in the prose face. */
.field-value--prose {
  font-family: var(--font-sans);
  font-size: var(--font-size-base);
  letter-spacing: normal;
}

/* --------------------------------------------------------------------------
   RECORD - one entry in the register. Ruled, not carded: the separation is
   done by a hairline and a whole unit of air, which is what keeps density low
   without drawing a box around everything.
   -------------------------------------------------------------------------- */

.register {
  display: flex;
  flex-direction: column;
  border-top: var(--rule) solid var(--color-rule);
}

.record {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--space-md) var(--space-lg);
  align-items: start;
  /* Inset from the rule so content reads as a row with room to breathe, not text
     pinned to the edges; the border still runs the full width either side. */
  padding: var(--space-lg);
  border-bottom: var(--rule) solid var(--color-rule);
  transition: background var(--duration-fast) var(--ease-standard);
}

.record:hover { background: var(--color-bg-subtle); }

/* Destination gets a full row: a narrow grid column would wrap a URL four times. */
.record-body > .field { margin-bottom: var(--space-md); }

.record-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-sm);
  margin-bottom: var(--space-sm);
}

.record-title {
  /* flex-basis: 0% instead of the auto default - otherwise the wrap decision in
     .record-head uses this title's full single-line (max-content) width before
     any shrinking happens, so a long title alone can push the stamp beside it
     onto the next row even though the title would happily wrap to make room. */
  flex: 1 1 0%;
  font-size: var(--font-size-md);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  line-height: var(--line-snug);
  min-width: 0;
  overflow-wrap: anywhere;
}

.record-body { min-width: 0; }

.record-actions {
  display: flex;
  gap: var(--space-2xs);
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* --------------------------------------------------------------------------
   MRZ - the machine-readable strip. Carries the record's canonical string and
   IS the copy control, so it is a <button>. Chevron fillers are drawn by the
   pseudo-element, never typed into the DOM, so the copied text stays clean.
   -------------------------------------------------------------------------- */

.mrz {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  width: 100%;
  margin-top: var(--space-sm);
  padding: var(--space-xs) var(--space-sm);
  background: var(--color-bg-inset);
  border: 0;
  border-top: var(--rule) solid var(--color-rule);
  border-bottom: var(--rule) solid var(--color-rule);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  letter-spacing: var(--tracking-mono);
  color: var(--color-text);
  text-align: left;
  cursor: copy;
  transition: background var(--duration-fast) var(--ease-standard);
}

.mrz:hover { background: var(--color-accent-bg); }
.mrz:active { background: var(--color-accent-bg-pressed); border-color: var(--color-accent-border); transition: none; }
.mrz:active .mrz-action { color: var(--color-accent); }

.mrz-value { overflow-wrap: anywhere; }

/* Filler pads to the strip edge like a real MRZ line. Decoration, so hidden from the a11y tree. */
.mrz-fill {
  flex: 1;
  overflow: hidden;
  white-space: nowrap;
  color: var(--color-text-disabled);
  user-select: none;
}
.mrz-fill::after {
  content: "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<";
}

/* Sized for its longest state and centred in it, so copying moves neither the strip nor this label. */
.mrz-action {
  flex-shrink: 0;
  min-width: 3.5rem;
  /* Fixed px padding plus line-height: 1 keeps top/bottom on the same whole pixel everywhere. */
  padding: 4px 0.5em;
  text-align: center;
  font-family: var(--font-label);
  font-stretch: var(--font-stretch-label);
  font-size: var(--font-size-2xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  text-indent: var(--tracking-caps);
  line-height: 1;
  color: var(--color-text-muted);
}

.mrz:hover .mrz-action { color: var(--color-accent); }

/* The one authored moment: on copy the whole strip is endorsed, not just its label. */
.mrz.is-stamped {
  background: var(--color-accent-bg);
  border-color: var(--color-accent-border);
}
.mrz.is-stamped .mrz-action {
  color: var(--color-accent-text);
  background: var(--color-accent);
  animation: stamp var(--duration-slow) var(--ease-out);
}

@keyframes stamp {
  from { transform: scale(1.35); opacity: 0; filter: blur(2px); }
  to   { transform: scale(1);    opacity: 1; filter: blur(0); }
}

/* --------------------------------------------------------------------------
   STAMP - a record's state, set as an impression rather than a pill. Square,
   ruled, never rotated: rotation is the pastiche this world refuses.
   -------------------------------------------------------------------------- */

.stamp {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  /* Fixed px padding plus line-height: 1 keeps top/bottom on the same whole pixel everywhere. */
  padding: 4px 0.55em;
  border: var(--rule-strong) solid currentColor;
  font-family: var(--font-label);
  font-stretch: var(--font-stretch-label);
  font-size: var(--font-size-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  text-indent: var(--tracking-caps);
  line-height: 1;
  white-space: nowrap;
  transition: color var(--duration-slow) var(--ease-standard);
}

.stamp--active  { color: var(--color-success); }
.stamp--pending { color: var(--color-warning); }
.stamp--expired,
.stamp--revoked { color: var(--color-danger); }
.stamp--neutral { color: var(--color-text-muted); }

/* --------------------------------------------------------------------------
   ENDORSEMENT vs TAG - an endorsement is access control and takes a named
   field; a tag is cosmetic and takes none. Never let the two converge.
   -------------------------------------------------------------------------- */

/* The accent lands here and almost nowhere else: one stamped field per record. */
.endorsement-field {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs) var(--space-md);
  margin-top: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  background: var(--color-accent-bg);
  border: var(--rule) solid var(--color-accent-border);
}
/* Label and value share a size here: side by side on one line, a smaller caps reads as subordinate. */
.endorsement-field > .label,
.refusal > .label {
  flex-shrink: 0;
  font-size: var(--font-size-xs);
}
.endorsement-field > .label { color: var(--color-accent); }

.endorsements {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-md);
  align-items: baseline;
}

/* Every value in this field is one face at one size: only the ink changes with the meaning. */
.endorsement {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  letter-spacing: var(--tracking-mono);
  line-height: var(--line-snug);
  color: var(--color-accent);
  white-space: nowrap;
  transition: color var(--duration-slow) var(--ease-standard);
}

/* No scopes means the key reaches everything, which is the caution, so it takes warning ink. */
.endorsement--all { color: var(--color-warning); }
.endorsement--refused { color: var(--color-text-muted); }

/* The refusal sits at the far end of the field, the way a document states a limit. */
.refusal {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-xs);
  margin-left: auto;
}
.refusal > .label { color: var(--color-text-subtle); }

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  align-items: center;
}

.tag {
  font-family: var(--font-label);
  font-stretch: var(--font-stretch-label);
  font-size: var(--font-size-2xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--color-text-muted);
}
.tag::before { content: "\00B7\00A0"; }

/* --------------------------------------------------------------------------
   BUTTON
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  /* Fixed px padding plus line-height: 1 keeps top/bottom on the same whole pixel everywhere. */
  padding: 10px var(--space-md) 9px;
  border: var(--rule) solid transparent;
  border-radius: var(--radius);
  background: transparent;
  font-family: var(--font-label);
  font-stretch: var(--font-stretch-label);
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  text-indent: var(--tracking-caps);
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-standard),
              color var(--duration-fast) var(--ease-standard),
              border-color var(--duration-fast) var(--ease-standard);
}

.btn:hover { text-decoration: none; }

.btn--primary {
  background: var(--color-action);
  border-color: var(--color-action);
  color: var(--color-action-text);
}
.btn--primary:hover {
  background: var(--color-action-hover);
  border-color: var(--color-action-hover);
  color: var(--color-action-text);
}

.btn--secondary {
  border-color: var(--color-border-strong);
  color: var(--color-text);
}
.btn--secondary:hover { background: var(--color-action-ghost-hover); color: var(--color-text); }

.btn--ghost { color: var(--color-text-muted); }
.btn--ghost:hover { background: var(--color-action-ghost-hover); color: var(--color-text); }

.btn--danger {
  border-color: var(--color-danger-border);
  color: var(--color-danger);
}
.btn--danger:hover {
  background: var(--color-danger);
  border-color: var(--color-danger);
  color: var(--color-danger-text);
}

/* Press reads instantly and without easing: a document control is either pushed or it is not. */
.btn:active { transition: none; }
.btn--primary:active { background: var(--color-action); border-color: var(--color-action); }
.btn--secondary:active,
.btn--ghost:active { background: var(--color-action-ghost-pressed); color: var(--color-text); }
.btn--danger:active { background: var(--color-danger-hover); border-color: var(--color-danger-hover); color: var(--color-danger-text); }

.btn--sm { padding: 7px var(--space-sm) 6px; font-size: var(--font-size-2xs); }
.btn--block { width: 100%; }

.btn:disabled,
.btn[aria-disabled="true"] {
  background: var(--color-disabled-bg);
  border-color: var(--color-disabled-border);
  color: var(--color-disabled-text);
  cursor: not-allowed;
}

/* Loading reads as a document being processed, not a spinner. */
.btn.is-loading { color: transparent; position: relative; pointer-events: none; }
.btn.is-loading::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 0.9em; height: 0.9em;
  border: 2px solid currentColor;
  border-top-color: transparent;
  color: var(--color-action-text);
  animation: spin 700ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* --------------------------------------------------------------------------
   FORM
   -------------------------------------------------------------------------- */

.form-group { margin-bottom: var(--space-lg); }

.form-group > .label { margin-bottom: var(--space-3xs); }

input[type="text"], input[type="url"], input[type="email"],
input[type="password"], input[type="number"], input[type="search"],
input[type="datetime-local"], textarea, select {
  width: 100%;
  padding: 0.55rem var(--space-sm);
  background: var(--color-bg-raised);
  border: var(--rule) solid var(--color-border);
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  letter-spacing: var(--tracking-mono);
  color: var(--color-text);
  transition: border-color var(--duration-fast) var(--ease-standard);
}

textarea { font-family: var(--font-sans); letter-spacing: normal; resize: vertical; }

input::placeholder, textarea::placeholder { color: var(--color-text-subtle); }

input:hover, textarea:hover, select:hover { border-color: var(--color-border-strong); }

input:disabled, textarea:disabled, select:disabled {
  background: var(--color-disabled-bg);
  color: var(--color-disabled-text);
  cursor: not-allowed;
}

input[aria-invalid="true"], textarea[aria-invalid="true"] {
  border-color: var(--color-danger);
  border-width: var(--rule-strong);
}

input[type="checkbox"],
input[type="radio"] {
  width: 1rem; height: 1rem;
  accent-color: var(--color-accent);
  cursor: pointer;
}

input[type="range"]:not(.color-picker-hue) {
  display: block;
  width: 100%;
  height: 0.9rem;
  margin-top: var(--space-sm);
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  background: var(--color-bg-subtle);
  border: var(--rule) solid var(--color-border);
  cursor: pointer;
}
input[type="range"]:not(.color-picker-hue)::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 0.6rem;
  height: 1.1rem;
  background: var(--color-accent);
  border: var(--rule-strong) solid var(--color-text);
  cursor: pointer;
}
input[type="range"]:not(.color-picker-hue)::-moz-range-thumb {
  width: 0.6rem;
  height: 1.1rem;
  background: var(--color-accent);
  border: var(--rule-strong) solid var(--color-text);
  border-radius: 0;
  cursor: pointer;
}
input[type="range"]:not(.color-picker-hue)::-moz-range-track {
  height: 0.9rem;
  background: var(--color-bg-subtle);
  border: none;
}

input[type="color"] {
  width: 100%; height: 2.5rem;
  padding: 2px;
  background: var(--color-bg-raised);
  border: var(--rule) solid var(--color-border);
  cursor: pointer;
}

.checkbox-row {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  cursor: pointer;
  font-size: var(--font-size-sm);
}

.hint {
  display: block;
  margin-top: var(--space-3xs);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  max-width: var(--measure);
}

/* The marker is a glyph as well as a colour, so the state never depends on colour alone. */
.error-text {
  display: block;
  margin-top: var(--space-xs);
  padding: var(--space-xs) var(--space-sm);
  background: var(--color-danger-bg);
  border: var(--rule) solid var(--color-danger-border);
  color: var(--color-danger);
  font-size: var(--font-size-sm);
}
.error-text::before {
  content: "[!] ";
  font-family: var(--font-mono);
  font-weight: 700;
}

/* Lighter-weight than .error-text - sits directly under the one field it warns about
   instead of banking every warning into a single banner far from the offending value. */
.field-warning {
  display: block;
  margin-top: var(--space-2xs);
  font-size: var(--font-size-xs);
  color: var(--color-danger);
}
.field-warning::before {
  content: "[!] ";
  font-family: var(--font-mono);
  font-weight: 700;
}

/* --------------------------------------------------------------------------
   SELECT - a native <select>'s open list is OS-drawn and ignores the page
   entirely, so ./js/custom-select.js gives it a themed trigger and listbox
   of its own. The real <select> stays in the DOM as the value of record,
   invisible but present, so every existing .value read and write still works.
   -------------------------------------------------------------------------- */

.select { position: relative; }

.select-native {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  pointer-events: none;
}

.select-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  width: 100%;
  padding: 0.55rem var(--space-sm);
  background: var(--color-bg-raised);
  border: var(--rule) solid var(--color-border);
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  letter-spacing: var(--tracking-mono);
  color: var(--color-text);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-standard);
}
.select-trigger:hover { border-color: var(--color-border-strong); }
.select-trigger:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }

.select-trigger::after {
  content: "";
  flex-shrink: 0;
  width: 0.4em; height: 0.4em;
  border-right: var(--rule-strong) solid var(--color-text-muted);
  border-bottom: var(--rule-strong) solid var(--color-text-muted);
  transform: rotate(45deg) translateY(-3px);
  transition: transform var(--duration-fast) var(--ease-standard);
}
.select.is-open .select-trigger::after { transform: rotate(225deg) translateY(1px); }

.select-list {
  position: absolute;
  z-index: var(--z-modal);
  top: calc(100% + var(--space-2xs));
  left: 0;
  right: 0;
  max-height: 14rem;
  overflow-y: auto;
  background: var(--color-bg-raised);
  border: var(--rule) solid var(--color-border-strong);
  box-shadow: var(--shadow-dialog);
  display: none;
}
.select.is-open .select-list { display: block; }

.select-option {
  padding: 0.5rem var(--space-sm);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  letter-spacing: var(--tracking-mono);
  color: var(--color-text);
  cursor: pointer;
  border-bottom: var(--rule) solid var(--color-rule);
}
.select-option:last-child { border-bottom: 0; }
.select-option:hover { background: var(--color-action-ghost-hover); }
.select-option.is-selected { background: var(--color-accent-bg); color: var(--color-accent); }

/* --------------------------------------------------------------------------
   DIALOG - the only surface in this world with real depth, because it is the
   only one that genuinely leaves the page.
   -------------------------------------------------------------------------- */

.overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: var(--space-lg);
  overflow-y: auto;
  background: var(--color-bg-overlay);
  animation: overlay-in var(--duration-base) var(--ease-standard);
}
@keyframes overlay-in { from { opacity: 0; } to { opacity: 1; } }

.sheet {
  width: 100%;
  max-width: 34rem;
  margin: auto;
  background: var(--color-bg-raised);
  border: var(--rule) solid var(--color-border-strong);
  box-shadow: var(--shadow-dialog);
  animation: sheet-in var(--duration-slow) var(--ease-out);
}
@keyframes sheet-in {
  from { transform: translateY(0.75rem); opacity: 0; }
  to   { transform: translateY(0);       opacity: 1; }
}

.sheet--wide { max-width: 46rem; }
.sheet--xwide { max-width: min(96vw, 120rem); }

.sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-lg) var(--space-xl);
  border-bottom: var(--rule) solid var(--color-rule);
}

.sheet-body { padding: var(--space-xl); }
.sheet-body > :last-child { margin-bottom: 0; }

.sheet-foot {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-xl);
  border-top: var(--rule) solid var(--color-rule);
}

.sheet-close {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem; height: 2rem;
  padding: 0;
  background: transparent;
  border: var(--rule) solid transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-standard);
}
.sheet-close:hover { background: var(--color-action-ghost-hover); color: var(--color-text); }
.sheet-close svg { width: 1rem; height: 1rem; }

/* A section inside a sheet is ruled off, never boxed: this is what replaces nested cards. */
.sheet-section { margin-bottom: var(--space-xl); }
.sheet-section > .label { margin-bottom: var(--space-md); padding-bottom: var(--space-2xs); border-bottom: var(--rule) solid var(--color-rule); }

/* --------------------------------------------------------------------------
   TABS
   -------------------------------------------------------------------------- */

.tabs {
  display: flex;
  gap: var(--space-lg);
  overflow-x: auto;
  scrollbar-width: none;
  border-bottom: var(--rule) solid var(--color-rule);
}
.tabs::-webkit-scrollbar { display: none; }

.tab {
  flex-shrink: 0;
  padding: var(--space-sm) 0;
  margin-bottom: -1px;
  background: transparent;
  border: 0;
  border-bottom: var(--rule-strong) solid transparent;
  font-family: var(--font-label);
  font-stretch: var(--font-stretch-label);
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--color-text-muted);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-standard),
              border-color var(--duration-fast) var(--ease-standard);
}
.tab:hover { color: var(--color-text); }
.tab:active { color: var(--color-text); border-bottom-color: var(--color-rule); }
.tab[aria-selected="true"],
.tab.active {
  color: var(--color-text);
  border-bottom-color: var(--color-rule-strong);
}

/* --------------------------------------------------------------------------
   TOAST - the product had no action feedback at all before this.
   -------------------------------------------------------------------------- */

.toast-region {
  position: fixed;
  bottom: var(--space-lg);
  left: 50%;
  transform: translateX(-50%);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  align-items: center;
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-lg);
  background: var(--color-text);
  color: var(--color-bg-raised);
  font-size: var(--font-size-sm);
  box-shadow: var(--shadow-dialog);
  animation: toast-in var(--duration-slow) var(--ease-out);
}
@keyframes toast-in {
  from { transform: translateY(0.5rem); opacity: 0; }
  to   { transform: translateY(0);      opacity: 1; }
}
.toast.is-leaving { animation: toast-out var(--duration-base) var(--ease-standard) forwards; }
@keyframes toast-out { to { transform: translateY(0.35rem); opacity: 0; } }

.toast--danger { background: var(--color-danger); color: var(--color-danger-text); }

/* --------------------------------------------------------------------------
   EMPTY + SKELETON
   -------------------------------------------------------------------------- */

.empty {
  padding: var(--space-3xl) var(--space-lg);
  border-bottom: var(--rule) solid var(--color-rule);
  text-align: left;
  max-width: var(--measure);
}
.empty h3 {
  font-size: var(--font-size-md);
  margin-bottom: var(--space-xs);
}
.empty p {
  color: var(--color-text-muted);
  margin-bottom: var(--space-lg);
}

/* The skeleton is a real record, laid out with its real fields, buttons and all,
   so the page never resizes when data arrives: only the ink is still a cipher.
   ./js/decipher.js keeps every [data-noise] span rolling through glyphs until
   the real record lands, then decipherIn() takes over and resolves it for real. */
.record--skeleton:hover { background: none; }
/* Color only: each element already carries its real font (.field-value's own
   mono rule, .record-title's mono modifier only where the real record has one,
   .stamp's label face). Forcing mono here made every skeleton title that isn't
   naturally mono - boards, receipts, keys, pending users - render visibly
   larger than its loaded counterpart, since Martian Mono sits bigger than
   Archivo at the same font-size. */
.skeleton-line {
  color: var(--color-text-disabled);
  user-select: none;
}
.mrz--skeleton { pointer-events: none; cursor: default; }

/* A record that just replaced a skeleton (or arrived past the skeleton's count)
   settles into place instead of popping in, so a shorter or longer real list
   than the placeholder never reads as a layout jolt. Fade only: a slide read
   as the whole card shifting, which fought the in-place decode underneath it. */
@keyframes record-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.record--enter {
  animation: record-in var(--duration-slow) var(--ease-out) both;
}

/* An excess skeleton card (real list came back shorter than predicted) leaves
   the same way a real one arrives, just in reverse. */
@keyframes record-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}
.record--exit {
  animation: record-out var(--duration-base) var(--ease-standard) both;
}

/* --------------------------------------------------------------------------
   COLOR PICKER - same rationale as SELECT above: input[type=color] is
   OS-drawn, so ./js/color-picker.js gives data-color-picker inputs a themed
   swatch trigger + popover instead. The real <input type=text> stays in the
   DOM as the value of record, invisible but present.
   -------------------------------------------------------------------------- */

.color-picker { position: relative; }

.color-picker-native {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  pointer-events: none;
}

.color-picker-trigger {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  padding: 0.4rem var(--space-sm);
  background: var(--color-bg-raised);
  border: var(--rule) solid var(--color-border);
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-standard);
}
.color-picker-trigger:hover { border-color: var(--color-border-strong); }
.color-picker-trigger:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }

.color-picker-swatch {
  flex-shrink: 0;
  width: 1.5rem; height: 1.5rem;
  border: var(--rule) solid var(--color-border-strong);
  background: var(--color-bg-subtle);
}
.color-picker-swatch--empty {
  background-image: linear-gradient(45deg, var(--color-border) 25%, transparent 25%, transparent 75%, var(--color-border) 75%),
    linear-gradient(45deg, var(--color-border) 25%, transparent 25%, transparent 75%, var(--color-border) 75%);
  background-size: 0.5rem 0.5rem;
  background-position: 0 0, 0.25rem 0.25rem;
}

.color-picker-hex {
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  letter-spacing: var(--tracking-mono);
  color: var(--color-text);
}

.color-picker-popover {
  display: none;
  position: absolute;
  z-index: var(--z-modal);
  top: calc(100% + var(--space-2xs));
  left: 0;
  width: 14rem;
  padding: var(--space-sm);
  background: var(--color-bg-raised);
  border: var(--rule) solid var(--color-border-strong);
  box-shadow: var(--shadow-dialog);
}
.color-picker.is-open .color-picker-popover { display: block; }

.color-picker-sv {
  display: block;
  width: 100%;
  height: 8rem;
  cursor: crosshair;
  border: var(--rule) solid var(--color-border);
  touch-action: none;
}

.color-picker-hue {
  display: block;
  width: 100%;
  height: 0.9rem;
  margin-top: var(--space-sm);
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  background: linear-gradient(to right,
    #ff0000, #ffbf00, #80ff00, #00ff40, #00ffff, #0040ff, #8000ff, #ff00bf, #ff0000);
  border: var(--rule) solid var(--color-border);
  cursor: pointer;
}
.color-picker-hue::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 0.6rem;
  height: 1.1rem;
  background: var(--color-bg-raised);
  border: var(--rule-strong) solid var(--color-text);
  cursor: pointer;
}
.color-picker-hue::-moz-range-thumb {
  width: 0.6rem;
  height: 1.1rem;
  background: var(--color-bg-raised);
  border: var(--rule-strong) solid var(--color-text);
  border-radius: 0;
  cursor: pointer;
}
.color-picker-hue::-moz-range-track {
  height: 0.9rem;
  background: linear-gradient(to right,
    #ff0000, #ffbf00, #80ff00, #00ff40, #00ffff, #0040ff, #8000ff, #ff00bf, #ff0000);
  border: none;
}

.color-picker-fields {
  display: flex;
  gap: var(--space-xs);
  margin-top: var(--space-sm);
}
.color-picker-hexfield { flex: 1; min-width: 0; }

.color-picker-recents {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin-top: var(--space-sm);
}
.color-picker-recent {
  width: 1.25rem; height: 1.25rem;
  border: var(--rule) solid var(--color-border-strong);
  cursor: pointer;
  padding: 0;
}

/* --------------------------------------------------------------------------
   GRADIENT BUILDER
   -------------------------------------------------------------------------- */

.gradient-builder-preview {
  height: 3.5rem;
  margin-bottom: var(--space-md);
  border: var(--rule) solid var(--color-border);
  background: var(--color-bg-subtle);
}

.gradient-angle-readout {
  font-family: var(--font-mono);
  color: var(--color-text-muted);
}

/* --------------------------------------------------------------------------
   FONT PAIRING GRID
   -------------------------------------------------------------------------- */

.font-pairing-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr));
  gap: var(--space-sm);
}

.font-pairing-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-md) var(--space-sm);
  background: var(--color-bg-raised);
  border: var(--rule) solid var(--color-border);
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-standard);
}
.font-pairing-card:hover { border-color: var(--color-border-strong); }
.font-pairing-card.is-selected { border-color: var(--color-accent); border-width: var(--rule-strong); }

.font-pairing-heading {
  font-size: var(--font-size-xl);
  color: var(--color-text);
}

.font-pairing-label {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

/* --------------------------------------------------------------------------
   APPEARANCE PANEL - two-column editor: controls on the left, a live
   sandboxed preview of the public board page on the right.
   -------------------------------------------------------------------------- */

.appearance-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 46rem;
  gap: var(--space-2xl);
  align-items: start;
}
@media (max-width: 90rem) {
  .appearance-body { grid-template-columns: 1fr; }
}

.appearance-preview-pane {
  position: sticky;
  top: 0;
}

.appearance-preview-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
  padding-bottom: var(--space-2xs);
  border-bottom: var(--rule) solid var(--color-rule);
}

.appearance-preview-toolbar { display: flex; gap: var(--space-2xs); }

.appearance-preview-toggle {
  padding: var(--space-2xs) var(--space-sm);
  background: var(--color-bg-raised);
  border: var(--rule) solid var(--color-border);
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-standard);
}
.appearance-preview-toggle:hover { border-color: var(--color-border-strong); }
.appearance-preview-toggle.is-active { border-color: var(--color-accent); color: var(--color-text); }

.appearance-preview-frame {
  display: block;
  width: 100%;
  max-width: 46rem;
  height: 34rem;
  margin: 0 auto;
  background: var(--color-bg-subtle);
  border: var(--rule) solid var(--color-border);
}
.appearance-preview-frame.is-mobile {
  max-width: 23rem;
  margin: 0 auto;
}
