/* Design system: shadcn/ui (new-york-v4 components, neutral dark theme),
   ported to plain CSS since this project has no React/Tailwind build.
   Token values are shadcn's `.dark` block (apps/v4/app/globals.css in
   shadcn-ui/ui), converted from oklch to hex; component rules below
   mirror the Tailwind classes of registry/new-york-v4/ui/*.tsx (button,
   input, select, slider, switch, checkbox, toggle, card, badge,
   progress) — each section names the component it follows.

   One deliberate deviation for WCAG 2.2 AA (1.4.11 Non-text Contrast):
   shadcn draws control boundaries with --input (white at 15%, ~1.6:1 on
   the card) and empty slider tracks with --muted (~1.3:1). Those use
   --control-border instead (#6e6e6e: 3.5:1 on --card, 3.2:1 on --field,
   3.9:1 on --background). Text colors are shadcn's own and already pass:
   --muted-foreground 6.9:1 on --card, 6.2:1 on --field. */
:root {
  --radius: 0.625rem; /* 10px */
  --radius-sm: 6px; /* radius × 0.6 */
  --radius-md: 8px; /* radius × 0.8 */
  --radius-lg: 10px; /* radius */
  --radius-xl: 14px; /* radius × 1.4 */
  /* Reference-video shapes: cards 20px, every control a full pill. */
  --radius-card: 20px;
  --radius-pill: 999px;

  --background: #0a0a0a; /* oklch(0.145 0 0) */
  --foreground: #fafafa; /* oklch(0.985 0 0) */
  --card: #171717; /* oklch(0.205 0 0) */
  --popover: #171717;
  --primary: #e5e5e5; /* oklch(0.922 0 0) */
  --primary-hover: rgba(229, 229, 229, 0.9); /* primary/90 */
  --primary-foreground: #171717;
  --secondary: #262626; /* oklch(0.269 0 0) */
  --muted: #262626;
  --muted-foreground: #a1a1a1; /* oklch(0.708 0 0) */
  --accent: #404040; /* oklch(0.371 0 0) */
  --accent-foreground: #fafafa;
  --destructive: #ff6467; /* oklch(0.704 0.191 22.216) */
  --border: rgba(255, 255, 255, 0.1); /* oklch(1 0 0 / 10%) */
  --input: rgba(255, 255, 255, 0.15); /* oklch(1 0 0 / 15%) */
  --input-30: rgba(255, 255, 255, 0.045); /* dark:bg-input/30 */
  --input-50: rgba(255, 255, 255, 0.075); /* dark:hover:bg-input/50 */
  --ring: #737373; /* oklch(0.556 0 0) */
  --ring-50: rgba(115, 115, 115, 0.5); /* ring-ring/50 */

  /* Solid equivalent of --input-30 over --card, for contrast math and
     places that need an opaque fill. */
  --field: #212121;
  --control-border: #6e6e6e;
  /* Grey for text inside controls (slider/field names and values):
     7.3:1 on --secondary, ≥ 4.7:1 on the slider fill even when hovered
     (#3e3e3e / #454545). */
  --control-text: #b4b4b4;

  /* Tailwind shadow scale */
  --shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);

  --focus-ring: 0 0 0 3px var(--ring-50);
  --gap: 24px;
  color-scheme: dark;
}

* {
  box-sizing: border-box;
}

/* Component rules like `.field { display: flex }` would otherwise beat
   the UA stylesheet and make `hidden` elements visible. */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  background: var(--background);
  color: var(--foreground);
  /* Tailwind's default font-sans stack, which shadcn inherits. */
  font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
  font-size: 14px;
  line-height: 1.43;
  -webkit-font-smoothing: antialiased;
}

button,
select,
input {
  font: inherit;
  color: inherit;
}

/* Borderless focusables (swatches, thumbnails, links) get an outline;
   bordered controls override this with shadcn's border + 3px ring. */
:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* --- Layout -------------------------------------------------------------
   Wide screens: [controls 568] [preview] [export 332], gallery below.   */

.layout {
  display: grid;
  grid-template-columns: 568px minmax(0, 1fr) 332px;
  grid-template-areas:
    'controls preview output'
    'gallery  gallery gallery';
  align-items: stretch;
  gap: 40px var(--gap);
  padding: 40px;
  max-width: 1920px;
  margin: 0 auto;
}

.controls {
  grid-area: controls;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.controls-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  align-items: stretch;
}

/* Card: rounded-xl border bg-card py-6 shadow-sm (px-6 on its parts). */
.card {
  background: var(--card);
  color: var(--foreground);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-sm);
  padding: 24px;
  min-width: 0;
}

.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 16px;
}

/* CardTitle: leading-none font-semibold. */
.card h2 {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.01em;
}

/* Secondary text next to a title ("Цвета  4 из 6"), like "All lights
   ˅ Office" in the reference. */
.card-meta {
  margin-left: 6px;
  font-weight: 500;
  color: var(--muted-foreground);
  font-variant-numeric: tabular-nums;
}

.header-actions {
  display: flex;
  gap: 2px;
  /* Icon buttons are 32px tall in a 16px header; let them overhang. */
  margin: -8px -8px -8px 0;
}

/* Button, variant "ghost", size "icon-sm" (size-8). */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  background: none;
  color: var(--foreground);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  flex: none;
  transition: background-color 0.15s, color 0.15s, box-shadow 0.15s;
}

.icon-btn:hover:not(:disabled) {
  background: rgba(64, 64, 64, 0.5); /* dark:hover:bg-accent/50 */
  color: var(--accent-foreground);
}

.icon-btn:focus-visible {
  outline: none;
  border-color: var(--ring);
  box-shadow: var(--focus-ring);
}

.icon-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.icon-btn-muted {
  width: 24px;
  height: 24px;
  margin-right: -6px;
  color: var(--muted-foreground);
  font-size: 12px;
}

.icon-btn-muted:hover:not(:disabled) {
  color: var(--destructive);
}

/* Button, variant "link": text-primary underline-offset-4 hover:underline. */
.text-btn {
  padding: 0;
  border: none;
  background: none;
  color: var(--primary);
  font-size: 14px;
  font-weight: 500;
  text-underline-offset: 4px;
  cursor: pointer;
  white-space: nowrap;
}

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

/* CardDescription-style helper text. */
.hint {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted-foreground);
}

/* Shared: Button, variant "outline" (dark): border-input bg-input/30
   shadow-xs hover:bg-input/50 — with --control-border for the border. */
.pattern-new,
.add-color,
.size-preset-btn,
.cancel-export-btn,
.result-download {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-pill);
  background: var(--secondary);
  box-shadow: var(--shadow-xs);
  color: var(--foreground);
  font-size: 14px;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.15s, border-color 0.15s, box-shadow 0.15s;
}

.pattern-new:hover,
.add-color:hover,
.size-preset-btn:hover,
.cancel-export-btn:hover,
.result-download:hover {
  background: var(--accent);
}

.pattern-new:focus-visible,
.add-color:focus-visible,
.size-preset-btn:focus-visible,
.cancel-export-btn:focus-visible,
.result-download:focus-visible {
  outline: none;
  border-color: var(--ring);
  box-shadow: var(--focus-ring);
}

/* --- Colors card -------------------------------------------------------- */

.controls-row > .card:first-child {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.color-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Input look (h-9 rounded-md border bg-input/30 shadow-xs) wrapping the
   swatch, hex field and remove button — an input group. */
.color-row {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 40px;
  padding: 0 8px 0 14px;
  background: var(--secondary);
  border: 1px solid var(--control-border);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-xs);
  transition: opacity 0.15s, border-color 0.15s, box-shadow 0.15s;
}

.color-row.dragging {
  opacity: 0.4;
}

.color-row.drag-over {
  border-color: var(--foreground);
}

.drag-handle {
  margin: 0 -6px 0 -2px;
  padding: 6px 2px;
  flex: none;
  font-size: 14px;
  line-height: 1;
  color: var(--muted-foreground);
  cursor: grab;
  user-select: none;
  touch-action: none;
}

.drag-handle:active {
  cursor: grabbing;
}

.color-index {
  width: 14px;
  flex: none;
  font-size: 12px;
  font-weight: 500;
  color: var(--muted-foreground);
  font-variant-numeric: tabular-nums;
}

input[type='color'] {
  width: 20px;
  height: 20px;
  flex: none;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: none;
  cursor: pointer;
}

input[type='color']::-webkit-color-swatch-wrapper {
  padding: 0;
}

/* Border so a swatch close to the row color (e.g. near-black) is still
   visible as a control. */
input[type='color']::-webkit-color-swatch {
  border: 1px solid var(--control-border);
  border-radius: 50%;
}

input[type='color']::-moz-color-swatch {
  border: 1px solid var(--control-border);
  border-radius: 50%;
}

.color-hex {
  flex: 1;
  min-width: 0;
  padding: 7px 0;
  border: none;
  background: none;
  font-size: 14px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  text-transform: lowercase;
}

.color-hex:focus {
  outline: none;
}

/* The hex field drops its own outline, so the row shows focus instead
   (focus-visible:border-ring focus-visible:ring-[3px] ring-ring/50). */
.color-row:focus-within {
  border-color: var(--ring);
  box-shadow: var(--focus-ring);
}

.color-hex::placeholder {
  color: var(--muted-foreground);
  text-transform: none;
}

.add-color {
  width: 100%;
  height: 40px;
  padding: 0 16px;
}

/* --- Sliders ------------------------------------------------------------
   Pill sliders after the reference video: a rounded track with a lighter
   rounded fill, a thin white handle just past the fill that bends into a
   bracket against the track's round end at min/max, and the name (left)
   and value (right) inside the track, above the handle (which dims while
   it passes under either — .handle-dim, set in bindRange()). The native range input
   lies on top, transparent, and keeps doing all input handling; the
   visuals read --f (0..1) from .slider. Handle centre = 12px + f × (width
   − 24px), which is exactly where the 24px-wide native thumb maps the
   value, so a click lands where the handle is drawn. */

.controls-row > .card:last-child,
.grain-main {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.sliders {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.slider {
  --slider-h: 40px;
  --f: 0.5;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.slider-compact {
  --slider-h: 36px;
}


.slider-control {
  position: relative;
  display: block;
  height: var(--slider-h);
}

/* Track boundary uses --control-border (3:1 on the card, 1.4.11). */
.slider-track {
  position: absolute;
  inset: 0;
  z-index: 0; /* own stacking context: fill (-1) under text, handle over */
  display: block;
  border: 1px solid var(--control-border);
  border-radius: 999px;
  background: var(--secondary);
  pointer-events: none;
  transition: border-color 0.15s, box-shadow 0.15s;
}

/* Fill: a pill inset 3px from the track, ending 7px before the handle. */
.slider-track::before {
  content: '';
  position: absolute;
  top: 3px;
  bottom: 3px;
  left: 3px;
  z-index: -1;
  width: max(0px, calc(2px + var(--f) * (100% - 24px)));
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.11);
  transition: width 0.08s ease-out, background-color 0.15s;
}

/* Handle: 3px white bar, 50% of the track's height; under the text. */
.slider-track::after {
  content: '';
  position: absolute;
  z-index: 0;
  top: 50%;
  left: calc(12px + var(--f) * (100% - 24px));
  width: 3px;
  height: 50%;
  border: 0 solid #fff;
  border-radius: 2px;
  background: #fff;
  transform: translate(-50%, -50%);
  transition: left 0.08s ease-out, height 0.15s, width 0.15s, border-radius 0.15s, background-color 0.15s, opacity 0.15s;
}

.slider.handle-dim .slider-track::after {
  opacity: 0.35;
}

/* At an end the bar becomes a bracket hugging the round end: ( or ). */
.slider[data-edge='max'] .slider-track::after,
.slider[data-edge='min'] .slider-track::after {
  width: 8px;
  height: 56%;
  background: transparent;
}

.slider[data-edge='max'] .slider-track::after {
  border-right-width: 3px;
  border-radius: 0 999px 999px 0;
}

.slider[data-edge='min'] .slider-track::after {
  border-left-width: 3px;
  border-radius: 999px 0 0 999px;
}

.slider-control:hover .slider-track::before {
  background: rgba(255, 255, 255, 0.14);
}

/* Grabbed: the handle stretches, like the reference. */
.slider-control:active .slider-track::after {
  height: 68%;
}

.slider-control:has(input:focus-visible) .slider-track {
  border-color: var(--ring);
  box-shadow: var(--focus-ring);
}

.slider-name,
.slider-track output {
  position: absolute;
  top: 50%;
  z-index: 1;
  transform: translateY(-50%);
  color: var(--control-text);
  font-size: 13px;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
}

/* Name left, value right, same type — like the value readout in the
   reference. The name truncates rather than running into the value. */
.slider-name {
  left: 14px;
  overflow: hidden; /* max-width set by bindRange(), from the value's width */
  text-overflow: ellipsis;
  padding-bottom: 1px; /* keeps descenders (у, д) inside the clip */
}

.slider-track output {
  right: 14px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Odometer: changed characters roll in (setRollingText in main.js). */
.slider-track output span {
  display: inline-block;
}

.slider-track output .roll-up {
  animation: roll-up 0.22s ease-out;
}

.slider-track output .roll-down {
  animation: roll-down 0.22s ease-out;
}

@keyframes roll-up {
  from { transform: translateY(45%); opacity: 0.2; filter: blur(2px); }
}

@keyframes roll-down {
  from { transform: translateY(-45%); opacity: 0.2; filter: blur(2px); }
}

@media (prefers-reduced-motion: reduce) {
  .slider-track::before,
  .slider-track::after {
    transition: none;
  }

  .slider-track output .roll-up,
  .slider-track output .roll-down {
    animation: none;
  }
}

/* The real input: full-size, invisible, on top. */
input[type='range'] {
  -webkit-appearance: none;
  appearance: none;
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  margin: 0;
  background: transparent;
  opacity: 0;
  cursor: pointer;
}

input[type='range']::-webkit-slider-runnable-track {
  height: 100%;
}

input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 24px;
  height: 100%;
}

input[type='range']::-moz-range-track {
  height: 100%;
}

input[type='range']::-moz-range-thumb {
  width: 24px;
  height: 100%;
  border: 0;
}

input[type='range']:disabled {
  cursor: not-allowed;
}

.slider.is-disabled {
  opacity: 0.5;
}

.range-legend {
  display: flex;
  justify-content: space-between;
  margin-top: -2px;
  font-size: 12px;
  color: var(--muted-foreground);
}

/* --- Grain card --------------------------------------------------------- */

.grain-card {
  display: flex;
  gap: var(--gap);
  align-items: flex-start;
}

.grain-main,
.grain-side {
  flex: 1 1 0;
  min-width: 0;
}

.grain-side {
  display: flex;
  flex-direction: column;
  gap: 20px;
  /* Lines the first field up with the sliders beside it (the header is
     16px + the 20px gap). */
  padding-top: 36px;
}

/* Switch (size "default"): h-[1.15rem] w-8 rounded-full; checked
   bg-primary with a primary-foreground thumb, unchecked bg-input/80
   with a foreground thumb. The unchecked track also gets a
   --control-border edge for 3:1. */
.switch {
  position: relative;
  display: inline-flex;
  cursor: pointer;
}

.switch input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.switch-track {
  position: relative;
  width: 32px;
  height: 18.4px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid var(--control-border);
  box-shadow: var(--shadow-xs);
  transition: background-color 0.15s, border-color 0.15s, box-shadow 0.15s;
}

.switch-track::after {
  content: '';
  position: absolute;
  top: 0.2px;
  left: 0;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--foreground);
  transition: transform 0.15s, background-color 0.15s;
}

.switch input:checked + .switch-track {
  background: var(--primary);
  border-color: var(--primary);
}

.switch input:checked + .switch-track::after {
  transform: translateX(14px);
  background: var(--primary-foreground);
}

.switch input:focus-visible + .switch-track {
  border-color: var(--ring);
  box-shadow: var(--focus-ring);
}

/* --- Fields (selects, number inputs) ------------------------------------ */

.field {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}

/* Input / SelectTrigger: h-9 rounded-md border bg-input/30 px-3 text-sm
   shadow-xs, focus-visible:border-ring + 3px ring-ring/50. */
.input,
select {
  width: 100%;
  height: 40px;
  padding: 0 16px;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-pill);
  background: var(--secondary);
  box-shadow: var(--shadow-xs);
  color: var(--foreground);
  font-size: 14px;
  transition: border-color 0.15s, box-shadow 0.15s, background-color 0.15s;
}

.input:focus-visible,
select:focus-visible {
  outline: none;
  border-color: var(--ring);
  box-shadow: var(--focus-ring);
}

.input:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Native <select> keeps the OS list; the trigger gets shadcn's look
   with a ▾ glyph in place of the chevron icon. */
.select-wrap {
  position: relative;
  display: block;
}

/* Name inside the control on the left, like the sliders; the chosen
   value is right-aligned before the ▾. Shared by the selects and the
   grain color row. */
.select-label {
  position: absolute;
  top: 50%;
  left: 14px;
  z-index: 1;
  transform: translateY(-50%);
  color: var(--control-text);
  font-size: 13px;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  pointer-events: none;
}

.select-wrap select {
  color: var(--control-text);
  font-size: 13px;
  font-weight: 600;
  text-align: right;
  text-align-last: right;
}

.select-wrap select option {
  text-align: left;
}

/* In a flex row (grain color) the label is a normal item that pushes
   the swatch, hex and ✕ to the right. */
.color-row-labeled .select-label {
  position: static;
  transform: none;
  margin-right: auto;
}

.color-row-labeled .color-hex {
  flex: none;
  width: 8ch;
  color: var(--control-text);
  font-size: 13px;
}

.select-wrap::after {
  content: '▾';
  position: absolute;
  top: 50%;
  right: 16px;
  transform: translateY(-50%);
  font-size: 12px;
  line-height: 1;
  color: var(--muted-foreground);
  pointer-events: none;
}

select {
  -webkit-appearance: none;
  appearance: none;
  padding: 0 36px 0 16px;
  cursor: pointer;
}

select:hover:not(:disabled) {
  background: var(--accent);
}

select:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

select option {
  background: var(--popover);
  color: var(--foreground);
}

.input::-webkit-inner-spin-button,
.input::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.input {
  -moz-appearance: textfield;
  appearance: textfield;
  font-variant-numeric: tabular-nums;
}

/* --- Preview ------------------------------------------------------------ */

.canvas-wrap {
  grid-area: preview;
  position: relative;
  min-width: 0;
  /* 701px = preview height in the mock; keeps it full size even while
     the grain card is collapsed. Overridden below 1440px. */
  min-height: 701px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  /* Height of the pattern-history strip pinned at the bottom, so
     .canvas-box (below) can reserve room for it instead of the canvas
     sitting underneath it. Bumped on the short mobile layout below. */
  --pattern-strip-h: 76px;
  /* The strip's own item size reacts to how much width .canvas-wrap
     actually has (not the viewport), since at some viewports it's the
     narrower of several grid columns. */
  container-type: inline-size;
}

/* Absolutely positioned so the canvas's intrinsic size (the export size,
   e.g. 1600×900) never feeds back into the grid row height. The bottom
   inset additionally reserves the pattern-history strip's height so the
   canvas never sits underneath it. */
.canvas-box {
  --inset: clamp(16px, 5%, 96px);
  position: absolute;
  top: var(--inset);
  left: var(--inset);
  right: var(--inset);
  bottom: calc(var(--inset) + var(--pattern-strip-h));
  display: flex;
  align-items: center;
  justify-content: center;
}

#previewCanvas {
  display: block;
  max-width: 100%;
  max-height: 100%;
  /* Set from state.previewRadius by applyPreviewRadius() in main.js. */
  border-radius: 16px;
}

/* Corner-radius handles, positioned by applyPreviewRadius(). White fill
   with a dark ring so they stay visible (≥ 3:1) on any gradient. Shown
   while the preview is hovered or a handle has focus. */
.radius-handle {
  position: absolute;
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  border: 2px solid var(--background);
  border-radius: 50%;
  background: var(--foreground);
  box-shadow: 0 0 0 1px var(--foreground);
  opacity: 0;
  transition: opacity 0.15s;
  touch-action: none;
}

.radius-handle[data-corner='tl'],
.radius-handle[data-corner='br'] {
  cursor: nwse-resize;
}

.radius-handle[data-corner='tr'],
.radius-handle[data-corner='bl'] {
  cursor: nesw-resize;
}

.canvas-wrap:hover .radius-handle,
.canvas-box:focus-within .radius-handle,
.radius-handle.is-dragging {
  opacity: 1;
}

/* --- Pattern history ------------------------------------------------- */

/* A floating toolbar (popover look: bg-popover, border, rounded-lg,
   shadow-md): "Новый узор", a vertical separator, then the history.
   Everything in the row is the same height (--pattern-item-h) and
   vertically centered; the age sits on the thumbnail as a chip, so no
   caption row hangs below the thumbnails. */
.pattern-bar {
  --pattern-item-w: 88px;
  --pattern-item-h: 48px;
  --pattern-gap: 8px;
  position: absolute;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: var(--pattern-gap);
  padding: 8px;
  max-width: calc(100% - 32px);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--popover);
  box-shadow: var(--shadow-md);
}

.pattern-new {
  flex: none;
  gap: 6px;
  height: var(--pattern-item-h);
  padding: 0 14px;
}

.pattern-new > [aria-hidden] {
  font-size: 16px;
  line-height: 1;
}

/* Separator, orientation "vertical": w-px bg-border. */
.pattern-sep {
  flex: none;
  align-self: stretch;
  width: 1px;
  margin: 4px 0;
  background: var(--border);
}

.pattern-history {
  display: flex;
  gap: var(--pattern-gap);
  min-width: 0;
  /* 4px of room inside the scroller for the active/hover rings (2px gap
     + 2px ring), which overflow-x would otherwise clip; the negative
     margin keeps the bar's own size unchanged. */
  padding: 4px;
  margin: -4px;
  /* Safety net only: the container query below sizes items to fit
     without scrolling at every layout width we support, same as
     .results hides its scrollbar because main.js drives that one by
     wheel instead. */
  overflow-x: auto;
  scrollbar-width: none;
}

.pattern-history::-webkit-scrollbar {
  display: none;
}

/* Wraps .pattern-item so the pin button (below) can sit over its
   thumbnail corner without nesting a <button> inside a <button>. */
.pattern-slot {
  position: relative;
  flex: none;
  width: var(--pattern-item-w);
  height: var(--pattern-item-h);
}

.pattern-item {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border: none;
  border-radius: var(--radius-pill);
  background: none;
  font: inherit;
  cursor: pointer;
}

.pattern-thumb {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-pill);
  border: 1px solid var(--control-border);
  background: var(--card);
  object-fit: cover;
  transition: box-shadow 0.15s;
}

/* Ring drawn with box-shadow (2px card-colored gap, then 2px ring) so
   it follows the rounded corners and fits the scroller's 4px padding. */
.pattern-item:hover .pattern-thumb {
  box-shadow: 0 0 0 2px var(--popover), 0 0 0 4px var(--ring);
}

.pattern-item[aria-pressed='true'] .pattern-thumb {
  border-color: transparent;
  box-shadow: 0 0 0 2px var(--popover), 0 0 0 4px var(--foreground);
}

.pattern-item:focus-visible {
  outline: none;
}

.pattern-item:focus-visible .pattern-thumb {
  box-shadow: 0 0 0 2px var(--popover), 0 0 0 4px var(--ring), 0 0 0 7px var(--ring-50);
}

.pattern-item[aria-pressed='true']:focus-visible .pattern-thumb {
  box-shadow: 0 0 0 2px var(--popover), 0 0 0 4px var(--foreground), 0 0 0 7px var(--ring-50);
}

/* Age chip inside the pill's left end (the pin sits in the right end).
   White on black/65% stays ≥ 7:1 even over a white thumbnail (the chip
   composites to #595959 or darker); on the active pattern it inverts to
   black on white (21:1 / 14:1), which together with the white ring marks
   the selection. */
.pattern-time {
  position: absolute;
  top: 50%;
  left: 6px;
  transform: translateY(-50%);
  max-width: calc(100% - 32px);
  overflow: hidden;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.65);
  color: #fff;
  font-size: 11px;
  font-weight: 500;
  line-height: 14px;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
  transition: background-color 0.15s, color 0.15s;
}

.pattern-item[aria-pressed='true'] .pattern-time {
  background: var(--foreground);
  color: var(--primary-foreground);
}

/* Pin toggle, layered over the thumbnail's top-right corner. Hidden
   until the slot is hovered/focused, except once pinned it stays put so
   a pinned pattern is recognizable at a glance. --background at 78%
   opacity under the glyph clears the 3:1 bar against every thumbnail
   color, not just flat ones. */
/* WCAG 2.5.8 target-size: the button's own box (what axe-core and the
   browser's hit-testing both use) is the full >=24x24 CSS px target;
   the 20px (16px in the compact size below) circle stays its visual
   size via ::before, inset from that box. A ::before that only
   enlarges the box visually (e.g. via negative inset) does NOT work
   here: axe-core's target-size check — and a browser's actual hit
   target — is the interactive element's own boundingClientRect, not a
   pseudo-element that merely overflows it. */
.pattern-pin {
  position: absolute;
  top: 50%;
  right: 2px;
  margin-top: -12px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  background: none;
  /* Own stacking context, so the ::before disc (z-index: -1) stays
     between the thumbnail and the glyph instead of behind the thumbnail. */
  z-index: 1;
  color: var(--foreground);
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.1s;
}

.pattern-pin::before {
  content: '';
  position: absolute;
  inset: 2px; /* 24px - 2*2px = 20px */
  border-radius: 50%;
  background: rgba(10, 10, 10, 0.78); /* --background at 78% opacity */
  z-index: -1; /* paint behind the glyph text, not over it */
}

.pattern-slot:hover .pattern-pin,
.pattern-slot:focus-within .pattern-pin,
.pattern-pin[aria-pressed='true'] {
  opacity: 1;
}

/* ★ vs ☆ carries the state; no color change needed. */

.pattern-pin:disabled {
  cursor: not-allowed;
}

/* Compact size, shared by the container query (5 full-size items don't
   fit .canvas-wrap's own width below ~700px: measured overflow from
   ~1024px up to ~1700px of viewport width, where .canvas-wrap is the
   narrow column of a wider grid) and the one-column layout below. */
@container (max-width: 700px) {
  .pattern-bar {
    --pattern-item-w: 64px;
    --pattern-item-h: 40px;
    --pattern-gap: 6px;
    padding: 6px;
  }

  .pattern-new {
    padding: 0 12px;
  }

  .pattern-time {
    padding: 0 5px;
    font-size: 10px;
  }

  .pattern-pin {
    /* Box stays 24x24 (the target-size hit area); only the visible
       circle below shrinks to 16x16. */
    font-size: 10px;
  }

  .pattern-pin::before {
    inset: 4px; /* 24px - 2*4px = 16px */
  }
}

/* Too narrow for the label next to five items: keep just the ↻ glyph
   (the button's accessible name still comes from the hidden text). */
@container (max-width: 460px) {
  .pattern-new {
    width: var(--pattern-item-h);
    padding: 0;
  }

  .pattern-new-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
}

/* --- Export panel ------------------------------------------------------- */

.output {
  grid-area: output;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.output-settings {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* Sits under #formatHint as a link-styled action, not a full-width row. */
#copySettingsLinkBtn {
  align-self: flex-start;
}

.size-inputs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

/* Name inside the field on the left, number on the right — the same
   layout and type as the sliders' name/value. The pill boundary moves
   from the <input> to the wrapper. */
.input-group {
  display: flex;
  flex: 1 1 0;
  align-items: center;
  gap: 6px;
  min-width: 0;
  height: 40px;
  padding: 0 14px;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-pill);
  background: var(--secondary);
  box-shadow: var(--shadow-xs);
  cursor: text;
  transition: border-color 0.15s, box-shadow 0.15s;
}

.input-group:focus-within {
  border-color: var(--ring);
  box-shadow: var(--focus-ring);
}

.input-label {
  flex: none;
  color: var(--control-text);
  font-size: 13px;
  font-weight: 500;
}

.input-group .input {
  flex: 1 1 auto;
  min-width: 0;
  height: 100%;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  color: var(--control-text);
  font-size: 13px;
  font-weight: 600;
  text-align: right;
}

.input-group .input:focus-visible {
  outline: none;
  box-shadow: none;
}

/* ToggleGroup, variant "outline", size "sm", with spacing (separate
   buttons): 2 per row in the 332px column, 4 in one row when wider.
   Pressed = data-[state=on]: bg-accent text-accent-foreground. */
.size-presets {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
  gap: 6px;
  margin-top: 4px;
}

.size-preset-btn {
  min-width: 0;
  height: 32px;
  padding: 0 8px;
  background: transparent;
  color: var(--muted-foreground);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

.size-preset-btn:hover {
  color: var(--foreground);
}

.size-preset-btn[aria-pressed='true'] {
  background: var(--accent);
  color: var(--accent-foreground);
}

/* Checkbox: size-4 rounded-[4px] border-input bg-input/30 shadow-xs;
   checked: bg-primary with a primary-foreground check. */
.checkbox-field {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 24px;
  font-size: 14px;
  font-weight: 500;
  line-height: 1;
  cursor: pointer;
}

.checkbox-field input {
  -webkit-appearance: none;
  appearance: none;
  position: relative;
  display: grid;
  place-content: center;
  width: 16px;
  height: 16px;
  margin: 0;
  flex: none;
  border: 1px solid var(--control-border);
  border-radius: 4px;
  background: var(--input-30);
  box-shadow: var(--shadow-xs);
  cursor: pointer;
  transition: box-shadow 0.15s, background-color 0.15s;
}

.checkbox-field input::after {
  content: '';
  width: 8px;
  height: 4px;
  margin-top: -2px;
  border: solid var(--primary-foreground);
  border-width: 0 0 2px 2px;
  transform: rotate(-45deg);
  opacity: 0;
}

.checkbox-field input:checked {
  border-color: var(--primary);
  background: var(--primary);
}

.checkbox-field input:checked::after {
  opacity: 1;
}

.checkbox-field input:focus-visible {
  outline: none;
  border-color: var(--ring);
  box-shadow: var(--focus-ring);
}

/* Primary pill: bg-primary text-primary-foreground hover:bg-primary/90;
   just "Экспорт <format>", no summary line (the user's call). */
.export-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 48px;
  padding: 0 24px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  background: var(--primary);
  box-shadow: var(--shadow-xs);
  color: var(--primary-foreground);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.15s, box-shadow 0.15s;
}

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

.export-btn:focus-visible {
  outline: none;
  border-color: var(--ring);
  box-shadow: var(--focus-ring);
}

.export-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Progress: h-2 rounded-full bg-primary/20, indicator bg-primary. */
.progress {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: -8px;
  font-size: 13px;
  color: var(--muted-foreground);
  font-variant-numeric: tabular-nums;
}

.progress-bar {
  flex: 1;
  height: 8px;
  background: rgba(229, 229, 229, 0.2);
  border-radius: 999px;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  width: 0%;
  background: var(--primary);
  transition: width 0.1s linear;
}

.cancel-export-btn {
  flex: none;
  height: 32px;
  padding: 0 12px;
  font-size: 13px;
}

.status {
  margin: -8px 0 0;
  font-size: 14px;
  line-height: 1.43;
}

.status[data-kind='success'] {
  color: var(--foreground);
}

.status[data-kind='error'] {
  color: var(--destructive);
}

.status[data-kind='cancelled'] {
  color: var(--muted-foreground);
}

.output .hint {
  margin-top: -8px;
}

/* --- Gallery ------------------------------------------------------------ */

.gallery {
  grid-area: gallery;
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding-top: 8px;
  min-width: 0;
}

/* Section heading: text-lg font-semibold tracking-tight. */
.gallery h2 {
  margin: 0;
  font-size: 18px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.015em;
}

.gallery-scroll {
  position: relative;
}

/* Edge fades while cards are scrolled out of view on that side
   ("shadow-scroll" in the layout; the left one mirrors it). */
.gallery-scroll::before,
.gallery-scroll::after {
  content: '';
  position: absolute;
  /* -4px: .results below spills 4px past this box on every side (room
     for focus rings), so the fades must cover that strip too or cards
     show through at the very edge. */
  top: -4px;
  bottom: -4px;
  z-index: 1;
  width: min(174px, 15%); /* about one card */
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s;
}

.gallery-scroll::before {
  left: -4px;
  background: linear-gradient(to left, rgba(10, 10, 10, 0), var(--background));
}

.gallery-scroll::after {
  right: -4px;
  background: linear-gradient(to right, rgba(10, 10, 10, 0), var(--background));
}

.gallery-scroll.more-before::before,
.gallery-scroll.more-after::after {
  opacity: 1;
}

/* No visible scrollbar: the strip scrolls with the mouse wheel (main.js),
   trackpad, Shift+wheel and Tab through the download buttons. */
.results {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  scrollbar-width: none;
  /* Room for the download buttons' focus ring. */
  padding: 4px;
  margin: -4px;
}

.results::-webkit-scrollbar {
  display: none;
}

.result-card {
  flex: none;
  width: 184px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 12px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-sm);
}

.result-preview {
  position: relative;
  height: 104px;
  border-radius: 14px;
  overflow: hidden;
  background: #000;
}

.result-preview img,
.result-preview video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Badge, variant "default": rounded-full px-2 py-0.5 text-xs
   font-medium bg-primary text-primary-foreground (14:1). */
.result-badge {
  position: absolute;
  top: 6px;
  left: 6px;
  z-index: 1;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--primary);
  color: var(--primary-foreground);
  font-size: 12px;
  font-weight: 500;
  line-height: 16px;
}

.result-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 13px;
  min-width: 0;
}

.result-name {
  color: var(--muted-foreground);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.result-size {
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

.result-download {
  display: flex;
  height: 32px;
  padding: 0 12px;
  font-size: 13px;
  text-decoration: none;
}

/* --- Narrower screens --------------------------------------------------- */

/* Not enough room for three columns: export panel moves under the preview. */
@media (max-width: 1439px) {
  .layout {
    grid-template-columns: 568px minmax(0, 1fr);
    grid-template-areas:
      'controls preview'
      'controls output'
      'gallery  gallery';
    align-items: start;
    padding: 24px;
  }

  .canvas-wrap {
    height: 420px;
    min-height: 320px;
  }
}

/* Phones/tablets: one column, live preview pinned on top while you tweak
   the controls below it. */
@media (max-width: 1023px) {
  .layout {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      'preview'
      'controls'
      'output'
      'gallery';
    gap: 16px;
    padding: 12px;
  }

  .canvas-wrap {
    position: sticky;
    top: 0;
    z-index: 2;
    height: 34vh;
    min-height: 180px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
    /* Own height is short here (34vh, min 180px), so the strip uses the
       compact size (54px bar + 10px offset); this leaves an 8px+ gap
       above it instead of the canvas touching the strip. */
    --pattern-strip-h: 64px;
  }

  .pattern-bar {
    --pattern-item-w: 64px;
    --pattern-item-h: 40px;
    --pattern-gap: 6px;
    bottom: 10px;
    padding: 6px;
  }

  .pattern-new {
    padding: 0 12px;
  }

  .pattern-time {
    padding: 0 5px;
    font-size: 10px;
  }

  .pattern-pin {
    font-size: 10px;
  }

  .pattern-pin::before {
    inset: 4px;
  }
}

@media (max-width: 599px) {
  .controls-row,
  .grain-card {
    display: flex;
    flex-direction: column;
    align-items: stretch;
  }

  .grain-main,
  .grain-side {
    flex: none;
  }

  .grain-side {
    padding-top: 0;
  }

  .card {
    padding: 20px;
  }
}

/* --- Desktop only ------------------------------------------------------- */

/* Touch-first devices get this banner instead of the app (boot.js uses
   the same media query to skip loading main.js). */
.desktop-only {
  display: none;
}

@media (hover: none) and (pointer: coarse) {
  .layout {
    display: none;
  }

  .desktop-only {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    min-height: 100dvh;
    padding: 16px;
  }
}

.desktop-only-card {
  max-width: 360px;
  padding: 24px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-sm);
  text-align: center;
}

.desktop-only-card h2 {
  margin: 0 0 8px;
  font-size: 16px;
  font-weight: 600;
  line-height: 1.2;
}

.desktop-only-card p {
  margin: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted-foreground);
}
