/* AC-STEP designer dashboard — plain modern CSS, no framework, no build step.
   Palette taken from ac-step.com (amber #FFBC00 on near-black #1A1A1A). */

:root {
  --accent: #ffbc00;
  --accent-hover: #e5a900;
  --sidebar-bg: #1a1a1a;
  --sidebar-text: #b9b6b6;
  --sidebar-active-text: #1a1a1a; /* near-black ON amber, not white */
  --bg: #f3f1f2;
  --surface: #ffffff;
  --border: #e1e1e1;
  --text: #1a1a1a;
  --text-muted: #707070;
  --danger: #b72f2f;
  --radius: 6px;
  --shadow: 0 1px 2px rgba(0, 0, 0, .05), 0 4px 16px rgba(0, 0, 0, .05);
  /* The bar that carries the menu button on a phone; nothing above 720px has
     one, but the height is what the layout below it is measured against. */
  --topbar-h: 56px;
  /* Acrom / Museo Sans Cyr are the site's licensed faces; they are not shipped
     here, so the stack degrades to a system geometric sans. Drop the woff2 in
     /static and add an @font-face to activate them — nothing else changes. */
  --font: "Acrom", "Museo Sans Cyr", -apple-system, BlinkMacSystemFont, "Segoe UI",
    Roboto, "Helvetica Neue", Arial, sans-serif;
}

* { box-sizing: border-box; }

/* `hidden` has to win over the display of a class (the busy overlay is shipped
   hidden and unhidden by static/app.js). */
[hidden] { display: none !important; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.45;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  /* A model id or a URL in a hint has no space to break at; on a phone one of
     them would push the whole page sideways rather than wrap. */
  overflow-wrap: break-word;
  /* Landscape iOS enlarges text on its own, which breaks a measured layout. */
  -webkit-text-size-adjust: 100%;
}

/* Nothing may be wider than what holds it — the sized images below all set
   their own width, so this only ever catches one that does not. */
img { max-width: 100%; }

/* Text only a screen reader reaches. Not `hidden`: it has to be announced. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---- layout: fixed dark sidebar + fluid content ---- */

/* `dvh` after `vh` throughout: on a phone `100vh` is the window with the address
   bar rolled away, so a page sized in it is taller than what is on screen and
   gains a scrollbar that scrolls nothing. Older browsers keep the first one. */
.layout { display: flex; min-height: 100vh; min-height: 100dvh; }

/* The menu drawer of narrow screens; from 721px up it is simply the sidebar,
   and the checkbox and the bar that opens it are switched off (below). */
.topbar, .nav-scrim { display: none; }
.nav-state { display: none; }

.sidebar {
  position: sticky;
  top: 0;
  align-self: flex-start;
  display: flex;
  flex-direction: column;
  width: 236px;
  min-width: 236px;
  height: 100vh;
  height: 100dvh;
  /* A short window (a phone on its side) would otherwise cut off the bottom of
     the menu — the language switch, the address and Log out — for good. */
  overflow-y: auto;
  padding: 22px 14px 16px;
  background: var(--sidebar-bg);
  color: var(--sidebar-text);
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 8px 24px;
  text-decoration: none;
}

.brand-mark {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  flex: none;
  border-radius: var(--radius);
  background: var(--accent);
  color: #1a1a1a;
  font-weight: 800;
  font-size: 15px;
  letter-spacing: .5px;
}

.brand-name {
  display: flex;
  flex-direction: column;
  color: #fff;
  font-weight: 700;
  letter-spacing: .6px;
  line-height: 1.15;
}

.brand-name small {
  color: var(--sidebar-text);
  font-weight: 400;
  font-size: 11px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
}

.nav { display: flex; flex-direction: column; gap: 2px; flex: 1; }

/* A caption over a run of nav items — what that part of the sidebar is about,
   and the whole of the separation between two runs (no rule, no divider).
   `:not(:first-child)` is what opens that gap; the first caption sits flush
   under the brand, which already has 24px under it. */
.nav-caption {
  margin: 0;
  padding: 0 12px 2px;
  color: rgba(255, 255, 255, .35);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.2px;
  text-transform: uppercase;
}

.nav-caption:not(:first-child) { margin-top: 32px; }

.nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  border-radius: var(--radius);
  color: var(--sidebar-text);
  text-decoration: none;
  transition: background .15s, color .15s;
}

.nav-item:hover { background: rgba(255, 255, 255, .07); color: #fff; }

.nav-item.is-active {
  background: var(--accent);
  color: var(--sidebar-active-text);
  font-weight: 600;
}

.nav-icon { width: 16px; height: 16px; flex: none; }

/* ---- collapsible nav group (<details>/<summary>, no JS) ---- */

.nav-parent { cursor: pointer; user-select: none; list-style: none; }
.nav-parent::-webkit-details-marker { display: none; } /* Safari's default triangle */

/* The parent never carries the amber active state — that belongs to the child
   page. It only brightens to show which branch the current page sits in. */
.nav-group[open] > .nav-parent { color: #fff; }

.nav-chevron {
  width: 14px;
  height: 14px;
  flex: none;
  margin-left: auto;
  opacity: .6;
  transition: transform .15s;
}

.nav-group[open] > .nav-parent .nav-chevron { transform: rotate(90deg); }

.nav-sub { display: flex; flex-direction: column; gap: 2px; margin-top: 2px; }

.nav-sub-item { padding-left: 26px; }

.sidebar-footer {
  border-top: 1px solid rgba(255, 255, 255, .1);
  padding-top: 14px;
}

/* ---- language switcher ---- */

/* A segmented control made of two submit buttons, so it still switches with
   JavaScript disabled. */
.lang-switch {
  display: flex;
  gap: 2px;
  padding: 2px;
  margin-bottom: 12px;
  border-radius: var(--radius);
  background: rgba(255, 255, 255, .07);
}

.lang-option {
  flex: 1;
  padding: 6px 0;
  border: 0;
  border-radius: calc(var(--radius) - 2px);
  background: none;
  color: var(--sidebar-text);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .4px;
  cursor: pointer;
  transition: background .15s, color .15s;
}

.lang-option:hover { color: #fff; }

.lang-option.is-active {
  background: rgba(255, 255, 255, .14);
  color: #fff;
  cursor: default;
}

/* The login card is on a light background, so the sidebar palette does not
   carry over. */
.login-card .lang-switch {
  margin: 22px 0 0;
  background: rgba(0, 0, 0, .05);
}

.login-card .lang-option { color: var(--text-muted); }
.login-card .lang-option:hover { color: var(--text); }

.login-card .lang-option.is-active {
  background: #fff;
  color: var(--text);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .12);
}

.user-email {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  color: var(--sidebar-text);
  margin: 0 4px 10px;
}

.content { flex: 1; min-width: 0; padding: 30px 36px 64px; }

.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 24px;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--border);
}

.page-header h1 { margin: 0; font-size: 24px; font-weight: 700; letter-spacing: -.2px; }
.page-actions { display: flex; flex-wrap: wrap; gap: 10px; }

/* ---- buttons ---- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 9px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none; /* the same class is used on <a> links */
  cursor: pointer;
  transition: background .15s, border-color .15s, color .15s;
}

.btn:hover { border-color: #c9c6c6; }

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #1a1a1a;
}

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

.btn-ghost {
  background: transparent;
  border-color: rgba(255, 255, 255, .18);
  color: var(--sidebar-text);
}

.btn-ghost:hover { background: rgba(255, 255, 255, .07); border-color: rgba(255, 255, 255, .3); color: #fff; }

.btn-block { width: 100%; }

/* ---- panels & empty state ---- */

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 22px;
}

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 6px;
  padding: 60px 24px;
  color: var(--text-muted);
}

.empty-state svg { width: 48px; height: 48px; color: #c9c6c6; margin-bottom: 8px; }
.empty-state h2 { margin: 0; font-size: 17px; font-weight: 600; color: var(--text); }
.empty-state p { margin: 0; max-width: 44ch; }

/* ---- forms ---- */

.field { display: block; margin-bottom: 14px; }

.field > span {
  display: block;
  margin-bottom: 6px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
}

.field input {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
}

.field input:focus,
.field textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(255, 188, 0, .22);
}

/* Prompts: text meant for a machine, and long. Monospace and a fixed line
   height keep a paragraph readable; only the height is resizable, so dragging
   one cannot break the panel it sits in. */
.field textarea {
  display: block;
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  line-height: 1.55;
  resize: vertical;
}

/* The base prompt is read-only; it says so by looking like a quotation. */
.field textarea[readonly] { background: #f7f6f6; color: var(--text-muted); }

.alert {
  padding: 10px 12px;
  border-radius: var(--radius);
  margin-bottom: 16px;
  font-size: 14px;
}

.alert-error { background: #fbeaea; border: 1px solid #eecccc; color: var(--danger); }
/* Not an error — something the designer has not filled in yet (API Keys). */
.alert-warning { background: #fff6e0; border: 1px solid #f0dba8; color: #7a5900; }
/* Neither: an answer to a form that went through (AI Generation). */
.alert-info { background: #eef3fb; border: 1px solid #cfdcf0; color: #2b4a7a; }
/* The set page's checklist of what a generation is still missing. */
.alert-list { margin: 6px 0 0; padding-left: 20px; }
.alert-list li { margin-top: 3px; }

/* ---- section content: cards, chips, tables, media grids ---- */

.icon { width: 16px; height: 16px; flex: none; }

/* ---- breadcrumbs ---- */

.crumbs {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-bottom: 12px;
  font-size: 13px;
  color: var(--text-muted);
}

.crumbs a { color: var(--text-muted); text-decoration: none; }
.crumbs a:hover { color: var(--text); text-decoration: underline; }
.crumb-sep { width: 12px; height: 12px; flex: none; opacity: .55; }

.crumb-current {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
  font-weight: 600;
}

.panel + .panel, .panel-fold + .panel { margin-top: 18px; }
.panel > .empty-state { padding: 40px 24px; }
.panel-form { max-width: 620px; }

.hint { margin: 10px 0 0; font-size: 13px; color: var(--text-muted); }
.hint a { color: inherit; }
/* A hint that explains the input above it, so it sits inside the <label> (as a
   span: a <label> takes phrasing content only). `.field > span` styles every
   direct span as a caption, so the weight and the margins are undone here. */
.field .hint { display: block; margin: 6px 0 0; font-weight: 400; }

/* Collapsible panel — same no-JS <details> trick as the sidebar group. */
.panel-fold { padding: 0; }

.panel-fold > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 16px 22px;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}

.panel-fold > summary::-webkit-details-marker { display: none; }

.panel-fold > summary::after {
  content: "";
  width: 8px;
  height: 8px;
  margin-left: auto;
  border-right: 1.5px solid var(--text-muted);
  border-bottom: 1.5px solid var(--text-muted);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .15s;
}

.panel-fold[open] > summary { border-bottom: 1px solid var(--border); }
.panel-fold[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.fold-body { padding: 22px; }

/* ---- chips ---- */

.chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.chips-row { margin-bottom: 20px; }

.chip {
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--accent);
  color: #1a1a1a;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
}

.chip-soft { background: #ecebeb; color: var(--text-muted); }
.chip-count { background: transparent; border: 1px solid var(--border); color: var(--text-muted); }
.chip-empty { background: transparent; color: var(--text-muted); font-weight: 400; }
/* A set that is short of something a generation needs (dashboard/readiness.py).
   The alert-warning palette, so the card and the set page read as one thing. */
.chip-warning {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: #fff6e0;
  border: 1px solid #f0dba8;
  color: #7a5900;
}
/* The weaker of the two: the set generates, some of its soles do not. The
   alert-info palette for the same reason — and because a blue note beside an
   amber warning is the hierarchy of the two at a glance. */
.chip-info {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: #eef3fb;
  border: 1px solid #cfdcf0;
  color: #2b4a7a;
}
.chip-warning .icon,
.chip-info .icon { width: 13px; height: 13px; }

/* ---- content cards: sole sets and collections ---- */

/* `min(260px, 100%)` rather than a bare 260px in every grid below: a track with
   a fixed floor cannot become narrower than that floor, so on a phone the grid
   is wider than the screen and the whole page scrolls sideways. Wrapped in
   `min()` the floor gives way at the width where it stops fitting, and the grid
   drops to one column by itself. */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
  gap: 18px;
}

.set-card {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  color: inherit;
  text-decoration: none;
  overflow: hidden;
  transition: border-color .15s, transform .15s;
}

.set-card:hover { border-color: var(--accent); transform: translateY(-2px); }

/* A flex strip rather than a grid: one to four previews always fill the width
   evenly, so a set with three soles leaves no empty cell. */
.set-thumbs {
  display: flex;
  gap: 1px;
  height: 148px;
  overflow: hidden;
  background: var(--border);
  border-bottom: 1px solid var(--border);
}

.set-thumbs img {
  flex: 1;
  min-width: 0;
  height: 100%;
  object-fit: cover;
  background: #fafafa;
}

/* Soles and AI Generation show sole views, which are wide: cropping them to a
   square-ish cell cuts the profile that the set is picked by. Whole image, the
   strip keeps its height so the cards stay aligned. Collection photos are shoe
   shots and stay cropped. */
.set-thumbs-fit img {
  object-fit: contain;
  padding: 6px;
}

.set-thumbs-empty {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: #fafafa;
  color: var(--text-muted);
  font-size: 13px;
}

.set-body { display: flex; flex-direction: column; gap: 8px; padding: 14px 16px 16px; }
.set-body h3 { margin: 0; font-size: 16px; font-weight: 700; }

/* ---- toe part: the one image a set carries itself ---- */

.panel-title { margin: 0 0 16px; font-size: 15px; font-weight: 700; }

.toe-row { display: flex; align-items: flex-start; gap: 18px; }

.toe-image {
  width: 168px;
  height: 120px;
  flex: none;
  object-fit: contain;
  background: #fafafa;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.toe-image.is-empty {
  display: grid;
  place-items: center;
  border-style: dashed;
  color: #c9c6c6;
}

.toe-image.is-empty .icon { width: 28px; height: 28px; }
.toe-row .upload { flex: 1; min-width: 0; }

/* ---- banner: the one photo the mini-app's home screen stands on ---- */

/* Its own block rather than the toe part's: a banner is judged by looking at
   it, so the preview is as wide as the panel affords instead of a thumbnail. */
.banner-row { display: flex; align-items: flex-start; gap: 18px; }

.banner-preview {
  width: 340px;
  flex: none;
  /* The shipped photo's ratio, so an image of another shape is letterboxed
     rather than resizing the page around it. */
  aspect-ratio: 700 / 437;
  object-fit: contain;
  background: #fafafa;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.banner-preview.is-empty {
  display: grid;
  place-items: center;
  border-style: dashed;
  color: #c9c6c6;
}

.banner-preview.is-empty .icon { width: 28px; height: 28px; }
.banner-row .upload { flex: 1; min-width: 0; }

/* ---- colors ---- */

.color-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(308px, 100%), 1fr));
  align-items: start; /* an open edit fold must not stretch its neighbours */
  gap: 16px;
}

.color-card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

/* The two views sit side by side: that pair is what identifies the color. */
.color-views { display: flex; gap: 1px; background: var(--border); }
.color-views figure { flex: 1; min-width: 0; margin: 0; background: var(--surface); }

.color-views img {
  display: block;
  width: 100%;
  height: 132px;
  object-fit: contain;
  background: #fafafa;
}

.color-views figcaption {
  padding: 5px 8px;
  border-top: 1px solid var(--border);
  font-size: 11px;
  letter-spacing: .3px;
  text-transform: uppercase;
  color: var(--text-muted);
}

.color-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 12px 14px;
  border-top: 1px solid var(--border);
}

.color-head strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.color-edit > summary {
  padding: 10px 14px;
  border-top: 1px solid var(--border);
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
  cursor: pointer;
  list-style: none;
}

.color-edit > summary::-webkit-details-marker { display: none; }
.color-edit > summary:hover { color: var(--text); }
.color-edit[open] > summary { color: var(--text); }
.color-edit-body { padding: 4px 14px 16px; border-top: 1px solid var(--border); }
.color-edit-body .form-actions { margin-top: 14px; }
/* A card column is too narrow for two file inputs side by side. */
.color-edit-body .field-row + .field-row { grid-template-columns: 1fr; }
.color-delete { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }

/* ---- collection gallery: the example photos a collection holds ---- */

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(150px, 100%), 1fr));
  gap: 14px;
}

.gallery figure {
  position: relative;
  margin: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.gallery img {
  display: block;
  width: 100%;
  height: 140px;
  object-fit: contain;
  background: #fafafa;
}

/* The delete button sits on the image; its own surface keeps it legible on a
   dark photo. The hover colors come from .icon-danger below. */
.gallery form { position: absolute; top: 6px; right: 6px; }
.gallery .icon-btn { background: rgba(255, 255, 255, .92); border-color: var(--border); }

/* ---- materials & colors: two curated lists side by side ---- */

/* auto-fit rather than a media query: the two panels drop under each other as
   soon as they no longer both fit, whatever the sidebar leaves of the width.
   The `min()` is what makes that true below 340px as well — see .card-grid. */
.pair-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
  align-items: start; /* an open fold must not stretch the other list */
  gap: 18px;
}

/* `.panel + .panel` spaces stacked panels; in this grid the gap already does. */
.pair-grid > .panel + .panel { margin-top: 0; }

/* Panels holding a prompt editor: they sit side by side like any other pair,
   but drop under each other earlier — a long text needs a wider column than a
   list of names, and on a laptop screen both still fit. */
.pair-grid-wide { grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr)); }

/* The same wide column, with the grid's empty tracks kept. `auto-fit` collapses
   them and stretches what is left, which is right for a page that is one run of
   panels and wrong for the Texts page, whose three runs are 2, 1 and 5 panels
   long: stretched, a run of one would be a full-width card around a 620px form.
   `auto-fill` keeps one column width down the whole page and lets a short run
   simply end early. */
.pair-grid-fill { grid-template-columns: repeat(auto-fill, minmax(min(420px, 100%), 1fr)); }

/* A run of panels under a heading of its own — the Texts page is three of them.
   The heading is the whole of the separation (no rule, no divider), the same way
   `.nav-caption` separates the sidebar's runs, and it sits between the page's h1
   and the panels' own titles so the hierarchy reads down. */
.panel-group + .panel-group { margin-top: 34px; }
.panel-group-title { margin: 0 0 6px; font-size: 17px; font-weight: 700; }
/* Above a grid rather than above a form, so it needs less air under it than the
   lead of a panel does. */
.panel-group > .hint-lead { margin-bottom: 14px; }

.option-list { margin-top: 22px; border-top: 1px solid var(--border); }
.option-list > .hint { margin: 14px 0 0; }
.option-item { border-bottom: 1px solid var(--border); }

/* The row itself is the summary, so the whole line toggles its edit form. */
.option-item > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 11px 2px;
  cursor: pointer;
  list-style: none;
}

/* The chevron is shared with the model rows of the API Keys section and the
   prompt rows of the Prompt one. */
.option-item > summary::-webkit-details-marker,
.model-fold > summary::-webkit-details-marker,
.prompt-fold > summary::-webkit-details-marker { display: none; }
.option-item > summary:hover .option-name { color: var(--accent-hover); }

.option-item > summary::after,
.model-fold > summary::after,
.prompt-fold > summary::after {
  content: "";
  width: 7px;
  height: 7px;
  flex: none;
  margin-left: auto;
  border-right: 1.5px solid var(--text-muted);
  border-bottom: 1.5px solid var(--text-muted);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .15s;
}

.option-item[open] > summary::after,
.model-fold[open] > summary::after,
.prompt-fold[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }

.option-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}

/* The hex code of a palette color, so the list reads as colors and not only as
   words. Its background is an inline style because the color is data; only a
   code the view accepted (views/materials.py::HEX) ever gets there. */
.swatch {
  flex: none;
  width: 14px;
  height: 14px;
  border-radius: 4px;
  border: 1px solid var(--border);
}

.option-body { padding: 6px 2px 20px; }
.option-delete { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); }

/* ---- API keys: one panel per AI ---- */

.panel-head { display: flex; align-items: center; gap: 10px; margin-bottom: 18px; }
.panel-head .panel-title { margin: 0; }

/* The AI the generation runs on is marked, not just labelled. */
.panel.is-active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), var(--shadow); }

.hint-lead { margin: 0 0 18px; }
.hint-danger { color: var(--danger); }

/* `min(…, 100%)` on the floor, for the same reason the grids carry one: inside
   a phone-width panel a fixed 220px would stick out of it. */
.field-key { flex: 1; min-width: min(220px, 100%); margin-bottom: 0; }

/* What is known about the stored key sits between the label and the row that
   follows it — the field, or the buttons that ask for the field back — so both
   states of the panel are the same height and the Save button stays level with
   the input. */
.field .key-note { margin: 0 0 6px; }

/* Edit and Delete, side by side where the field would be. */
.key-actions { display: flex; flex-wrap: wrap; gap: 12px; }

/* Only the ends of a key ever reach the screen (views/ai.py::masked_key), and
   they read as an identifier rather than as words: monospace, and never split
   over two lines — a key broken at its dash looks like two of something. */
.field-key code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  color: var(--text);
  white-space: nowrap;
}

.sub-title {
  margin: 26px 0 0;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .4px;
  text-transform: uppercase;
  color: var(--text-muted);
}

.model-list { margin-top: 10px; border-top: 1px solid var(--border); }
.model-list > .hint { margin: 12px 0 0; }

.model-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  border-bottom: 1px solid var(--border);
}

/* A one-button form rather than an <input type="checkbox">: a real checkbox
   would need a submit button of its own to work with JavaScript disabled. */
.model-check {
  display: grid;
  place-items: center;
  width: 19px;
  height: 19px;
  flex: none;
  margin-top: 10px;
  padding: 0;
  border: 1.5px solid var(--border);
  border-radius: 4px;
  background: var(--surface);
  color: transparent; /* the tick is there, invisible, until it is checked */
  cursor: pointer;
  transition: background .15s, border-color .15s, color .15s;
}

.model-check:hover { border-color: var(--accent); color: #d8d5d5; }
/* The chosen prompt: already what its AI generates with, so there is nothing
   left to press. Not dimmed — it is the marked state, not an unavailable one. */
.model-check:disabled { cursor: default; }
.model-check .icon { width: 12px; height: 12px; stroke-width: 2.4; }

.is-selected .model-check {
  background: var(--accent);
  border-color: var(--accent);
  color: #1a1a1a;
}

.model-fold { flex: 1; min-width: 0; }

.model-fold > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 2px;
  cursor: pointer;
  list-style: none;
}

/* Model names are API identifiers, not words — monospace keeps them readable. */
.model-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
}

.is-selected .model-name { font-weight: 700; }
.model-body { padding: 2px 2px 16px; }
.model-body .inline-form input[type="text"] { font-size: 13px; }
.model-delete { margin-top: 12px; }
.model-add { margin-top: 16px; }
.panel-foot { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--border); }

/* ---- prompts: one list per AI, one of them in use ---- */

.prompt-list { margin-top: 4px; border-top: 1px solid var(--border); }
.prompt-list > .hint { margin: 12px 0 0; }
/* The markers a refinement comment is put between, under every refinement
   prompt: an identifier to recognise in the text, so monospace like a key. */
.prompt-markers code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  color: var(--text);
}

.prompt-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  border-bottom: 1px solid var(--border);
}

.prompt-fold { flex: 1; min-width: 0; }

.prompt-fold > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 11px 2px;
  cursor: pointer;
  list-style: none;
}

.prompt-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}

.is-selected .prompt-name { color: var(--text); }
.prompt-fold > summary:hover .prompt-name { color: var(--accent-hover); }
.prompt-body { padding: 2px 2px 18px; }

/* The copy link and the delete button sit on one line under the text. */
.prompt-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

/* An <a> styled as a button needs the line-height a <button> brings itself. */
.prompt-actions .btn { text-decoration: none; line-height: 1.2; }

/* The add form is a fold inside the panel, not a panel of its own. */
.prompt-add { margin-top: 18px; border: 1px solid var(--border); border-radius: var(--radius); }
.prompt-add > summary { padding: 12px 16px; font-size: 14px; }
.prompt-add .fold-body { padding: 4px 16px 18px; }

/* ---- AI Generation: the sole picker and the answer to a checked form ---- */

/* Cards of one set's colors: the picture is the control, and the whole card is
   the click target. It is an <a> rather than a <label> around a radio — the
   palette below the form is this sole's own, so picking one is a page of its
   own (?color=) and the choice rides in a hidden field. */
.pick-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(150px, 100%), 1fr));
  gap: 12px;
}

.pick {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: border-color .15s, box-shadow .15s;
}

.pick:hover { border-color: #c9c6c6; }
.pick.is-picked { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

.pick img { width: 100%; height: 110px; object-fit: cover; background: #f4f2f2; }

.pick-name {
  padding: 8px 10px;
  font-size: 13px;
  font-weight: 600;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* A question about the list above it (how strictly the colors are meant), so it
   sits under that list and its caption is lighter than the one it belongs to. */
.fieldset-sub { margin: 14px 0 0; }
.fieldset-sub legend { font-weight: 400; }

/* A collection with no example photos cannot be generated from: the option is
   still listed, so that it is clear which one is missing what. */
.check.is-disabled { opacity: .55; cursor: not-allowed; }
.check.is-disabled:hover { border-color: var(--border); }

/* The pictures that came back. Two at most, side by side while they fit. */
.result-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  gap: 14px;
}

.result-grid figure { margin: 0; }

.result-grid img {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #f7f6f6;
}

.result-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 16px 0 12px;
  font-size: 13px;
  color: var(--text-muted);
}

/* What a refinement was asked with, where a generation shows its chips. */
.result-comment { margin: 0; font-size: 14px; }

/* The comment box under one picture: what to change about that one. It sits
   inside the <figure>, so it is as wide as the picture it belongs to. */
.refine { margin-top: 10px; }
.refine .field { margin-bottom: 6px; }
.refine .checks { margin: 10px 0; }

/* The wait itself, revealed by static/app.js while the press is in flight. A
   sheet over the whole window rather than a <dialog>: it must not be closable,
   since closing it would only hide a generation that is already being paid
   for, and it has to be visible from wherever the page is scrolled to. */
.busy {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  padding: 24px;
  background: rgba(26, 26, 26, .55);
}

.busy-card {
  display: grid;
  justify-items: center;
  width: 100%;
  max-width: 320px;
  padding: 26px 24px 24px;
  border-radius: 10px;
  text-align: center;
  background: var(--surface);
  box-shadow: 0 18px 50px rgba(0, 0, 0, .35);
}

.busy-title { margin: 14px 0 0; font-size: 17px; font-weight: 700; }
.busy .hint { margin: 4px 0 0; }

.spinner {
  width: 34px;
  height: 34px;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .8s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* Slowed rather than stopped: a spinner that does not turn says nothing, and
   this is the only thing on screen saying the press is still alive. */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation-duration: 2.4s; }
}

/* ---- icon buttons ---- */

.icon-btn {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  flex: none;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 4px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: background .15s, color .15s, border-color .15s;
}

.icon-btn:hover { background: #f1efef; border-color: var(--border); color: var(--text); }
.icon-danger:hover { background: #fbeaea; border-color: #eecccc; color: var(--danger); }

/* ---- forms: fieldsets, checkboxes, uploads, tables ---- */

.fieldset { margin: 0 0 16px; padding: 0; border: 0; }

.fieldset legend {
  padding: 0;
  margin-bottom: 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
}

.checks { display: flex; flex-wrap: wrap; gap: 10px; }

.check {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  user-select: none;
}

.check:hover { border-color: #c9c6c6; }
.check input { accent-color: var(--accent); width: 16px; height: 16px; margin: 0; }
.check:has(input:checked) { border-color: var(--accent); background: #fff9e8; }

/* Set by static/app.js when a name is already taken. */
.field input.is-invalid { border-color: var(--danger); }
.field input.is-invalid:focus { box-shadow: 0 0 0 3px rgba(183, 47, 47, .18); }

/* static/app.js inserts it as a direct child of the label, where `.field > span`
   would hand it a caption's bottom margin. Those 6px are invisible while it is
   empty but still make the label taller than the input, which pushes the button
   of a `.upload` row below it. Reset first, so the margin below applies. */
.field .field-error { margin: 0; }

.field-error:not(:empty) {
  display: block;
  margin-top: 6px;
  font-size: 13px;
  color: var(--danger);
}

.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-primary:disabled:hover { background: var(--accent); border-color: var(--accent); }

.form-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 20px; }

.btn-danger { background: var(--surface); border-color: #eecccc; color: var(--danger); }
.btn-danger:hover { background: #fbeaea; border-color: var(--danger); }

.danger-zone {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 26px;
  padding-top: 18px;
  border-top: 1px solid var(--border);
}

.danger-zone .hint { margin-top: 2px; }

.upload { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; }
.field-file { flex: 1; min-width: min(240px, 100%); margin-bottom: 0; }

/* Two fields on one line (name + site id, full view + side view). */
.field-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  margin-bottom: 14px;
}

.field-row .field { margin-bottom: 0; }
.field-row .field-file { min-width: 0; } /* the grid column already sizes it */

.field-file input[type="file"] {
  width: 100%;
  padding: 8px 10px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  font: inherit;
  font-size: 13px;
}

.field-file input[type="file"]::file-selector-button {
  margin-right: 10px;
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: #f7f6f6;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.inline-form { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }

/* `1 1 160px` rather than `flex: 1; min-width: 0`: squeezed, the field would go
   to nothing beside its Save button instead of sending the button one line
   down, which is what there is room for on a phone. */
.inline-form input[type="text"] {
  flex: 1 1 160px;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
}

.inline-form input[type="text"]:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(255, 188, 0, .22);
}

.table { width: 100%; border-collapse: collapse; }

.table th {
  padding: 0 8px 10px;
  text-align: left;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .4px;
  text-transform: uppercase;
  color: var(--text-muted);
}

.table td { padding: 6px 8px; border-top: 1px solid var(--border); }
.table .num { width: 100px; text-align: right; color: var(--text-muted); }
.table .row-actions { width: 48px; text-align: right; }
.table .row-actions form { display: inline-flex; }

/* ---- confirmation dialog (native <dialog>, centered and Esc-closable) ---- */

.modal {
  width: calc(100% - 32px);
  max-width: 420px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text);
}

.modal::backdrop { background: rgba(26, 26, 26, .55); }

.modal-card {
  padding: 24px;
  border-radius: 10px;
  background: var(--surface);
  box-shadow: 0 18px 50px rgba(0, 0, 0, .35);
}

.modal-title { margin: 0 0 8px; font-size: 17px; font-weight: 700; }
.modal-text { margin: 0; color: var(--text-muted); }
.modal-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; margin-top: 22px; }

.modal[open] { animation: modal-in .14s ease-out; }
.modal[open]::backdrop { animation: backdrop-in .14s ease-out; }

@keyframes modal-in {
  from { opacity: 0; transform: translateY(-8px); }
}

@keyframes backdrop-in {
  from { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .modal[open], .modal[open]::backdrop { animation: none; }
}

/* ---- login ---- */

.login-body {
  display: grid;
  place-items: center;
  min-height: 100vh;
  min-height: 100dvh;
  padding: 24px;
  background: var(--sidebar-bg);
}

.login-card {
  width: 100%;
  max-width: 380px;
  /* 32px off both sides of a 320px phone is a third of the card. */
  padding: clamp(20px, 6vw, 32px);
  background: var(--surface);
  border-radius: 10px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, .35);
}

.brand-login { justify-content: center; padding-bottom: 22px; }
.brand-login .brand-name { color: var(--text); }
.brand-login .brand-name small { color: var(--text-muted); }

.login-card h1 {
  margin: 0 0 20px;
  font-size: 20px;
  font-weight: 700;
  text-align: center;
}

/* ---- tablets: the same layout, on a sidebar that takes less of it ---- */

/* 236px out of an iPad's 768 is a third of the screen spent on seven links. */
@media (min-width: 721px) and (max-width: 1080px) {
  .sidebar { width: 200px; min-width: 200px; padding: 18px 10px 14px; }
  .brand { padding: 0 4px 20px; }
  .content { padding: 26px 24px 56px; }
}

/* ---- phones: the sidebar becomes a drawer behind a menu button ---- */

/* Laid out as a strip above the page, it was the brand, seven links, an open
   submenu, the language switch, an address and Log out — a screenful before the
   heading. It is the same sidebar, moved off the left edge and brought back by
   #nav-open, the checkbox base.html renders ahead of everything. No script is
   involved, so this works with JavaScript off and cannot flash the strip while
   one loads. */
@media (max-width: 720px) {
  /* Hidden but still focusable — `display: none` would take the menu away from
     anyone using the keyboard. */
  .nav-state {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
  }

  .topbar {
    position: sticky; /* the button stays reachable down a long list */
    top: 0;
    /* Over the drawer and the sheet both: the button that opened the menu is
       the cross that shuts it, so it has to stay pressable. */
    z-index: 45;
    display: flex;
    align-items: center;
    gap: 6px;
    height: var(--topbar-h);
    padding: 0 12px;
    background: var(--sidebar-bg);
  }

  .brand-top { padding: 0; }
  .brand-top .brand-mark { width: 32px; height: 32px; font-size: 13px; }

  .nav-toggle {
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    flex: none;
    border-radius: var(--radius);
    color: var(--sidebar-text);
    cursor: pointer;
  }

  .nav-toggle .icon { width: 22px; height: 22px; }
  /* One button showing what pressing it will do: bars, then the cross. */
  .nav-toggle .icon-close { display: none; }
  .nav-state:checked ~ .topbar .nav-toggle .icon-open { display: none; }
  .nav-state:checked ~ .topbar .nav-toggle .icon-close { display: block; }
  /* The checkbox holds the focus ring; the label is what is on screen. */
  .nav-state:focus-visible ~ .topbar .nav-toggle {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
  }

  .layout { min-height: calc(100vh - var(--topbar-h)); min-height: calc(100dvh - var(--topbar-h)); }

  /* The bar carries the brand up here, so the drawer opens straight onto the
     links instead of repeating it. */
  .sidebar .brand { display: none; }

  .sidebar {
    position: fixed;
    top: var(--topbar-h);
    left: 0;
    z-index: 40;
    width: min(82vw, 296px);
    min-width: 0;
    /* Spelled out rather than left to `top: 0; bottom: 0`: the sidebar carries
       `align-self: flex-start` for the desktop layout, and a flex container's
       alignment still reaches a positioned child — with it, the drawer stops at
       the last link instead of running the height of the screen. */
    height: calc(100vh - var(--topbar-h));
    height: calc(100dvh - var(--topbar-h));
    transform: translateX(-100%);
    /* Off-screen is not gone: without this, Tab walks into a menu nobody can
       see. The delay holds it visible for the length of the slide out, and the
       open state below drops the delay so it is there for the slide in. */
    visibility: hidden;
    transition: transform .2s ease-out, visibility 0s linear .2s;
    box-shadow: 0 0 40px rgba(0, 0, 0, .5);
  }

  .nav-state:checked ~ .layout .sidebar {
    transform: none;
    visibility: visible;
    transition: transform .2s ease-out, visibility 0s;
  }

  .nav-scrim {
    position: fixed;
    inset: 0;
    z-index: 35;
    background: rgba(26, 26, 26, .5);
  }

  .nav-state:checked ~ .layout .nav-scrim { display: block; }
  /* What is under the drawer must not scroll away behind it. */
  body:has(.nav-state:checked) { overflow: hidden; }

  .nav-item { padding: 11px 12px; }
  .lang-option { padding: 9px 0; }
  .content { padding: 22px 18px 48px; }

  .page-header { flex-wrap: wrap; }
  .card-grid { grid-template-columns: 1fr; }
  .color-grid { grid-template-columns: 1fr; }
  .field-row { grid-template-columns: 1fr; }
  .toe-row { flex-direction: column; }
  /* A card is the full width here, so the toe part can be seen rather than
     squeezed into the 120px strip the two-column row left it. */
  .toe-image { width: 100%; height: 180px; }
  .banner-row { flex-direction: column; }
  .banner-preview { width: 100%; }
  .danger-zone { flex-direction: column; align-items: stretch; }
  .upload { align-items: stretch; }

  /* Four soles across a 324px card leave ~80px each, and a sole is a wide
     shape: two of them are the same strip with something visible in it. */
  .set-thumbs img:nth-child(n+3) { display: none; }
  /* A panel takes 22px off each side of an already narrow screen, which leaves
     these two grids one column and a half — so they show one, in a cell twice
     as wide as the picture standing in it. A lower floor gives back the pair. */
  .pick-grid,
  .gallery { grid-template-columns: repeat(auto-fill, minmax(min(120px, 100%), 1fr)); }

  /* Under 16px iOS zooms the page in on focus and does not come back out. */
  .field input,
  .field textarea,
  .inline-form input[type="text"],
  .model-body .inline-form input[type="text"],
  .field-file input[type="file"] { font-size: 16px; }

  /* Fingers, not a pointer. */
  .btn { padding: 11px 16px; }
  .icon-btn { width: 36px; height: 36px; }
  .model-check { width: 22px; height: 22px; }

  /* On one line these are cut off after a few characters — “Базовий про…”.
     Width is what is short on a phone, not lines. */
  .option-name,
  .model-name,
  .prompt-name { overflow: visible; white-space: normal; }
}

/* A tap leaves :hover behind until the next one lands somewhere else, so the
   lift a pointer gets would stay stuck on the card last pressed. */
@media (hover: none) {
  .set-card:hover { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .sidebar { transition: none; }
}
