/* Fruggals theme — one stylesheet, no build step (see AGENTS.md). Sections are
   marked with `=== NAME ===` banners; jump to one by searching for `=== `.

   Table of contents:
     1.  Design tokens & base
     2.  Site chrome (header · nav · footer · layout)
     3.  Home: hero
     4.  Campaign cards (home grid · states · filter)
     5.  Campaign gallery & lightbox (detail)
     6.  Campaign detail: prices, meta & description
     7.  Pricing breakdown
     8.  Forms (auth · join · admin shared inputs)
     9.  Progress & status badges
     10. Join confirm / leave
     11. Admin backoffice */

/* === 1. DESIGN TOKENS & BASE ============================================= */
:root {
  --color-bg: #faf9f7;
  --color-surface: #ffffff;
  --color-text: #1e2329;
  --color-text-muted: #5b6470;
  --color-accent: #1f6f54;
  /* A darker accent for the hover/active state of filled green buttons. */
  --color-accent-dark: #18573f;
  --color-accent-soft: #e3f0ea;
  --color-border: #e2ddd5;
  --color-error: #a13333;
  /* Form controls need a perceivable boundary (WCAG 1.4.11 wants 3:1 against
     the surrounding color); the decorative card border above is far too
     light for that, so inputs get this darker warm gray (~4:1). */
  --color-border-input: #857c6f;
  --radius: 8px;
  /* Spacing scale on a 4px grid; --space (1rem) is the base unit. Off-grid
     micro-padding (the badge's and savings pill's vertical 0.1/0.4rem and
     table cells' 0.35rem) stays literal — optical tuning, not a scale step. */
  --space-xs: 0.25rem;
  --space-sm: 0.5rem;
  --space-md: 0.75rem;
  --space: 1rem;
  --max-width: 56rem;
  /* Type scale, in rem relative to the 16px root. Every font-size references
     one of these so sizes stay consistent and adjustable in one place. */
  --text-xs: 0.8rem;
  --text-sm: 0.9rem;
  --text-base: 1rem;
  --text-lg: 1.1rem;
  --text-xl: 1.25rem;
  --text-2xl: 1.75rem;
  font-size: 16px;
}

* {
  box-sizing: border-box;
}

/* Visible to screen readers only; the standard clipping pattern. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* A bordered surface — the shared look behind several panels (campaign card, buy
   box, order summary, the pricing table). Each component keeps its own layout;
   this is just the surface, applied by adding the class. */
.panel {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
}

/* An image rendered to fill its box, cropping rather than distorting — the
   shared base for every thumbnail/hero; each keeps its own size and aspect. */
.thumb {
  object-fit: cover;
  border-radius: var(--radius);
}

body {
  margin: 0;
  font-family: system-ui, sans-serif;
  line-height: 1.6;
  color: var(--color-text);
  background: var(--color-bg);
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/* === 2. SITE CHROME (header · nav · footer · layout) ===================== */
.site-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space) calc(var(--space) * 1.5);
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface);
}

.site-nav {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.site-nav a {
  color: var(--color-accent);
}

.user-email {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

.language-switch {
  display: flex;
  gap: var(--space-xs);
  font-size: var(--text-sm);
  margin-right: var(--space-sm);
}

.language-current {
  font-weight: 700;
  color: var(--color-text-muted);
}

.link-button {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--color-accent);
  text-decoration: underline;
  cursor: pointer;
}

.site-name {
  font-weight: 700;
  font-size: var(--text-xl);
  color: var(--color-accent);
  text-decoration: none;
}

main {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: calc(var(--space) * 2);
  width: 100%;
  max-width: var(--max-width);
  margin: 0 auto;
  padding: calc(var(--space) * 2) calc(var(--space) * 1.5);
}

.site-footer {
  padding: var(--space) calc(var(--space) * 1.5);
  border-top: 1px solid var(--color-border);
  color: var(--color-text-muted);
  text-align: center;
  font-size: var(--text-sm);
}

/* === 3. HOME: hero ======================================================= */
.hero {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.hero > * {
  margin: 0;
}

.hero h1 {
  font-size: var(--text-2xl);
  line-height: 1.3;
}

.hero p {
  color: var(--color-text-muted);
  max-width: 40rem;
}

/* === 4. CAMPAIGN CARDS (home grid · states · filter) ===================== */
/* Single-column list of wide rows. One row per campaign gives the price
   comparison and quorum progress room to read, which a tight grid card cannot. */
.campaign-list {
  display: flex;
  flex-direction: column;
  gap: var(--space);
}

/* A wide row: hero thumbnail on the left, info column on the right. The whole
   card is one click target via the title's stretched ::after. On narrow screens
   the thumbnail stacks on top (see the media query at the end of this section). */
.campaign-card {
  position: relative;
  display: flex;
  gap: var(--space);
  padding: var(--space);
}

.campaign-card > * {
  margin: 0;
}

/* Hero thumbnail: a fixed-size panel on the left so every row's text column
   starts at the same x. flex-shrink: 0 stops a long title from squashing it;
   align-self: flex-start lets its own 4:3 box set the height (rather than being
   tied to the text height), so the image stays prominent; object-fit: cover
   crops to fill it. */
.card-thumb {
  flex-shrink: 0;
  align-self: flex-start;
  width: 15rem;
  aspect-ratio: 4 / 3;
}

/* The info column: head, prices, progress, meta stacked. min-width: 0 lets it
   shrink so long unbroken text wraps instead of widening the row. */
.card-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  flex: 1;
  min-width: 0;
}

/* These rows are grandchildren of the card, so the card's own margin reset does
   not reach them; the gap above owns the vertical rhythm. */
.card-body > * {
  margin: 0;
}

/* The card's bottom row: meta on the left, the join CTA on the right. Pinned to
   the bottom (margin-top: auto) so it lands flush with the foot of the image,
   collecting the slack into one gap above it; on a narrow card the CTA wraps
   below the meta. Closed campaigns have no foot and simply stay top-aligned. */
.card-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm) var(--space);
  margin-top: auto;
}

.card-foot > * {
  margin: 0;
}

/* The title fills the row; the status badge (open's is a quiet outline) is
   pushed to the top-right corner. align-items: flex-start keeps the pill at the
   top when a long title wraps to two lines. */
.card-head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
}

.card-head .status-badge {
  margin-left: auto;
  flex-shrink: 0;
}

.campaign-card:hover {
  border-color: var(--color-accent);
}

.campaign-card.status-funded {
  background: var(--color-accent-soft);
  border-color: var(--color-accent);
}

/* The join CTA on a card carries .button-primary (funded) or .button-secondary
   (open) for its look; .card-cta only adds the card-specific layout. It's a span
   affordance — the whole card is the link — so it has no :hover of its own;
   instead the card's hover darkens it, the same end state as a real button. */
.card-cta {
  flex-shrink: 0;
  font-weight: 600;
  font-size: var(--text-sm);
}

.campaign-card:hover .card-cta {
  background: var(--color-accent-dark);
  color: #fff;
}

/* Closed campaigns stay on the card surface — they must not melt into the
   page background — and instead carry a status badge plus a colored left
   edge: the accent for a completed (successful) campaign, muted gray for one
   that expired or was cancelled. */
.campaign-card.status-completed {
  border-left: 4px solid var(--color-accent);
}

.campaign-card.status-expired,
.campaign-card.status-cancelled {
  border-left: 4px solid var(--color-text-muted);
}

.campaign-card h2 {
  /* Zero the heading's default margin so the title sits flush with the top of
     the thumbnail and the status pill (it's nested in .card-head, so the
     .card-body reset does not reach it). */
  margin: 0;
  font-size: var(--text-lg);
}

.campaign-card h2 a {
  color: inherit;
  text-decoration: none;
}

/* Stretched link: the title anchor's ::after covers the whole card, making the
   entire card clickable without wrapping block content in an <a>. */
.campaign-card h2 a::after {
  content: "";
  position: absolute;
  inset: 0;
}

.campaign-card:hover h2 a {
  color: var(--color-accent);
}

/* Narrow screens (phones): keep the thumbnail on the left but shrink it to a
   compact square, so each row stays a tidy list item instead of a full-width
   banner that dwarfs the text. */
@media (max-width: 36rem) {
  .card-thumb {
    width: 6.5rem;
    aspect-ratio: 1;
  }
}

/* The link that toggles closed campaigns into and out of the home list. */
.campaign-filter {
  font-size: var(--text-sm);
}

/* === 5. CAMPAIGN GALLERY & LIGHTBOX (detail) ============================= */
/* Detail-page gallery: a large hero above a row of smaller thumbnails. Capped
   narrower than the text column so the hero is a sensible size and the thumbnail
   tiles below don't blow up to a quarter of the page each. */
.campaign-gallery {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

/* .thumb (object-fit: cover) fills the frame edge to edge, so a photo whose shape
   differs from the fixed aspect ratio is cropped rather than letterboxed. */
.campaign-hero {
  width: 100%;
  aspect-ratio: 3 / 2;
}

/* A fixed number of equal columns that shrink with the viewport, so the strip
   stays a single row from desktop down to mobile instead of wrapping to a second
   line. The column count must match _MAX_THUMBS in pages.py. */
.campaign-thumbs {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-sm);
}

.campaign-thumb {
  width: 100%;
  aspect-ratio: 1;
}

/* Clickable gallery images (buttons that open the lightbox): reset the button
   chrome, and a zoom cursor + hover/focus outline signal they enlarge. */
.gallery-hero,
.gallery-thumb {
  display: block;
  padding: 0;
  border: none;
  background: none;
  cursor: zoom-in;
}

.gallery-hero {
  width: 100%;
}

.gallery-hero:hover .campaign-hero,
.gallery-hero:focus-visible .campaign-hero,
.gallery-thumb:hover .campaign-thumb,
.gallery-thumb:focus-visible .campaign-thumb {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* The "+N" overflow tile: the last thumbnail carries a dark badge counting the
   images it stands in for. Clicking it opens the lightbox at the first of them. */
.gallery-more {
  position: relative;
}

.gallery-more-count {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius);
  background: rgb(0 0 0 / 0.55);
  color: #fff;
  font-size: var(--text-xl);
  font-weight: 600;
}

/* Lightbox as a modal <dialog> (opened with showModal()): a full-screen overlay
   that centres the image. Modal makes the page behind inert, so it cannot be
   clicked through; the dark ::backdrop dims it, and a click on the dialog itself
   (the area around the image) closes it. Controls are fixed to the viewport
   edges. */
.lightbox {
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  inset: 0;
  border: none;
  padding: calc(var(--space) * 3) var(--space);
  background: transparent;
}

.lightbox[open] {
  display: flex;
  align-items: center;
  justify-content: center;
}

.lightbox::backdrop {
  background: rgba(0, 0, 0, 0.88);
}

.lightbox-image {
  max-width: 100%;
  max-height: 90vh;
  object-fit: contain;
}

.lightbox-close,
.lightbox-nav {
  position: fixed;
  border: none;
  background: none;
  color: #fff;
  line-height: 1;
  cursor: pointer;
  text-shadow: 0 0 6px rgba(0, 0, 0, 0.7);
  opacity: 0.85;
  transition: opacity 0.1s ease;
}

.lightbox-close:hover,
.lightbox-close:focus-visible,
.lightbox-nav:hover,
.lightbox-nav:focus-visible {
  opacity: 1;
}

.lightbox-close {
  top: 0;
  right: 0;
  padding: var(--space);
  font-size: var(--text-2xl);
}

.lightbox-nav {
  top: 50%;
  transform: translateY(-50%);
  padding: 0 var(--space);
  font-size: calc(var(--text-2xl) * 2);
}

.lightbox-prev {
  left: 0;
}

.lightbox-next {
  right: 0;
}

/* === 6. CAMPAIGN DETAIL: prices, meta & description ====================== */
/* The top row: gallery on the left, the buy box (price, progress, join) on the
   right. The gallery column is the wider of the two; both shrink (minmax 0) so
   long content wraps instead of forcing the row wider. The default
   align-items: stretch makes the buy box match the gallery's height. */
.campaign-overview {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  gap: calc(var(--space) * 2);
}

/* The buy box is a bordered panel so that filling the gallery's height reads as
   a deliberate panel rather than content floating above empty space. It owns its
   vertical rhythm through gap; its children (grandchildren of .campaign-detail)
   bring no margins of their own. */
.campaign-buybox {
  display: flex;
  flex-direction: column;
  gap: var(--space);
  min-width: 0;
  padding: var(--space);
  /* Let the data inside respond to the panel's own width (a container query),
     so it goes two-column when the panel is wide (stacked on a tablet) and stays
     one column in the narrow desktop sidebar — independent of the page width. */
  container-type: inline-size;
}

.campaign-buybox > * {
  margin: 0;
}

/* The data cluster (price, progress, dates) sits at the top of the panel with
   the panel's own rhythm; its items are nested here, so the buy box's margin
   reset above does not reach them. */
.campaign-data {
  display: flex;
  flex-direction: column;
  gap: var(--space);
}

.campaign-data > * {
  margin: 0;
}

/* The two data groups (cost, status); each keeps a tighter internal rhythm than
   the gap between them. */
.buybox-cost,
.buybox-status {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.buybox-cost > *,
.buybox-status > * {
  margin: 0;
}

/* When the buy box is wide enough for it (stacked on a tablet, not the narrow
   desktop sidebar), set the cost and status groups side by side so the panel
   doesn't read half-empty. */
@container (min-width: 30rem) {
  .campaign-data {
    flex-direction: row;
    align-items: start;
  }

  .buybox-cost,
  .buybox-status {
    flex: 1;
  }
}

/* Glue the data to the top and push the join/login control to the bottom edge
   of the panel (the same move as the card's pinned meta). margin-top: auto on
   the first element after the data cluster absorbs the slack; a leave link, if
   present, follows right after it. */
.campaign-buybox > .campaign-data + * {
  margin-top: auto;
}

/* Below this width the two columns get too cramped, so stack them. */
@media (max-width: 56rem) {
  .campaign-overview {
    grid-template-columns: 1fr;
  }
}

/* Rich description: the creator's paragraphs, line breaks, and pasted links. */
.campaign-description {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.campaign-prices {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-sm);
}

.group-price {
  font-size: var(--text-xl);
  color: var(--color-accent);
}

.retail-price {
  color: var(--color-text-muted);
}

.savings {
  /* The row aligns on text baselines; center the badge box itself so its
     padded background doesn't hang below the prices. */
  align-self: center;
  background: var(--color-accent-soft);
  color: var(--color-accent);
  border-radius: var(--radius);
  padding: 0.1rem var(--space-sm);
  font-size: var(--text-sm);
  white-space: nowrap;
}

.status-funded .savings {
  background: var(--color-surface);
}

.campaign-meta {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

/* Where the struck-through retail price was seen, on the detail page. */
.retail-source {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

/* Expected delivery window on the detail page. Slow delivery is the tradeoff
   buyers accept, so it reads in the normal text color, not dimmed. */
.campaign-delivery {
  font-size: var(--text-base);
}

.campaign-detail {
  display: flex;
  flex-direction: column;
  gap: var(--space);
}

/* Containers own the vertical rhythm through gap; direct children never
   bring their own margins. */
.campaign-detail > * {
  margin: 0;
}

/* === 7. PRICING BREAKDOWN =============================================== */
.pricing-breakdown {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.pricing-breakdown > * {
  margin: 0;
}

.pricing-breakdown h2 {
  font-size: var(--text-lg);
}

/* Full width: the inline "what it covers" column fills it (no longer a narrow
   two-column figures table). */
.pricing-breakdown table {
  border-collapse: collapse;
  width: 100%;
}

.pricing-breakdown caption {
  caption-side: bottom;
  text-align: left;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  padding-top: var(--space-xs);
}

.pricing-breakdown th,
.pricing-breakdown td {
  padding: 0.35rem var(--space-md);
  vertical-align: top;
}

.pricing-breakdown th {
  text-align: left;
  font-weight: 600;
  white-space: nowrap;
}

/* The explanation column takes the slack so the table fills the width; the
   figures column hugs the right and never wraps. */
.breakdown-covers {
  width: 100%;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

.breakdown-amount {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.pricing-breakdown tfoot th,
.pricing-breakdown tfoot td {
  border-top: 1px solid var(--color-border);
  font-weight: 600;
  color: var(--color-accent);
}

.breakdown-note {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

/* Small screens: drop the explanation column so the breakdown stays a compact
   component + amount table rather than a wall of text. The figures and the
   total note still convey the breakdown; the prose is a desktop nicety. */
@media (max-width: 40rem) {
  .breakdown-covers {
    display: none;
  }
}

/* === 8. FORMS (auth · join · admin shared inputs) ======================== */
.auth-form {
  display: flex;
  flex-direction: column;
  gap: var(--space);
  max-width: 24rem;
}

.auth-form > * {
  margin: 0;
}

.auth-form form {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.auth-form input,
.join-confirm input,
.admin-form input,
.admin-form textarea {
  padding: var(--space-sm);
  border: 1px solid var(--color-border-input);
  border-radius: var(--radius);
  font: inherit;
}

/* The two button styles, applied by class to every button/CTA across the site
   (forms, the join flows, the card CTAs) so the look lives in one place.
   `.button-primary` is the filled green action; `.button-secondary` is the
   outlined accent one. Both work on <button> and <a> (border/font/text rules a
   link doesn't get for free); they're inline-block, and stretch to full width
   inside the column-flex forms that contain them. */
.button-primary,
.button-secondary {
  display: inline-block;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid transparent;
  border-radius: var(--radius);
  font: inherit;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color 0.1s ease,
    color 0.1s ease;
}

.button-primary {
  background: var(--color-accent);
  color: #fff;
}

.button-secondary {
  border-color: var(--color-accent);
  background: var(--color-surface);
  color: var(--color-accent);
}

/* Filled darkens; outlined fills. Same on hover and keyboard focus. */
.button-primary:hover,
.button-primary:focus-visible {
  background: var(--color-accent-dark);
}

.button-secondary:hover,
.button-secondary:focus-visible {
  background: var(--color-accent);
  color: #fff;
}

.form-error {
  color: var(--color-error);
}

/* === 9. PROGRESS & STATUS BADGES ========================================= */
.campaign-progress {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
}

.campaign-progress progress {
  flex: 1;
  min-width: 6rem;
  accent-color: var(--color-accent);
}

.campaign-detail .campaign-progress {
  max-width: 28rem;
}

.status-badge {
  display: inline-block;
  background: var(--color-accent);
  color: #fff;
  border-radius: var(--radius);
  /* Trim the line box to the font's cap height and baseline so the empty
     ascender/descender space (which all-caps text leaves above and below the
     letters) is removed; symmetric padding then centers the text exactly. */
  line-height: 1;
  text-box-trim: trim-both;
  text-box-edge: cap alphabetic;
  padding: 0.4rem var(--space-sm);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.status-badge.status-expired,
.status-badge.status-cancelled {
  background: var(--color-text-muted);
}

/* Open is the active default: a quiet outlined pill, not a filled chip, so it
   keeps every card's top row uniform without competing with funded/closed. */
.status-badge.status-open {
  background: transparent;
  color: var(--color-text-muted);
  border: 1px solid var(--color-border-input);
}

/* === 10. JOIN CONFIRM / LEAVE =========================================== */
/* The confirm page: a narrow checkout column — order summary, address choice,
   submit. */
.join-confirm {
  display: flex;
  flex-direction: column;
  gap: var(--space);
  max-width: 40rem;
  /* Centre the column within main's wider content width. */
  align-self: center;
  width: 100%;
}

.join-confirm > * {
  margin: 0;
}

/* The campaign at the top: thumbnail, title + price, and the quantity, on one
   row (the quantity wraps below on a narrow screen). */
.order-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space);
  padding: var(--space);
}

.order-thumb {
  flex-shrink: 0;
  width: 6rem;
  height: 6rem;
}

.order-detail {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  /* Grow to fill the row (pushing the quantity to the right), but keep a floor
     so the quantity wraps below instead of the title being crushed. */
  flex: 1;
  min-width: 10rem;
}

.order-detail > * {
  margin: 0;
}

/* The quantity item on the order row; it does not shrink, and wraps below the
   thumbnail + detail when the row is too narrow. */
.order-quantity {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  flex-shrink: 0;
}

.order-detail h2 {
  font-size: var(--text-lg);
}

.order-detail h2 a {
  color: inherit;
  text-decoration: none;
}

.address-choice {
  display: flex;
  flex-direction: column;
  gap: var(--space);
}

.address-choice > * {
  margin: 0;
}

/* The saved-address radio list: each option is a bordered, clickable row that
   highlights when selected (:has(:checked), no JavaScript). */
.address-options {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  border: none;
  padding: 0;
  margin: 0;
}

.address-options legend {
  font-weight: 600;
  padding: 0;
  margin-bottom: var(--space-sm);
}

.address-option {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-input);
  border-radius: var(--radius);
  cursor: pointer;
}

.address-option:has(input:checked) {
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
}

.address-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

/* When a saved-address list is present, hide the new-address form unless the
   "new" option is chosen (:has, no JavaScript). With no saved addresses there
   is no list, so the `~` rule never matches and the form shows by default. */
.address-options ~ .address-form {
  display: none;
}

.address-choice:has(#address-new:checked) .address-form {
  display: flex;
}

.address-row {
  display: flex;
  gap: var(--space-sm);
}

.address-row .address-field {
  flex: 1;
}

.address-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  min-width: 0;
}

.address-field label {
  font-weight: 600;
  font-size: var(--text-sm);
}

/* The buy-box quantity stepper: a small labelled number input above the CTA,
   whose GET submit carries the quantity to the confirm page. */
.join-quantity {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.join-quantity-field {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.join-quantity-field label {
  font-weight: 600;
  font-size: var(--text-sm);
}

.join-quantity-field input {
  width: 4rem;
  padding: var(--space-sm);
  border: 1px solid var(--color-border-input);
  border-radius: var(--radius);
  font: inherit;
}

.leave-form .link-button {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

/* The checkout (save a payment method) and success pages: a centred column like
   the confirm page. */
.join-checkout,
.join-success {
  display: flex;
  flex-direction: column;
  gap: var(--space);
  max-width: 40rem;
  align-self: center;
  width: 100%;
}

.join-checkout > *,
.join-success > * {
  margin: 0;
}

.payment-note {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

/* Reserve height so the page doesn't jump when Stripe.js mounts the form. */
.checkout-mount {
  min-height: 18rem;
}

.order-recap {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space);
}

.order-recap > * {
  margin: 0;
}

/* === 11. ADMIN BACKOFFICE ================================================ */
/* Admin backoffice (internal, English-only). Reuses the public header, form
   inputs, and buttons; these are the backoffice-specific pieces. The backoffice
   is wider than the public site so the campaign table and its row actions have
   room. */
.admin-main {
  max-width: 72rem;
}

.admin-main a {
  color: var(--color-accent);
}

.admin-form {
  display: flex;
  flex-direction: column;
  gap: var(--space);
}

.admin-form fieldset {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space);
}

.admin-form legend,
.admin-images legend {
  font-weight: 600;
  padding: 0 var(--space-xs);
}

.admin-form label {
  font-weight: 600;
  font-size: var(--text-sm);
}

.admin-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

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

.admin-table th,
.admin-table td {
  text-align: left;
  padding: var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--color-border);
}

/* Failed charges in the charge report: tint the row and bold the outcome so a
   buyer who needs following up stands out from the charged ones. */
.charge-failed-row {
  background: var(--color-danger-bg, #fdecea);
}

.charge-failed {
  color: var(--color-danger, #b3261e);
  font-weight: 600;
}

/* Pending (SEPA, settling) charges: a quieter amber tint — not a problem, just
   not yet confirmed. */
.charge-pending-row {
  background: var(--color-pending-bg, #fff6e5);
}

.charge-pending {
  color: var(--color-pending, #8a5a00);
  font-weight: 600;
}

/* Row actions sit inline; the POST forms (publish, duplicate) must not stack. */
.admin-actions {
  display: flex;
  gap: var(--space-md);
  align-items: center;
}

.admin-action {
  display: inline;
}

/* Image manager on the edit page: a list of thumbnails with reorder/delete. */
/* A bordered section like the form's fieldsets, so the image manager reads as
   its own group. */
.admin-images {
  display: flex;
  flex-direction: column;
  gap: var(--space);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space);
}

.admin-image {
  display: flex;
  gap: var(--space);
  align-items: center;
  padding: var(--space-sm);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
}

.admin-thumb {
  width: 5rem;
  height: 5rem;
}

/* The image section is its own single form (one override for every image
   action), so it does not carry .admin-form; style its add-image inputs and
   submit here, without touching the link-styled move/delete buttons that share
   the form. */
.admin-images input[type="text"],
.admin-images input[type="file"] {
  padding: var(--space-sm);
  border: 1px solid var(--color-border-input);
  border-radius: var(--radius);
  font: inherit;
}

/* Override banner for editing or cancelling a live campaign. */
.admin-warning {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-error);
  border-radius: var(--radius);
}

.admin-confirm {
  display: flex;
  gap: var(--space-sm);
  align-items: center;
}

.admin-confirm label {
  font-weight: 400;
}
