:root { --ink: #f2efe4; --bg: #14181b; --muted: #8a9299; }

* { box-sizing: border-box; }

body {
  margin: 0;
  padding: 1.5rem 1rem 2rem;
  background: var(--bg);
  color: var(--ink);
  /* 1rem, а не 16px: прибитий піксель ігнорує піднятий базовий кегль
     браузера, і вся «тихіша» типографіка сторінки, задана в rem, тоді
     виростає, а основний текст — ні. Ієрархія читалася б навпаки. */
  font: 1rem/1.5 system-ui, sans-serif;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2rem;
  min-height: 100vh;
}

header { text-align: center; }
h1 { margin: 0; font-size: clamp(1.8rem, 5vw, 3rem); letter-spacing: 0.04em; }

/* Сцена й стрічка прикладів поруч. Стрічка вужча й фіксованої ширини:
   вона супровідна, і не має конкурувати за увагу з принтом на сцені. */
.layout {
  width: min(860px, 100%);
  display: grid;
  grid-template-columns: minmax(0, 1fr) 170px;
  gap: 2rem;
  align-items: start;
}

/* Явні координати, а не автопотік. У розмітці стрічка стоїть останньою —
   там її місце в порядку читання й табуляції, — а на екрані вона мусить
   бути праворуч від сцени, у першому ряду. Автопотік поклав би її під
   форму, у четвертий ряд.

   Кнопки й форма розтягнуті на обидва стовпці навмисно: у першому вони
   звузилися б до колонки сцени (658 px), хоч під стрічкою місце вільне.

   Без медіазапиту. На вузькому екрані .layout стає display: contents, діти
   переходять у флекс-контейнер body, а grid-column/grid-row на флекс-
   елементі не діють узагалі — тож ці чотири правила там просто мовчать. */
#stage    { grid-column: 1;      grid-row: 1; }
.examples { grid-column: 2;      grid-row: 1; }
.controls { grid-column: 1 / -1; grid-row: 2; }
.custom   { grid-column: 1 / -1; grid-row: 3; }

#stage { display: flex; justify-content: center; min-width: 0; }

/* min-width: 0 — інакше типове min-width: auto флекс-елемента тримає фігуру
   на ширині найдовшого слова в посиланні під принтом, і на 320 px футболка
   вилазить на два пікселі в поле сторінки. */
.print { margin: 0; text-align: center; min-width: 0; max-width: 100%; }

/* Принт має пропорцію 4500:5400, тож на повну ширину він виходить вищим
   за екран і виштовхує кнопки за межі видимого. Обмежуємо висотою вікна,
   а не шириною: керування має лишатися під рукою без прокручування. */
.print img {
  display: block;
  margin: 0 auto;
  max-width: 100%;
  max-height: 58vh;
  width: auto;
  height: auto;
}

.print figcaption { display: none; }

/* Ціль дотику, а не рядок тексту: підкреслене посилання заввишки 24 px —
   найдрібніша ціль сторінки, і це головна дія (принт під друк). */
.download {
  display: inline-block;
  margin-top: 1rem;
  padding: 0.5rem 0;
  color: var(--ink);
  text-decoration: underline;
}

/* Тихіша за головну: принт під друк — товар, футболка — картинка показати.
   Однаково гучні кнопки поруч змусили б вибирати там, де вибору немає. */
.download.secondary {
  margin-left: 1.25rem;
  color: var(--muted);
  font-size: 0.9rem;
}

.download:active { color: var(--muted); }

.pending, .failed, .empty { text-align: center; padding: 3rem 1rem; max-width: 100%; }

/* anywhere, а не break-word: у тексті відмови буває ідентифікатор запиту
   провайдера — суцільний токен на 50+ символів без жодного пробілу. Без
   розриву посеред слова він розпирає картку в обидва боки. */
.reason { color: var(--muted); font-size: 0.9rem; overflow-wrap: anywhere; }

.spinner {
  width: 2.5rem; height: 2.5rem; margin: 0 auto 1rem;
  border: 3px solid rgba(242, 239, 228, 0.2);
  border-top-color: var(--ink);
  border-radius: 50%;
  animation: spin 0.9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }

.controls { display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center; }

/* Один вигляд на всі дії-кнопки сторінки. Список селекторів, а не клас:
   кнопки в станових фрагментах (порожній каталог, відмова, ліміт) — така
   сама дія, як і в футері, і половина з них так само POST /generate. Без
   цього рядка вони діставали типову кнопку браузера: 21 px заввишки, тобто
   вдвічі менше за палець. */
.controls button,
.custom button,
.empty button,
.failed button {
  background: none;
  border: 1px solid var(--ink);
  color: var(--ink);
  padding: 0.7rem 1.4rem;
  font: inherit;
  cursor: pointer;
}
.empty button, .failed button { margin-top: 1rem; }

.controls button:hover,
.custom button:hover,
.empty button:hover,
.failed button:hover { background: var(--ink); color: var(--bg); }

/* :hover на телефоні не існує, тож без :active між дотиком і відповіддю
   сервера не міняється взагалі нічого — і людина тисне вдруге. Друге
   натискання по «Згенерувати» коштує грошей, тож кнопки ще й вимикають
   себе на час запиту (hx-disabled-elt у розмітці). */
.controls button:active,
.custom button:active,
.empty button:active,
.failed button:active,
.examples button:active,
.font:active { background: var(--ink); color: var(--bg); }

.swatch:active { border-color: var(--ink); transform: scale(0.92); }

/* Стан очікування видно на самій кнопці, а не лише на сцені: сцена внизу
   екрана могла бути поза полем зору в мить дотику.

   Тільки на кнопках. Клас htmx-request висить на елементі, що зробив запит,
   а картку очікування опитує вона сама — від загального правила вона
   блимала б напівпрозорою на кожному тику черги. */
button.htmx-request,
button[disabled] { opacity: 0.5; cursor: default; }

/* Стрічка чужих генерацій */
.examples h2 {
  margin: 0 0 0.75rem;
  font-size: 0.8rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
/* Стрічка прокручується всередині себе, а не тягне сторінку вниз: інакше
   п'ять портретних мініатюр роблять документ удвічі вищим за сцену й
   виштовхують кнопки керування за межі екрана. */
.examples ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
  max-height: 58vh;
  overflow-y: auto;
}

.examples button {
  display: block;
  width: 100%;
  padding: 0.5rem;
  background: none;
  border: 1px solid transparent;
  color: var(--muted);
  font: inherit;
  text-align: center;
  cursor: pointer;
}
.examples button:hover,
.examples button:focus-visible { border-color: var(--ink); color: var(--ink); }

/* Принт портретний, тож мініатюру обмежуємо висотою. На такому розмірі
   напис усередині принта вже не читається — саме тому під ним є підпис. */
.examples img {
  display: block;
  width: 100%;
  height: 88px;
  object-fit: contain;
}

.examples span {
  display: block;
  margin-top: 0.3rem;
  font-size: 0.72rem;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

/* Плашки кольорів футболки. Колір тканини приходить інлайновою змінною
   --swatch: список кольорів живе в Go, і дублювати його в CSS означало б
   тримати два джерела правди. */
.garments {
  display: flex;
  /* 1rem, а не 0.5rem: зони дотику плашок розширені на 8 px у кожен бік
     (::after нижче), і при меншому проміжку вони перекривалися б. Той самий
     проміжок між рядами розводить зони дотику й по вертикалі. */
  gap: 1rem;
  /* Перенос обов'язковий. Одинадцять плашок — це 11×28 + 10×16 = 468 px при
     343 px, які лишає екран 375 px. Без переносу justify-content: center
     виштовхує надлишок НА ОБИДВА боки: заміряно −118 px у першої плашки, і
     прокрутки туди немає — колір просто недосяжний. */
  flex-wrap: wrap;
  justify-content: center;
  list-style: none;
  padding: 0.75rem 0 0;
  margin: 0;
}

.swatch {
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  cursor: pointer;
  background: var(--swatch);
  border: 2px solid var(--muted);
  border-radius: 50%;
  position: relative;
}

/* Зона дотику більша за саму плашку. Псевдоелементом, а не розміром: сім
   плашок по 44 px із проміжками дають 380 px і не влазять у 343 px, які є
   на екрані 375 px — крайня плашка виїхала б за край, куди немає прокрутки.
   Так видимий розмір лишається 28 px, а палець влучає в 44 px. */
.swatch::after {
  content: "";
  position: absolute;
  inset: -8px;
  border-radius: 50%;
}

.swatch:hover { border-color: var(--ink); }
.swatch.selected { border-color: var(--ink); box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--ink); }
.swatch:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* Назва кольору лишається в розмітці для читачів екрана: сама плашка —
   лише колір, і без тексту вона для них порожня кнопка. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Перемикачі мови й абревіатури. Кожен веде на корінь свого варіанта —
   відповідника принта там немає, бо пари незалежні.

   У звичайному потоці, а не абсолютом: назви продукту різної довжини
   («Always Distracted», «Регулярно Дістають»), і абсолютне позиціювання
   неминуче наїжджало б на довшу з них. */
header { display: flex; flex-direction: column; align-items: center; }
/* flex-wrap обов'язковий, і це вимір, а не обачність. Третя плашка
   (крамниця) не влазить у 343 px разом із двома перемикачами: без переносу
   flex стискає всіх трьох, і підписи ламаються на два рядки — заміряно, усі
   три виростають із 35 px до 56 px. Тобто нова плашка псує дві наявні.

   З переносом крамниця йде на власний рядок, перемикачі лишаються
   однорядковими, а на широкому екрані всі три однаково стоять в один ряд:
   82 + 112 + 157 + проміжки ≈ 365 px при 860 px контейнера. */
.switchers {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.4rem;
  align-self: flex-end;
  margin-top: 0.25rem;
}
.lang {
  color: var(--muted); text-decoration: none; font-size: 0.85rem;
  border: 1px solid var(--muted); border-radius: 2px; padding: 0.4rem 0.6rem;
}
.lang:hover, .lang:focus-visible { color: var(--ink); border-color: var(--ink); }

/* Крамниця. Залита, а не в рамці: у шапці вона стоїть поруч із двома
   приглушеними перемикачами, і різниця кольору — єдине, що відрізняє
   «назовні» від «углиб сайту». Інверсія тими самими двома змінними, без
   третього кольору в палітрі. */
.shop {
  background: var(--ink);
  color: var(--bg);
  border-color: var(--ink);
  text-decoration: none;
}
.shop:hover, .shop:focus-visible { color: var(--bg); }

/* Рядок під кнопками завантаження. Той самий вигляд, що й у шапці, але не
   .lang: там плашка живе у флекс-ряді перемикачів, тут — сама, і потребує
   власних відступів. */
.shop-line { margin: 1.25rem 0 0; text-align: center; }
.shop-line .shop {
  display: inline-block;
  border: 1px solid var(--ink);
  border-radius: 2px;
  padding: 0.5rem 0.9rem;
  font-size: 0.9rem;
}

/* Плашки шрифтів і розкладок. Текстові, а не кольорові: ні шрифт, ні
   розкладка не мають кольору, тож показуємо назву — і вона ж пояснює
   вигляд. Обидва ряди однакові, тож і правило одне: без нього .layouts
   лишався б типовим <ul> — з маркерами-крапками й відступом 40 px. */
.fonts, .layouts {
  display: flex; gap: 0.4rem; justify-content: center; flex-wrap: wrap;
  list-style: none; padding: 0.75rem 0 0; margin: 0;
}
.font {
  background: none; border: 1px solid var(--muted); color: var(--muted);
  padding: 0.35rem 0.7rem; cursor: pointer; font: inherit; font-size: 0.8rem;
}
.font:hover { color: var(--ink); border-color: var(--ink); }
.font.selected { color: var(--bg); background: var(--ink); border-color: var(--ink); }
.font:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* Власна пара. Поля без власних правил дістають типові стилі браузера, а в
   Safari це font-size 13.33 px — і iOS при фокусі збільшує всю сторінку,
   щоб текст став читабельним, назад не повертаючи. Успадкований 1rem знімає
   це повністю: зум спрацьовує лише нижче 16 px. */
.custom {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  justify-content: center;
  width: min(860px, 100%);
}
.custom input {
  /* 8rem, а не ширше, і це вимір, а не смак.

     flex-grow розтягує елементи ОСТАННЬОГО рядка на всю його ширину — у
     flexbox це не обходиться нічим. Поки полів було чотири, наслідку не
     видно: кнопка не input, правило її не чіпає, тож вона лишалася на
     другому рядку сама й своєї природної ширини. П'яте поле для РДУГ
     переїхало туди ж — і роздулося до 724 px проти 209 px у сусідів згори.

     Базис 8rem = 128 px рятує тим, що ВСІ шість елементів влазять в один
     рядок: 5×128 + 128 (кнопка) + 5×8 (проміжки) = 808 ≤ 860. А коли
     контейнер вужчий і кнопка все ж переноситься, п'ять полів однаково
     ділять цілий рядок порівну.

     Заміряно на 1280 px у контейнерах 860 / 800 / 760 / 721 px (нижче 720
     працює стовпчик із блоку в кінці файлу). Відношення найширшого поля до
     найвужчого: 8rem → 1.00 на кожній ширині; 9rem → 4.57 на 721;
     10rem → 5.19 на 800; 12rem → 5.66 на 860.

     Підказки вміщуються: у полі 138 px під текст лишається 112 px, а
     найширша підказка «Асиметричні…» — 110 px, і вона на /asd/uk/, де
     чотири поля дістають по 175 px. */
  flex: 1 1 8rem;
  min-width: 0;
  background: none;
  border: 1px solid var(--muted);
  color: var(--ink);
  font: inherit;
  padding: 0.6rem 0.75rem;
}
.custom input::placeholder { color: var(--muted); }
.custom input:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }
/* Тихо: підтримка не має конкурувати з кнопками генерації. */
.support {
  margin: 2rem 0 1rem;
  text-align: center;
  font-size: 0.85rem;
}
.support a { color: var(--muted); }

/* Перелік друкарень. Тон той самий, що в підтримки: це підказка, а не дія —
   дія тут кнопка завантаження, і сперечатися з нею перелік не має.

   Посилання через flex-wrap, а не комами в тексті: на 343 px три назви в
   один рядок не влазять, а кома, що лишилася наприкінці рядка, читається
   як обрив. */
.shops {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: baseline;
  gap: 0.35rem 0.75rem;
  margin: 1rem 0 0;
  font-size: 0.85rem;
}
.shops-label { color: var(--muted); }
.shops a { color: var(--muted); }

/* ─── Вузький екран ───────────────────────────────────────────────────────
   Блок стоїть ОСТАННІМ у файлі, і це не оформлення. Медіазапит не додає
   специфічності, тож будь-яке правило нижче за ним перебиває його при
   однаковому селекторі. Коли цей блок лежав у середині файлу, мобільні
   .custom і .font мовчки програвали своїм же базовим правилам знизу.
   Нове правило для вузького екрана — сюди, і нічого після цього блоку. */

@media (max-width: 720px) {
  /* Сітка розчиняється, і всі її діти стають прямими дітьми body у своєму
     порядку з розмітки — сцена, кнопки, форма, стрічка. Це вже потрібний
     порядок на екрані, тож жодного order тут немає й бути не повинно:
     order переставив би малювання, лишивши табуляцію та читача екрана в
     розмітці, і візуальний порядок розійшовся б із фокусним.

     Ролі не втрачаються: display: contents прибирає з дерева лише сам
     <div class="layout">, у якого ролі немає, а <main>, <footer>, <form>
     і <aside> лишаються власними коробками. */
  .layout { display: contents; }

  /* 1.5rem замість 2rem. Чотири проміжки по 0.5rem — це 32 px, і саме їх
     бракує, щоб кнопка «Ще один» стала вище згину на екрані 375×812. */
  body { gap: 1.5rem; }

  /* min-width: 0 обов'язковий. Типове min-width: auto у флекс-елемента
     дорівнює його min-content, а min-content стрічки — це п'ять колонок
     по 108 px, тобто 572 px. Саме вони розпирали документ до 588 px на
     будь-якому екрані вужчому за 604 px і виносили футболку за правий
     край. Із нулем стрічка прокручується всередині себе, як і задумано. */
  #stage, .examples { width: 100%; min-width: 0; }

  .examples ul {
    grid-auto-flow: column;
    grid-auto-columns: 108px;
    max-height: none;
    overflow-x: auto;
    overflow-y: hidden;
    padding-bottom: 0.5rem;
  }

  /* Два посилання в рядок на 343 px злипаються в одну смугу тексту, у якій
     не видно, де кінчається одне й починається інше. */
  .download { display: block; padding: 0.75rem 0; }
  .download.secondary { margin-left: 0; }

  /* Пальцем, а не мишею: 28 px заввишки — половина того, у що надійно
     влучають. Плашки й так у flex-wrap, тож зростання нічого не ламає. */
  .font { min-height: 2.75rem; padding: 0.5rem 0.9rem; }

  /* width/height у розмітці самі по собі коробку НЕ резервують: поки ширину
     задає auto, браузер не має від чого рахувати висоту ненавантаженої
     картинки, і на кожній підміні сцена схлопується на 338 px, а потім
     стрибає назад. Виміряно: CLS 0.42 на одну зміну кольору.

     Із width: 100% пропорція з атрибутів дає висоту одразу. object-fit
     страхує від спотворення на низькому екрані, де max-height стисне
     коробку, а ширина лишиться заданою. */
  .print img { width: 100%; object-fit: contain; }

  /* Самі ряди притискаються ближче один до одного рівно настільки, скільки
     вкрали вищі плашки. Відступ між рядами — порожнеча, а кнопка внизу —
     дія: на 812 px висоти вони борються за ті самі пікселі. */
  .fonts, .layouts, .garments { padding-top: 0.5rem; }

  /* Стовпчиком: два поля по 12rem поруч не влазять у 343 px і однаково
     переносяться, але з різною шириною — рвана драбинка замість форми.

     flex: 0 0 auto обов'язковий разом зі зміною напрямку: у стовпчику
     flex-basis 12rem із правила вище стає ВИСОТОЮ, і кожне поле роздувалося
     б до 192 px. */
  .custom { flex-direction: column; align-items: stretch; }
  .custom input { flex: 0 0 auto; }

  /* Друкарні пальцем: заміряно 53×20 px, тобто менше за 28 px, які вище вже
     визнано половиною надійного влучання. Плашки шрифтів дістали 44 px саме
     тому, і це посилання заслуговує того самого — на ньому людина йде
     платити, це не виноска.

     inline-block обов'язковий: у рядкового елемента вертикальні відступи
     не збільшують коробку, тож площа дотику лишилася б тією самою 20 px,
     хоч на око стало б просторіше.

     Підпис не чіпаємо — він текст, а не ціль. */
  .shops { gap: 0 1rem; }
  .shops a { display: inline-block; padding: 0.7rem 0; }
}
