:root {
  --bg: #fff6e4;
  --surface: #ffffff;
  --surface-2: #f7e7c4;
  --nav: #241a10;
  --text: #2b1b0e;
  --text-muted: #7a5b40;
  --border: #e9d3a0;
  --accent: #f0790e;
  --accent-light: #ffb35c;
  --accent-dark: #d9650a;
  --accent-deep: #b8500a;
  --green: #3f7d3a;
  --danger: #b3261e;
  --shadow: 0 2px 0 var(--accent-deep), 0 12px 26px -12px rgba(184, 80, 10, 0.45);
}

* {
  box-sizing: border-box;
}

html,
body,
#root {
  height: 100%;
}

body {
  margin: 0;
  font-family: 'Nunito Sans', -apple-system, sans-serif;
  color: var(--text);
  background: var(--bg);
}

h1,
h2,
h3 {
  font-family: 'Fredoka', sans-serif;
  font-weight: 600;
  margin: 0;
}

p {
  margin: 0;
}

a {
  color: var(--accent-dark);
  text-decoration: none;
}

button,
input,
select {
  font-family: inherit;
}

.mono {
  font-family: 'IBM Plex Mono', monospace;
}

.kicker {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  font-weight: 600;
}

.btn-primary {
  background: linear-gradient(180deg, var(--accent-light) 0%, var(--accent) 55%, var(--accent-dark) 100%);
  border: 1px solid var(--accent-dark);
  color: #fff;
  font-family: 'Fredoka', sans-serif;
  font-weight: 600;
  text-transform: uppercase;
  /* Heavy capitals need tracking. At 0.03em Fredoka's kerning tucked V so far under R that "CARVING" smudged;
     turning kerning off instead left a hole between T and A, since kerning is what closes the triangle under
     T's crossbar. Kerning stays on and the letters get room: 0.12em was the most even of 0.03 to 0.15 when
     compared side by side at real size - enough that nothing touches, not so much it stops reading as a word. */
  letter-spacing: 0.12em;
  border-radius: 11px;
  padding: 0.9em 1.7em;
  font-size: 0.95rem;
  box-shadow: var(--shadow);
  cursor: pointer;
}

.btn-primary:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.btn-secondary {
  background: var(--surface);
  border: 1.5px solid var(--border);
  color: var(--text);
  font-weight: 700;
  border-radius: 11px;
  padding: 0.85em 1.4em;
  font-size: 0.95rem;
  cursor: pointer;
}

.btn-text {
  background: transparent;
  border: none;
  color: var(--accent-dark);
  font-weight: 700;
  font-size: 0.88rem;
  cursor: pointer;
  padding: 0;
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 16px;
}

.page-shell {
  min-height: 100%;
  display: flex;
  flex-direction: column;
}

.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 22px 56px;
  background: var(--nav);
}

.nav a {
  color: var(--accent-light);
  font-weight: 700;
  font-size: 0.94rem;
  margin-right: 32px;
}

.nav-logo {
  display: flex;
  align-items: center;
  gap: 10px;
  color: #fbf1d9;
  font-family: 'Fredoka', sans-serif;
  font-weight: 700;
  font-size: 1.2rem;
  text-shadow: 0 0 14px rgba(240, 121, 14, 0.55);
}

.nav-logo span {
  color: var(--accent-light);
}

.avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--accent-dark);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 0.85rem;
}

.container {
  max-width: 1100px;
  margin: 0 auto;
  width: 100%;
  padding: 48px 24px 64px;
}

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

/* The label text is a <span class="field-label"> inside <label class="field">, so
   styling `.field label` matched the wrapper rather than the text. */
.field-label {
  font-weight: 700;
  font-size: 0.9rem;
}

/* input and textarea must be listed together: a textarea left out falls back to the
   browser's own border colour and square corners, which is why the comment box did
   not match the two fields above it. */
.field input,
.field textarea {
  border: 1.5px solid var(--border);
  border-radius: 9px;
  padding: 0.75em 1em;
  font-size: 1rem;
  font-family: inherit;
  line-height: 1.5;
  background: var(--surface);
  color: var(--text);
}

.field input:focus,
.field textarea:focus {
  outline: 2px solid var(--accent-light);
  outline-offset: 1px;
  border-color: var(--accent);
}

.field textarea {
  resize: vertical;
  min-height: 8em;
}

/* This page has no subtitle under the heading, so the gap the subtitle used to
   provide has to come from somewhere. Scoped here rather than on .page-title,
   which every other page relies on. */
.contact-page .page-title {
  margin-bottom: 28px;
}

.contact-page .form-section {
  gap: 20px;
}

.error-banner {
  background: #fdecea;
  border: 1px solid var(--danger);
  color: var(--danger);
  border-radius: 10px;
  padding: 12px 16px;
  font-size: 0.9rem;
}

.pill-btn {
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-radius: 9px;
  padding: 0.7em 1.3em;
  font-size: 0.92rem;
  font-weight: 700;
  color: var(--text);
  cursor: pointer;
}

.pill-btn.selected {
  background: var(--surface-2);
  border-color: var(--accent);
  color: var(--accent-dark);
}

.complexity-card {
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-radius: 14px;
  padding: 22px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  flex: 1;
  cursor: pointer;
  text-align: left;
}

.complexity-card.selected {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--surface-2);
}

.stat-tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 16px 18px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 1;
}

.stat-num {
  font-family: 'IBM Plex Mono', monospace;
  font-variant-numeric: tabular-nums;
  font-size: 1.6rem;
  font-weight: 500;
  color: var(--accent-dark);
}

/* ---------------------------------------------------------------------------
   Server-rendered pages (Thymeleaf + htmx)

   Everything below styles the markup in src/main/resources/templates. The rules
   above came from the earlier React app and are shared.
   --------------------------------------------------------------------------- */

.nav-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  color: #fbf1d9;
  font-family: 'Fredoka', sans-serif;
  font-weight: 700;
  font-size: 1.2rem;
  margin-right: 0;
  text-shadow: 0 0 14px rgba(240, 121, 14, 0.55);
}

/* On the home page the logo is not a link, so it should not invite a click. */
.nav-brand:not([href]) { cursor: default; }

.nav-brand strong { color: var(--accent-light); }
.nav-brand-mark { font-size: 1.3rem; }

.nav-links {
  display: flex;
  align-items: center;
  gap: 4px;
}

.nav-logout { margin: 0; }
.nav-logout .btn-text { color: var(--accent-light); }

.container.narrow { max-width: 520px; }

.page-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 32px;
}

.page-title {
  font-size: 2rem;
  color: var(--accent-dark);
}

.page-subtitle {
  color: var(--text-muted);
  font-weight: 600;
  margin-top: 6px;
}

.form-card {
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 28px;
  margin-top: 24px;
}

/* The code step is one box: the form, then the way to ask for another code under it. */
.stacked-form { display: flex; flex-direction: column; gap: 20px; }
.resend-form { text-align: center; }

.form-card input {
  padding: 11px 13px;
  border: 1px solid var(--border);
  border-radius: 9px;
  font: inherit;
  background: var(--surface);
}

.form-note {
  color: var(--text-muted);
  font-weight: 600;
}

.code-input {
  font-size: 1.5rem;
  letter-spacing: 0.4em;
  text-align: center;
}

.empty-state {
  padding: 40px;
  text-align: center;
  color: var(--text-muted);
}

.stencil-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 24px;
}

.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 20px;
  margin-top: 32px;
}

.pagination-status {
  color: var(--text-muted);
  font-weight: 700;
  font-size: 0.92rem;
}

.stencil-card {
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* Just the stencil, on its pumpkin. */
.stencil-thumbs {
  display: grid;
  grid-template-columns: 1fr;
  height: 220px;
}

.thumb {
  background: var(--surface-2);
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
}

.thumb + .thumb { border-left: 1px solid var(--border); }

.thumb img {
  max-width: 90%;
  max-height: 90%;
  object-fit: contain;
  border-radius: 12px;
}

.thumb-label {
  position: absolute;
  left: 8px;
  bottom: 6px;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 0.65rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  background: rgba(255, 250, 240, 0.82);
  padding: 1px 6px;
  border-radius: 4px;
}

/* The body fills the card and the View button sits at its foot, so every card's button lines up. */
.stencil-card-body {
  padding: 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  flex: 1;
}
.stencil-card-body > :last-child { margin-top: auto; }

.stencil-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}

/* Always two lines tall -- a longer name ends in an ellipsis, the full name on hover -- so cards in different rows match too. */
.stencil-name {
  font-weight: 700;
  word-break: break-word;
  line-height: 1.35;
  min-height: 2.7em;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.btn-secondary.full {
  text-align: center;
  margin-top: 6px;
}

/* Confirmation dialog ------------------------------------------------------ */

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(31, 20, 8, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  z-index: 50;
}

.modal {
  padding: 28px;
  max-width: 420px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.modal-head {
  display: flex;
  gap: 16px;
  align-items: center;
}

.modal-thumb {
  width: 64px;
  height: 64px;
  object-fit: contain;
  background: var(--surface-2);
  border-radius: 8px;
}

.modal-title {
  font-size: 1.2rem;
  color: var(--accent-dark);
}

.modal-filename {
  color: var(--text-muted);
  font-weight: 600;
  font-size: 0.9rem;
  word-break: break-word;
}

.modal-body {
  color: var(--text-muted);
  font-weight: 600;
  font-size: 0.9rem;
  line-height: 1.5;
}

.modal-actions {
  display: flex;
  gap: 12px;
  justify-content: flex-end;
}

/* Home ---------------------------------------------------------------------- */

/* The headline sits as far below the header as the image sits below it. */
.hero {
  display: flex;
  flex-direction: column;
  gap: 24px;
  padding-top: 12px;
  max-width: 1280px;
}

/* 80% of the page width, centred; a phone keeps the full width, where the picture is small already. */
.hero-image { width: 80%; height: auto; border-radius: 16px; display: block; }
@media (max-width: 600px) {
  .hero-image { width: 100%; }
}

/* Centres the picture, which is narrower than the hero. The caption is no longer in here: it speaks for
   both pictures, so it sits between them as the hero's own child, evenly spaced from each. */
.hero-figure { margin: 0; display: flex; flex-direction: column; align-items: center; }
/* One line of small print does not need the hero's full gap on each side; pulling both in by the same
   amount keeps it centred between the two pictures. */
.hero-caption { font-size: 0.85rem; color: var(--text-muted); text-align: center; margin-block: -8px; }

.hero-copy {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
}

.hero-title {
  text-align: center;
  font-size: clamp(1.4rem, 3.4vw, 2.6rem);
  line-height: 1.1;
  color: var(--accent-dark);
  white-space: nowrap;
}

.hero-text {
  font-size: 1.1rem;
  color: var(--text-muted);
  font-weight: 600;
}


/* The headline does not wrap on a desktop, so a box sized to its content is exactly one headline wide --
   which is then the measure the paragraph under it wraps to. `gap: inherit` takes the hero's own gap, so
   the two stay spaced like every other row, on a phone as well. */
.hero-pitch {
  align-self: center;
  /* min-content, not fit-content: fit-content grows to the space available whenever a child's natural line
     is longer than that, and the paragraph's is, so the box became the whole hero and the paragraph came
     out wider than the headline. A box's min-content is the widest of its children's, the headline cannot
     break (white-space: nowrap) so its min-content is the whole line, and the paragraph's is one word --
     which makes the box exactly one headline wide, whatever the window. */
  width: min-content;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  gap: inherit;
}

/* Left, not centred: it runs to two lines on a desktop and five on a phone, and a centred block leaves the
   eye hunting for the start of each one. The box is one headline wide, so this edge is flush with the
   headline's first letter. */
.hero-lede { text-align: left; line-height: 1.45; }

/* On a phone the whole pitch has to fit above the fold, button included, so the hero closes up:
   a smaller headline and tighter gaps, which together save about 60px. */
@media (max-width: 600px) {
  .hero-title {
    white-space: normal;
    font-size: 1.6rem;
  }

  /* The headline wraps here, so its min-content is one word and the box would collapse to it. */
  .hero-pitch { width: auto; }

  /* The headline drops to 1.6rem on a phone but the lede did not follow, so body copy came out nearly as
     loud as the headline it explains. Back to the ratio the desktop has. */
  .hero-lede { font-size: 1rem; }

  .hero { gap: 16px; padding-top: 4px; }
  .hero-copy { gap: 16px; }
}

/* The two-tone beta banner. Half the height of the hero picture's own 80%, so it reads as a note beside
   the pitch rather than a second hero; the strip is 4:1, so the height follows the width. */
.two-tone-image { display: block; width: 40%; height: auto; border-radius: 12px; }
/* A phone is narrow enough that 40% would be unreadable; the strip is already short there. */
@media (max-width: 600px) {
  .two-tone-image { width: 80%; }
}

.section-head {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 32px;
}

.section-title { font-size: 1.7rem; color: var(--accent-dark); }

.step-grid,
.example-grid {
  display: grid;
  gap: 24px;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}

.example-grid { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }

.step-card { padding: 28px; display: flex; flex-direction: column; gap: 12px; }

.step-number {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
}

.step-title { font-size: 1.05rem; }
.step-text { color: var(--text-muted); font-weight: 600; font-size: 0.92rem; line-height: 1.5; }

.band { background: var(--surface-2); padding: 8px 0 48px; }

.example-card { padding: 18px; display: flex; flex-direction: column; gap: 8px; }
.example-title { font-weight: 700; }
.example-meta { font-size: 0.82rem; color: var(--text-muted); }

.site-footer {
  background: var(--nav);
  color: #c9b896;
  padding: 28px 56px;
  font-weight: 600;
  font-size: 0.85rem;
}

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

.preview-layout {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
  gap: 32px;
}

@media (max-width: 820px) {
  .preview-layout { grid-template-columns: 1fr; }
}

.panel { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
.panel-head { padding: 16px 20px; border-bottom: 1px solid var(--border); }

.panel-body {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 18px;
  background: var(--surface-2);
  min-height: 160px;
}

.panel-body.large { padding: 40px; min-height: 320px; }
.panel-body img { max-width: 100%; max-height: 200px; }
.panel-body.large img { max-height: 420px; }

.preview-side { display: flex; flex-direction: column; gap: 24px; }
.download-block { display: flex; flex-direction: column; gap: 12px; }

.download-note {
  font-size: 0.8rem;
  color: var(--text-muted);
  text-align: center;
  font-weight: 600;
}

.btn-primary.full,
.btn-secondary.full { display: block; text-align: center; }

/* Upload -------------------------------------------------------------------- */

/* 33 rather than the 40 used inside the form below, so it reads as the same space.
   This gap sits under a line of text, which has no edge: the glyphs stop 3px above
   the paragraph's line box, and an unbounded gap looks wider than one closed off by
   a border. The boxes below bound their gaps on both sides, so they keep the 40. */
.upload-page { max-width: 720px; display: flex; flex-direction: column; gap: 33px; }
.upload-form { display: flex; flex-direction: column; gap: 40px; }
.form-section { display: flex; flex-direction: column; gap: 14px; }

/* Which kind of image this is -- asked, because measuring it could not be made to work.
   Laid out as two cards rather than bare radios so the choice reads as part of the form
   and not as a question tacked onto it. Neither is preselected: the submit button stays
   disabled until one is picked, so nobody can post without answering. */
/* .form-section and .form-footer both set display:flex, which outranks the hidden
   attribute -- so hiding them from JavaScript does nothing without this. Scoped to the two
   blocks the upload form reveals in turn, rather than made global, so it cannot surprise
   anything else carrying the attribute. Lost once already: it sat immediately above
   .kind-choice and was cut when that block was rewritten, which put the submit button back
   on screen before a kind had been chosen. */
#kind-section[hidden],
#form-footer[hidden] { display: none !important; }

/* The invisible reCAPTCHA draws nothing here -- its badge is positioned by Google, and we
   hide that anyway -- but the div is still a flex child of the form, so it was claiming a
   40px gap either side of itself: eighty pixels of nothing between the last question and
   the button. Out of flow, so the form closes up around it. */
#recaptcha.g-recaptcha,
#feedback-recaptcha.g-recaptcha {
    position: absolute;
    width: 0;
    height: 0;
    overflow: hidden;
}

.kind-choice {
    padding: 14px 16px;
    border: 1.5px solid var(--border, #d8d3cb);
    border-radius: 12px;
}
/* The box is .kind-choice, a div around the fieldset -- see upload.html. A legend is drawn on
   its fieldset's top border, so with the border on the fieldset a question wrapping to two
   lines had the border running through it. The old fix floated the legend, which the spec
   says takes it off the border; Safari disagreed. With no border here there is nothing to
   cross, in any browser, and the legend is simply the first line in the box. */
.kind-fieldset {
    border: 0;
    margin: 0;
    padding: 0;
    min-width: 0;
}
.kind-legend {
    display: block;
    padding: 0;
    margin: 0 0 10px;
    font-size: 0.95rem;
    font-weight: 700;
    color: var(--text);
}
/* Was the fieldset's flex gap. A flex fieldset is what made the float unreliable, so the
   spacing is plain margins now: the same 8px between options. */
.kind-option + .kind-option { margin-top: 8px; }
.kind-option {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 2px 0;
    cursor: pointer;
}
.kind-option input { width: 18px; height: 18px; flex: none; accent-color: var(--accent, #d4622a); }
/* The same brown the rest of the page's quieter copy uses, so the choice reads as part of
   the page's own voice rather than as a form control bolted on. */
.kind-text { color: var(--text-muted); font-size: 0.95rem; }
.kind-title { display: block; font-weight: 700; color: var(--text); }
/* The arrow reads as "becomes", the same relationship the two thumbnails show. Kept light so
   the two names either side of it carry the line. */
.kind-to { color: var(--text-muted); font-weight: 400; }
.kind-desc { display: block; margin-top: 1px; }

/* A worked example beside each choice: the same picture before and after, so the
   difference between an outline, a drawn shape and a tonal portrait is shown rather
   than described. Pushed to the trailing edge so the wording still reads as a list. */
.kind-option { gap: 10px; }
.kind-option .kind-text { flex: 1 1 auto; }
.kind-example {
    display: flex;
    align-items: center;
    gap: 5px;
    flex: none;
    margin-left: auto;
}
.kind-thumb {
    width: 104px;
    height: 104px;
    border-radius: 8px;
    border: 1px solid var(--border, #d8d3cb);
    background: #fff;
    object-fit: cover;
    display: block;
}
.kind-becomes {
    color: var(--text-muted);
    font-size: 0.95rem;
    line-height: 1;
}
/* Narrow screens: a grid instead of wrapping flex. Wrapping put the radio on a line of its
   own above its own label, because the text claimed the full width and pushed it out. The
   grid pins the radio beside the wording and drops the examples underneath, aligned to it.
   Thumbnails shrink too: three options with guidance already push the submit button well
   down a phone, which is what the old layout was arranged to avoid. */
@media (max-width: 480px) {
    /* Each option is three rows tall here -- name, what it is best for, the example -- so the
       8px that separated two single-line rows put one option's thumbnails against the next
       option's name. They need to read as three blocks, not one list. */
    .kind-choice {
        padding: 18px 16px 20px;
    }
    /* The heading sits close to the first option; it is the options that need air between
       them, so the space goes there rather than on every child of the box. 24px: what the
       8px gap and a 16px margin used to add up to. */
    .kind-option + .kind-option { margin-top: 24px; }
    .kind-option {
        display: grid;
        grid-template-columns: 18px 1fr;
        grid-template-areas: "radio text" ". example";
        column-gap: 10px;
        row-gap: 8px;
        align-items: start;
    }
    .kind-option input { grid-area: radio; margin-top: 2px; }
    .kind-option .kind-text { grid-area: text; }
    .kind-example { grid-area: example; margin-left: 0; }
    .kind-thumb { width: 76px; height: 76px; }

    /* A phone has ~140px less room than the form wants, so the submit button lands off
       the bottom of the screen and choosing an option looks like it did nothing. The
       script scrolls the button up; these keep that scroll short. The 40/33 rhythm is a
       desktop measure -- at this width the boxes are already hard against the screen
       edges, which separates them on its own. */
    .upload-page { gap: 22px; }
    .upload-form { gap: 26px; }
    .container { padding: 28px 16px 40px; }
}


.form-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; }
@media (max-width: 640px) { .form-columns { grid-template-columns: 1fr; } }

.dropzone {
  padding: 18px;
  display: block;
  cursor: pointer;
  border: 1px solid var(--border);
}

/* Empty: one dashed invitation with a single instruction. */
.dropzone:has(#dropzone-empty:not([hidden])) {
  border: 2px dashed var(--border);
  background: var(--surface);
}

.dropzone.is-dragging {
  border: 2px dashed var(--accent);
  background: var(--surface-2);
}

.dropzone-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 26px 18px;
  text-align: center;
}

.dropzone-icon {
  width: 34px;
  height: 34px;
  fill: none;
  stroke: var(--text-muted);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.dropzone-lead {
  margin: 0;
  font-family: 'Fredoka', sans-serif;
  font-size: 1.05rem;
  color: var(--text);
}

.dropzone-hint { margin: 0; font-size: 0.85rem; color: var(--text-muted); font-weight: 600; }

/* Chosen: a compact row saying which picture is going up. */
.dropzone-chosen {
  display: flex;
  align-items: center;
  gap: 18px;
}

/* Both halves lay themselves out with flex, which would otherwise beat [hidden]. */
.dropzone-empty[hidden],
.dropzone-chosen[hidden] { display: none; }

.dropzone-preview {
  width: 64px;
  height: 64px;
  border-radius: 10px;
  object-fit: cover;
  background: var(--surface-2);
  flex-shrink: 0;
}

.dropzone-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }

/* On a phone the row is tight, so the Replace button gives its room to the filename. */
@media (max-width: 640px) {
  .dropzone-chosen { gap: 12px; }
  .dropzone-preview { width: 52px; height: 52px; }
  .dropzone-replace { padding: 0.55em 0.9em; font-size: 0.85rem; }
}
.dropzone-name { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dropzone-size { font-size: 0.85rem; color: var(--text-muted); font-weight: 600; }
.dropzone-replace { flex-shrink: 0; }

.dropzone-error {
  margin: 14px 0 0;
  color: var(--danger);
  font-size: 0.9rem;
  font-weight: 600;
  text-align: center;
}

.choice-row { display: flex; gap: 18px; flex-wrap: wrap; }

/* Radios are hidden; the label itself is the control, and :has() styles the
   selected one. */
.choice-row input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }

.complexity-card { cursor: pointer; flex: 1 1 180px; }
.complexity-card:has(input:checked) { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.complexity-card.is-disabled { opacity: 0.45; cursor: not-allowed; }

.choice-title {
  font-family: 'Fredoka', sans-serif;
  font-weight: 600;
  font-size: 1.05rem;
  display: block;
}

.complexity-card:has(input:checked) .choice-title { color: var(--accent-dark); }
.choice-text { color: var(--text-muted); font-size: 0.9rem; line-height: 1.5; font-weight: 600; }

.pill-btn { cursor: pointer; }
.pill-btn:has(input:checked) { border-color: var(--accent); color: var(--accent-dark); }

/* The button with its status line beneath it, both to the right. The column direction used to live in a bare rule
   stranded after the phone media query, where it read as a mistake and quietly contradicted this block. */
.form-footer {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: flex-end;
}


/* The contact page's "on its way", under its button: a status line, not a banner. */
.sent-note {
  margin: 10px 0 0;
  color: var(--green);
  font-weight: 600;
}

/* The upload and the start of the job take a few seconds on a phone, so the button says it is working. */
.submit-btn {
  position: relative;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6em;
}

/* How much of the picture has gone up, painted behind the button's label. */
.submit-fill {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 0;
  background: rgba(255, 255, 255, 0.28);
  transition: width 0.2s linear;
}

.submit-spinner,
.submit-label { position: relative; }

.submit-btn.is-sending .submit-spinner { display: none; }

.submit-status {
  margin: 12px 0 0;
  color: var(--danger);
  font-size: 0.9rem;
  text-align: right;
}

.submit-spinner { display: none; }

.submit-btn.is-busy .submit-spinner {
  display: block;
  width: 1em;
  height: 1em;
  border: 2px solid rgba(255, 255, 255, 0.45);
  border-top-color: #fff;
  border-radius: 50%;
  animation: submit-spin 0.7s linear infinite;
}

.submit-btn.is-busy:disabled { opacity: 0.85; }

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

@media (prefers-reduced-motion: reduce) {
  .submit-btn.is-busy .submit-spinner { animation-duration: 2.5s; }
}

@media (max-width: 640px) {
  .submit-btn { width: 100%; }
  .sent-note { align-self: center; }
}

/* Generating ---------------------------------------------------------------- */

.generating {
  padding: 40px;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 12px;
  align-items: center;
}

/* The step being done reads as a sentence, so it takes the page's heading face rather than the
   monospace reserved for the small uppercase labels. */
.generating-title { font-family: 'Fredoka', sans-serif; font-weight: 600; color: var(--text-muted); }
.generating-note { color: var(--text-muted); font-size: 0.85rem; }

/* ---------- Generating: the stencil being made, step by step ---------- */
.progress-page { max-width: 1240px; }

.generating-progress {
  padding: 28px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.progress-head { display: flex; flex-direction: column; gap: 4px; }

.progress-bar {
  height: 12px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, var(--accent-light), var(--accent));
  transition: width 0.6s ease;
}

.stage-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 12px;
}

.stage-slot { display: flex; flex-direction: column; gap: 6px; }

.stage-frame {
  aspect-ratio: 1 / 1;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.stage-frame img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  opacity: 0;
  transform: scale(0.97);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.stage-slot.revealed .stage-frame { background: var(--surface); }
.stage-slot.revealed .stage-frame img { opacity: 1; transform: none; }
.stage-slot.current .stage-frame { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-light); }
.stage-slot.skipped .stage-frame { opacity: 0.4; }

.stage-caption { font-size: 0.75rem; color: var(--text-muted); line-height: 1.25; }
.stage-slot.revealed .stage-caption { color: var(--text); }

.generating-failed { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }

@media (max-width: 960px) {
  .stage-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

@media (max-width: 560px) {
  .stage-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .generating-progress { padding: 18px; }
}

/* The next step to be shown: a bar sliding back and forth across its frame until its picture appears. */
.stage-frame { position: relative; }

.stage-slot.working .stage-frame { border-color: var(--accent-light); }

.stage-slot.working .stage-frame::before,
.stage-slot.working .stage-frame::after {
  content: "";
  position: absolute;
  left: 12%;
  right: 12%;
  top: 50%;
  height: 6px;
  margin-top: -3px;
  border-radius: 999px;
}

.stage-slot.working .stage-frame::before { background: var(--border); }

.stage-slot.working .stage-frame::after {
  right: auto;
  width: 30%;
  background: var(--accent);
  animation: stage-working 1.2s ease-in-out infinite alternate;
}

@keyframes stage-working {
  from { transform: translateX(0); }
  to { transform: translateX(153%); }
}

@media (prefers-reduced-motion: reduce) {
  .stage-slot.working .stage-frame::after { animation-duration: 3s; }
}

/* The failure message is hidden until a job fails; its display:flex above would otherwise override [hidden]. */
.generating-failed[hidden] { display: none; }

button.btn-primary.full { width: 100%; }

/* The stencil (glowing cuts on a transparent ground) over a darkened pumpkin, centred on its face, like a lit carving. */
.stencil-view {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: 12px;
  overflow: hidden;
}
.stencil-view::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("/img/pumpkin-f1f33755d5fc7beb556bd14dafab1de9.png") center / cover no-repeat;
  filter: brightness(0.68) saturate(1.15);
}
.stencil-view img,
.thumb .stencil-view img,
.panel-body .stencil-view img,
.panel-body.large .stencil-view img {
  position: absolute;
  left: 50%;
  top: 53%;
  transform: translate(-50%, -50%);
  max-width: 72%;
  max-height: 72%;
  object-fit: contain;
  border-radius: 0;
  filter: drop-shadow(0 0 2px rgba(255, 170, 40, 0.9)) drop-shadow(0 0 6px rgba(255, 140, 20, 0.6));
}
.thumb .stencil-view { height: 90%; }
.panel-body .stencil-view { width: min(100%, 480px); }
.modal-thumb.stencil-view { flex: none; border-radius: 8px; }

/* Terms of Service and Privacy Policy. */
.legal { max-width: 760px; padding-bottom: 64px; }
.legal h2 { font-size: 1.15rem; margin: 32px 0 8px; }
.legal p, .legal li { line-height: 1.6; }
.legal ul { padding-left: 22px; margin: 8px 0 12px; }
.legal li + li { margin-top: 6px; }
.legal .caps { text-transform: uppercase; font-size: 0.86rem; letter-spacing: 0.01em; }
.legal a, .legal-consent a { color: var(--accent); }
.legal-consent { font-size: 0.85rem; color: var(--text-muted); margin: 0; }
mark.placeholder { background: #ffe58a; padding: 0 3px; border-radius: 3px; }
.site-footer a { color: inherit; }

/* "Keep this stencil" on an unsaved stencil. */
/* Like the Stencil and PDF Preview boxes: a white header bar over a tinted body. */
.save-card .panel-body {
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  gap: 12px;
  padding: 20px;
  min-height: 0;
}
.save-text { margin: 0; color: var(--text-muted); line-height: 1.5; font-size: 0.92rem; }
button.btn-secondary.full { width: 100%; }

/* Account page. */
.account-actions { display: flex; gap: 12px; justify-content: flex-end; align-items: center; }
.btn-primary.danger { background: var(--danger); box-shadow: none; }
.notice-banner { background: #eaf6ea; border: 1px solid #9cc79c; color: #2d5a2d; padding: 12px 16px; border-radius: 10px; font-weight: 600; margin: 16px 0 0; }
.download-note a { color: var(--accent); }

/* Logo banner on the left, filling the nav bar top to bottom, with the links on the right. */
.nav { padding-top: 0; padding-bottom: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: 16px; }
.nav > .nav-brand { grid-column: 1; justify-self: start; min-width: 0; margin: 0; }
.nav > .nav-links { grid-column: 2; gap: 24px; }
.nav .nav-links a { margin-right: 0; white-space: nowrap; }
.nav .nav-links .btn-text { white-space: nowrap; }
.nav-logo-image { display: block; height: 72px; width: auto; max-width: 100%; object-fit: contain; object-position: left center; }

/* Phones: the logo across the top on its own row when signed in (more links), beside Sign in otherwise. */
@media (max-width: 600px) {
  .nav { padding-left: 12px; padding-right: 12px; }
  .nav-logo-image { height: auto; max-height: 56px; }
  .nav.nav-signed-in { grid-template-columns: 1fr; padding: 0; }
  .nav.nav-signed-in > .nav-brand { justify-self: stretch; }
  .nav.nav-signed-in .nav-logo-image { width: 100%; max-height: 80px; object-position: center; }
  .nav.nav-signed-in > .nav-links { grid-column: 1; justify-self: center; gap: 18px; padding: 8px 12px; flex-wrap: wrap; justify-content: center; }
}

/* Footer at the bottom of the window on short pages, after the content on long ones. */
body { display: flex; flex-direction: column; min-height: 100vh; height: auto; }
body > main { flex: 1 0 auto; }

/* Error page (404, 500, ...) */
.error-page { text-align: center; padding-top: 64px; padding-bottom: 64px; }
.error-page-code { font-family: 'IBM Plex Mono', monospace; font-size: 0.95rem; color: var(--text-muted); margin: 0 0 8px; }
.error-page .page-subtitle { margin-bottom: 28px; }
.error-page .btn-primary { display: inline-block; text-decoration: none; }

/* ---------- Stencil page: the finished stencil above the steps that made it ---------- */
.stencil-page .preview-layout { margin-bottom: 48px; }
/* "How it was made" is a box like Stencil and PDF Preview: the same header bar and kicker, the same tinted body. */
.stencil-steps-body { background: var(--surface-2); }
.progress-head[hidden], .stencil-steps > .panel-head[hidden] { display: none; }

/* Side by side, the download controls sit at the bottom of the side column, so the Download PDF button (or, before the
   stencil is saved, the save card) ends level with the preview box. The terms note hangs below the button so it does
   not count. */
@media (min-width: 821px) {
  /* The save card sits under the side column in its own row, so the Stencil box ends level with the
     Download PDF button whether or not the stencil is saved. */
  .stencil-page .preview-layout > .panel:not(.save-card),
  .stencil-page .preview-side { grid-row: 1; }
  .stencil-page .save-card { grid-column: 2; grid-row: 2; }
  .stencil-page .preview-side { justify-content: flex-end; }
  /* The PDF preview box grows to fill the column, so it starts level with the Stencil box as well. */
  .stencil-page .preview-side .pdf-preview { flex: 1; }
}
/* Uploaded filenames can be one long word; let it wrap instead of pushing the page sideways on a phone. */
.stencil-page .page-head > div { min-width: 0; }
.stencil-page .page-title {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---------- PDF preview: the printed Letter sheets at the chosen pumpkin size (see pdf-preview.js) ---------- */
.pdf-preview .panel-body { flex-direction: column; gap: 10px; }
/* The size chooser and the download button share the printed sheet's box, which is narrower than the
   column was, so the three sizes share its width evenly rather than wrapping. */
.pdf-preview .download-block { width: 100%; margin-top: 6px; }
.pdf-preview .choice-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.pdf-preview .pill-btn { padding: 0.6em 0.4em; text-align: center; font-size: 0.9rem; }

/* "Would you carve this?" -- see preview.html. The thumbs and the reason chips are the same pill
   buttons the size picker uses, with their inputs hidden the same way; they keep focus, so the
   keyboard ring is drawn on the pill instead. */
.feedback-card .panel-body { display: flex; flex-direction: column; }
.feedback-ask { display: flex; flex-direction: column; gap: 14px; }
/* .panel-body centres what it holds, which suits one picture; here it shrank the thumbs to their
   text beside a full-width comment box. Stretch instead, as the download block does. */
.feedback-card .panel-body { align-items: stretch; justify-content: flex-start; }
/* .panel-body's minimum height suits a picture; here it left a band of nothing under the thumbs. */
.feedback-card .panel-body { min-height: 0; }
.feedback-thumbs { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.feedback-thumb { text-align: center; }
.feedback-card input[type="radio"],
.feedback-card input[type="checkbox"] { position: absolute; opacity: 0; pointer-events: none; }
.feedback-card .pill-btn:has(input:checked) { background: var(--surface-2); }
.feedback-card .pill-btn:has(input:focus-visible) { outline: 2px solid var(--accent-light); outline-offset: 2px; }
.feedback-more { display: flex; flex-direction: column; gap: 14px; }
/* A fieldset is laid out as a block with its legend on top; the chips wrap inline beneath it. */
.feedback-reasons { border: 0; margin: 0; padding: 0; min-width: 0; }
/* The labels are .kicker, as "Pumpkin size" is above the size pills: the same job, the same look. */
.feedback-legend { display: block; padding: 0; margin-bottom: 8px; }
.feedback-chip { display: inline-block; margin: 0 6px 8px 0; padding: 0.5em 0.9em; font-size: 0.88rem; }
.feedback-send { align-self: flex-start; }
.feedback-status { color: var(--text-muted); font-weight: 600; font-size: 0.9rem; min-height: 1.2em; }
/* What the card becomes once sent: nothing left to do, so nothing but the thanks. */
/* In the card's own label style (.kicker), like everything else in it; centred in the card's
   minimum height rather than pinned to its top like the ask. */
.feedback-done { text-align: center; margin: auto 0; }
/* "Provide feedback", beside the filename: small, since it is optional and should not compete
   with the stencil for attention. */
/* An outlined accent button -- the selected pill's colours, with the drop edge the site's buttons
   have -- so it reads as something to press without competing with Download PDF. */
.feedback-open {
    font-size: 0.85rem; padding: 0.55em 1.1em; text-decoration: none;
    border-color: var(--accent); color: var(--accent-dark);
    box-shadow: 0 2px 0 var(--accent-light);
}
.feedback-open:hover { background: var(--surface-2); }
.feedback-open:active { transform: translateY(1px); box-shadow: 0 1px 0 var(--accent-light); }
.feedback-open:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; }

/* The popup. Shown non-modally over a backdrop of its own, so reCAPTCHA's challenge -- stacked far
   above both -- is never trapped behind it; see preview.html. */
.feedback-backdrop {
    position: fixed; inset: 0; z-index: 900;
    background: rgba(36, 26, 16, 0.45);
}
/* Centred with inset and auto margins, never a transform: a transformed element becomes the frame
   that position:fixed children are placed against, and reCAPTCHA's badge -- fixed, 256px wide, and
   hidden at the window's corner on every page -- was laid out inside the popup instead, overflowing
   it into a scrollbar. */
.feedback-dialog {
    position: fixed; inset: 0; margin: auto; z-index: 901;
    padding: 0; border: 0; background: transparent;
    width: min(440px, calc(100vw - 32px)); height: fit-content;
    max-height: calc(100vh - 32px); overflow: auto;
}
.feedback-dialog .feedback-card { box-shadow: 0 24px 48px -16px rgba(36, 26, 16, 0.45); }
.feedback-card .panel-head { display: flex; align-items: center; justify-content: space-between; }
.feedback-close {
    color: var(--text-muted); text-decoration: none;
    font-size: 1.5rem; line-height: 1; padding: 0 2px;
}
.feedback-close:hover { color: var(--text); }

/* The display rules above would otherwise beat the hidden attribute the script sets. */
.feedback-ask[hidden],
.feedback-more[hidden],
.feedback-reasons[hidden],
.feedback-open[hidden],
.feedback-backdrop[hidden] { display: none !important; }
/* The sheets are drawn inside a stage of fixed height, scaled to fit it, so choosing a size never changes the height of
   the column -- and so of the row, and everything below it. */
.pdf-stage {
  width: 100%;
  height: 320px;
  display: flex;
  justify-content: center;
  align-items: flex-start;
}
.pdf-pages {
  display: grid;
  grid-template-columns: repeat(var(--columns, 1), minmax(0, 1fr));
  gap: 6px;
  width: 240px;
}
.pdf-sheet {
  position: relative;
  container-type: inline-size;
  aspect-ratio: 8.5 / 11;
  background: #fff;
  border: 1px solid var(--border);
  box-shadow: 0 2px 10px rgba(60, 30, 0, 0.12);
}
.pdf-print-area { position: absolute; overflow: hidden; }
.pdf-print-area img.pdf-stencil {
  position: absolute;
  max-width: none;
  max-height: none;
  filter: brightness(0);
  transition: width 0.25s ease, height 0.25s ease, left 0.25s ease, top 0.25s ease;
}
.pdf-guide { position: absolute; display: none; }
.pdf-guide.shown { display: block; }
.pdf-guide.across { top: 0; bottom: 0; border-left: 1px dashed #777; }
.pdf-guide.down { left: 0; right: 0; border-top: 1px dashed #777; }
.pdf-footer {
  position: absolute;
  font-family: Helvetica, Arial, sans-serif;
  font-size: calc(900cqw / var(--sheet-w-pt, 612));
  line-height: 1;
  color: #000;
  white-space: nowrap;
}
/* The caption is always two lines tall, whether its text takes one line or two. */
.pdf-caption {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text-muted);
  text-align: center;
  line-height: 1.3;
  height: 2.6em;
}
@media (prefers-reduced-motion: reduce) {
  .pdf-print-area img.pdf-stencil { transition: none; }
}

/* The pumpkin and the printed sheet both sit at the top of their boxes, level with each other. */
.stencil-page .preview-layout > .panel .panel-body.large { align-items: flex-start; padding: 24px; }
.stencil-page .pdf-preview .panel-body { justify-content: flex-start; padding: 24px; }

/* ---------- A stencil being made: one picture turning into the carving (stencil-steps.js) ---------- */
.morph .progress-head { align-items: center; text-align: center; }
/* The dots under the picture already count the steps, so the line carries a spinner rather than a second bar. */
.progress-line { display: inline-flex; align-items: center; gap: 8px; }
.progress-spinner {
  width: 0.95em;
  height: 0.95em;
  flex: none;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: submit-spin 0.8s linear infinite;
}
.progress-spinner[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
  .progress-spinner { animation-duration: 3s; }
}
.morph-stage {
  position: relative;
  width: min(100%, 640px);
  aspect-ratio: 4 / 3;
  margin: 0 auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
}
.morph-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  transform: scale(1.03);
  transition: opacity 0.9s ease, transform 1.4s ease;
}
.morph-layer.shown { opacity: 1; transform: none; }
.morph-layer.pumpkin.shown { filter: drop-shadow(0 0 22px rgba(255, 160, 40, 0.55)); }

/* While the physics runs: the cuts of the draft slowly glow like candlelight (screen blending leaves the rind white),
   and the rind near them is pressed, with a ripple and a tick where it holds. */
.morph-stage { isolation: isolate; }
.morph-glow {
  position: absolute;
  pointer-events: none;
  mix-blend-mode: screen;
  background: radial-gradient(ellipse at 50% 45%, #ffe29a 0%, #ffae42 45%, #ff7a1a 100%);
  --reveal: 0%;
  -webkit-mask-image: linear-gradient(90deg, #000 var(--reveal), transparent calc(var(--reveal) + 14%));
  mask-image: linear-gradient(90deg, #000 var(--reveal), transparent calc(var(--reveal) + 14%));
  transition: opacity 0.9s ease;
}
.morph-glow.leaving { opacity: 0; }
.tap { position: absolute; width: 28px; height: 28px; margin: -14px 0 0 -14px; pointer-events: none; }
.tap::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid rgba(255, 138, 31, 0.9);
  animation: tap-ring 1.1s ease-out forwards;
}
.tap::after {
  content: "\2713";
  position: absolute;
  inset: 5px;
  border-radius: 50%;
  background: #1b8f3e;
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  animation: tap-tick 1.3s ease forwards;
}
@keyframes tap-ring {
  from { transform: scale(0.3); opacity: 1; }
  to { transform: scale(1.6); opacity: 0; }
}
@keyframes tap-tick {
  0%, 45% { opacity: 0; transform: scale(0.5); }
  60% { opacity: 1; transform: scale(1.1); }
  85% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; }
}

/* As the final stencil appears, each bridge lights up in turn. */
.spark {
  position: absolute;
  width: 18px;
  height: 18px;
  margin: -9px 0 0 -9px;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(circle, #fff 0%, #ffd27a 35%, rgba(255, 138, 31, 0) 70%);
  animation: spark-pop 1.2s ease-out forwards;
}
@keyframes spark-pop {
  0% { transform: scale(0.2); opacity: 0; }
  30% { transform: scale(1.5); opacity: 1; }
  100% { transform: scale(0.8); opacity: 0; }
}


.morph-dots { list-style: none; margin: 0; padding: 0; display: flex; justify-content: center; gap: 10px; }
.morph-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid var(--border);
  background: var(--surface);
  transition: background 0.4s ease, border-color 0.4s ease;
}
.morph-dot.done { background: var(--accent); border-color: var(--accent); }
.morph-dot.next { border-color: var(--accent); animation: dot-pulse 1.2s ease-in-out infinite alternate; }
@keyframes dot-pulse { from { transform: scale(1); } to { transform: scale(1.25); } }

/* Under "How it was made", the strength check's picture carries a tick. */
.stage-slot.checked .stage-frame::after {
  content: "\2713";
  position: absolute;
  right: 6px;
  bottom: 6px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  font-weight: 700;
  font-size: 0.8rem;
  display: flex;
  align-items: center;
  justify-content: center;
}
@media (prefers-reduced-motion: reduce) {
  .morph-layer { transition: none; transform: none; }
  .morph-stage.checking::after, .morph-dot.next { animation: none; }
}

/* The stencil being made sits in the Stencil box, filling it; when it is done it hands over to the finished stencil. */
.stencil-page .panel-body.large .stencil-making { width: 100%; display: flex; flex-direction: column; gap: 14px; }

/* Phones: the stencil page closes up, so the printed sheet and its Download button are not a screen away.
   The picture of the carved pumpkin is the page's showpiece, but it should not push everything else off. */
@media (max-width: 600px) {
  .stencil-page.container { padding-top: 20px; padding-bottom: 32px; }
  .stencil-page .page-title { font-size: 1.35rem; line-height: 1.2; }
  .stencil-page .page-head { margin-bottom: 12px; gap: 4px; }
  .stencil-page .preview-layout { gap: 14px; margin-bottom: 24px; }
  .stencil-page .panel-head { padding: 10px 14px; }
  .stencil-page .preview-layout > .panel .panel-body.large { padding: 12px; min-height: 0; }
  .stencil-page .pdf-preview .panel-body { padding: 12px; gap: 8px; }
  .stencil-page .panel-body.large .stencil-view { width: min(100%, 300px); }
  .stencil-page .save-card .panel-body { padding: 14px; }
}
.panel-body .morph-stage img.morph-layer { max-width: none; max-height: none; border-radius: 0; }
.fade-in { transition: opacity 0.7s ease, transform 0.7s ease; }
.fade-in.leaving { opacity: 0; }
.fade-in.arriving { opacity: 0; transform: translateY(8px); }
.preview-side.fade-in { transition-delay: 0.25s; }
.stencil-steps.fade-in { transition-delay: 0.5s; }
@media (prefers-reduced-motion: reduce) {
  .fade-in { transition: none; }
}

/* The floating reCAPTCHA badge is hidden, which Google permits only where the
   attribution it replaces is shown instead -- see the footer in layout.html. */
.grecaptcha-badge { visibility: hidden; }
.recaptcha-attribution { display: block; margin-top: 6px; font-size: 0.78rem; opacity: 0.75; }

/* ---------------------------------------------------------------------------
   How it works
   --------------------------------------------------------------------------- */

/* "Learn more" closes the lede rather than starting its own line: it is an aside for the people who want it,
   not a second call to action competing with Start carving. */
.hero-more { font-weight: 700; white-space: nowrap; }
/* The button and the line under it are both centred on the column, not aligned to each other's left edge. */
.hero-cta { text-align: center; }
/* The last line of the hero, under the two-tone picture. Spaced by .hero-copy's own gap like everything else
   in it, so it needs no margin of its own. */
.hero-more-line { text-align: center; color: var(--text-muted); font-size: 0.95rem; }

/* Start carving: the one thing to click on the page, larger than other primary buttons. A row of the hero, so
   the hero's own gap spaces it above and below.

   Every few seconds a band of candlelight passes across it and it glows as the light goes by: the same warm
   sweep the waiting page uses while the physics runs (.morph-glow), and the halo the finished pumpkin gets.
   Most of each cycle is at rest, so it catches the eye without being constant motion. */
.hero-cta-top .btn-primary {
  position: relative;
  overflow: hidden;
  display: inline-block;
  font-size: 1.25rem;
  padding: 1em 2.4em;
  animation: cta-glow 4.5s ease-in-out infinite;
}
.hero-cta-top .btn-primary::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 40%;
  pointer-events: none;
  background: linear-gradient(100deg, transparent, rgba(255, 236, 190, 0.6), transparent);
  transform: translateX(-120%) skewX(-18deg);
  animation: cta-sheen 4.5s ease-in-out infinite;
}
@keyframes cta-sheen {
  0%, 55% { transform: translateX(-120%) skewX(-18deg); }
  85%, 100% { transform: translateX(320%) skewX(-18deg); }
}
@keyframes cta-glow {
  0%, 55%, 100% { box-shadow: var(--shadow); }
  72% { box-shadow: var(--shadow), 0 0 26px rgba(255, 160, 40, 0.55); }
}
@media (prefers-reduced-motion: reduce) {
  .hero-cta-top .btn-primary,
  .hero-cta-top .btn-primary::after { animation: none; }
}

/* 960 rather than the container's full 1100: at 1100 the two step pictures would be drawn a shade wider than
   the files themselves (512px) and start to soften. This uses the space without upscaling them. */
.how { max-width: 960px; }
.how-title { margin-bottom: 12px; }
/* One voice for the whole page. Every paragraph, caption and closing line is the opening paragraph's type --
   same size, same colour, same leading -- so no part of the argument reads as a footnote to another. Only the
   headings and the red word differ, and they say so for themselves. */
.how-lede,
.how-text,
.how-close {
  font-size: 1rem;
  line-height: 1.45;
  color: var(--text-muted);
  font-weight: 400;
}

.how-lede { margin-bottom: 0; }

.how-section { margin-top: 40px; }
.how-heading { font-size: 1.35rem; color: var(--accent-dark); margin-bottom: 14px; }
.how-text { margin-bottom: 12px; }
.how-note { color: var(--text-muted); font-size: 0.92rem; margin-top: 12px; }

.how-steps {
  line-height: 1.55;
  margin: 0;
  padding-left: 1.3em;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Each step is a sentence and then the pictures it refers to, so the words lead and the pictures follow. A
   strip reads left to right like the pipeline itself.

   One rhythm for the whole page: 22px, about one line of text, between every paragraph and every strip. That
   is what puts the same gap above a row of pictures as below it, and one blank line rather than two between
   the opening paragraph and the first step. */
.how-step,
.how-text + .how-strip,
.how-strip + .how-text { margin-top: 22px; }

.how-step { margin-top: 22px; }

/* 104px, the size of the style thumbnails on the upload page, so a step picture is the same object there and
   here. They are evidence for the sentence above them, not the point of the page. */
.how-single { margin: 22px auto 0; max-width: 104px; }

.how-strip {
  margin: 22px auto 0;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: 16px;
}

.how-frame { margin: 0; flex: 0 0 104px; }

.how-arrow {
  flex: none;
  align-self: center;
  /* The same colour as the words around it: it is punctuation in the sentence, not a control. */
  color: var(--text-muted);
  font-size: 1.8rem;
  line-height: 1;
}

.how-single img,
.how-frame img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--surface);
}

.how-close { margin-top: 22px; }

.how-cta { margin-top: 44px; display: flex; justify-content: center; }
.how-cta .btn-primary { display: inline-block; font-size: 1.1rem; padding: 1em 2.2em; }

@media (max-width: 600px) {
  /* No room for two side by side, so the strip stacks and the arrow turns to point down it. */
  .how-strip { flex-direction: column; align-items: center; gap: 12px; }
  .how-single { max-width: 76px; }
  .how-frame { flex-basis: 76px; }
  .how-arrow { align-self: center; transform: rotate(90deg); }
  .how-section, .how-step { margin-top: 30px; }
}
