/* Portal stylesheet — layout and type from the SWOPP Website design
   canvas (scratchpad/swopp-capture/design/, extraction in STEP1-1.1);
   ground restyled to clean white per Hugh, 2026-09-03 (the cream paper
   was the old site's). Surfaces separate by edge and elevation, not
   background colour. No brand colour is hardcoded as a fact about a
   cooperative; these are the design system's palette. */

@font-face {
  font-family: 'Archivo';
  src: url('/fonts/archivo-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Archivo';
  src: url('/fonts/archivo-latin-ext.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Anton loads only when the published theme names it (display_font:
   anton — the documented web substitute where a brand book says
   Impact). One weight; body text never uses it. */
@font-face {
  font-family: 'Anton';
  src: url('/fonts/anton-latin.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Anton';
  src: url('/fonts/anton-latin-ext.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  --bg: #FFFFFF;
  --ink: #1F2A18;
  --deep: #1F3A28;
  --deep-hover: #16301F;
  --red: #D3000A; /* the logo's red dot; 5.6:1 against white either way */
  --red-hover: #A80008; /* the accent, pressed: a published theme derives its own */
  --green: #72AB2D;
  --green-strong: #568321;  /* 4.5:1 on white — the display green for big numbers */
  --green-deep: #4E7A1A;    /* 5.1:1 on white — green at small sizes */
  --sage: #AFC2A8;          /* eyebrow tint of --deep; 6.55:1 on it. A theme derives its own */
  --deep-text: #DDE6D9;     /* body text on --deep; a theme derives its own */
  --chip-bg: #F0F6E8;       /* the offer chip: a wash of --green; a theme derives its own */
  --green-ink: #3E6318;     /* chip text and form-ok; ~7:1 on white; a theme derives its own */
  --border: #E3E5DE;
  --border-soft: #ECEEE8;
  --muted: #767463; /* 4.7:1 on white */
  --body: #4E564C;
  --radius-card: 16px;
  --radius-panel: 28px;
  --shadow-card: 0 1px 2px rgba(15, 23, 12, .04), 0 6px 16px rgba(15, 23, 12, .05);
  --font: 'Archivo', system-ui, -apple-system, 'Segoe UI', sans-serif;
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  font-family: var(--font);
  color: var(--ink);
  background: var(--bg);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}
img { max-width: 100%; }
a { color: inherit; }

.eyebrow {
  font-size: 11px; font-weight: 700; letter-spacing: 1.8px;
  text-transform: uppercase; color: var(--muted); margin-bottom: 14px;
}
.eyebrow-light { color: var(--sage); }
.eyebrow-red { color: var(--red); }
.band-deep .eyebrow { color: var(--sage); } /* --red is 2.2:1 on --deep */
.cta .eyebrow-light { color: rgba(255,255,255,.92); } /* sage fails on the red half */

.btn {
  display: inline-block; padding: 14px 28px; border-radius: 999px;
  font-weight: 800; font-size: 15px; text-decoration: none;
  border: 2px solid transparent; cursor: pointer; font-family: var(--font);
}
.btn-red { background: var(--red); color: #fff; }
.btn-red:hover { background: var(--red-hover); }
/* One committed action per section: the secondary reads as a text link,
   not a competing button. */
.btn-ghost {
  border: 0; padding: 14px 8px; color: #fff; font-weight: 700;
  text-decoration: underline; text-underline-offset: 4px;
  text-decoration-thickness: 1.5px;
}
.btn-ghost:hover { opacity: .85; background: none; }
.btn-deep { background: var(--deep); color: #fff; }
.btn-deep:hover { background: var(--deep-hover); }
.btn-block { display: block; text-align: center; }

/* header. THE TOP INSET IS THE STATUS BAR. The member door and the
   signed-in portal are this site's pages too (portal/build.ts dresses
   member/index.html in this header), and installed to a home screen
   they run edge to edge under a translucent status bar (member/
   index.html: viewport-fit=cover, black-translucent). Without this the
   lockup sat under the clock, signal and battery (Hugh, 2026-09-21).
   In a browser tab the inset is 0 and nothing here changes. The strip
   the inset uncovers is painted --deep, the same colour the system
   chrome around the installed app already wears (webmanifest.ts
   chromeColour derives it from the same brand), because the status
   bar's text is white there and white on this header's white ground
   would be a clock nobody can read. It is FIXED and full-bleed rather
   than a band of the header's own background: the header is capped at
   1200px and centred, so its background alone would leave white
   shoulders either side on a wide tablet, and a fixed strip stays
   under the clock while the page scrolls, as the bar of any installed
   app does. Zero-height in a browser, so it paints nothing there. */
.site-header {
  display: flex; align-items: center; justify-content: space-between;
  gap: 24px; max-width: 1200px; margin: 0 auto;
  padding: calc(env(safe-area-inset-top, 0px) + 18px) 24px 18px;
  /* width, stated: on the member door the body is a flex column
     (member.css, the chrome height contract) and a flex item with auto
     side margins is sized to its content, not stretched — the header
     would shrink to logo-plus-toggle. In block flow this line changes
     nothing. */
  width: 100%;
}
.site-header::before {
  content: ''; position: fixed; top: 0; left: 0; right: 0; z-index: 30;
  height: env(safe-area-inset-top, 0px); background: var(--deep);
}
.brand { text-decoration: none; display: flex; align-items: center; }
.brand-mark { font-weight: 900; font-size: 22px; letter-spacing: .5px; color: var(--deep); text-transform: uppercase; }
.brand-logo { height: 40px; width: auto; display: block; }
.site-nav { display: flex; align-items: center; gap: 22px; }
.site-nav a { text-decoration: none; font-weight: 700; font-size: 15px; color: var(--ink); }
.site-nav a:hover { color: var(--red); }
.site-nav a[aria-current="page"] { color: var(--red); }
.site-nav .btn-nav { color: #fff; padding: 10px 20px; }
/* the member's own door, beside the visitor's. An outlined link, not a
   filled button: joining is what the site is selling, signing in is
   what a member already has — one CTA in the nav, and this is not it. */
.site-nav .nav-signin { border: 2px solid var(--deep); border-radius: 999px; padding: 8px 18px; color: var(--deep); }
.site-nav .nav-signin:hover { background: var(--deep); color: #fff; }
.nav-toggle { display: none; background: none; border: 0; padding: 8px; cursor: pointer; }
.nav-toggle span { display: block; width: 22px; height: 3px; background: var(--ink); margin: 4px 0; border-radius: 2px; }

/* hero — the split (ruling 2026-09-04). Copy on white at left, the
   image full-bleed at right and untouched: it is the asset, and nothing
   is laid over it. Phones stack the image on top. Ink on white needs no
   measuring, whatever the cooperative's picture is. */
.hero {
  display: grid; grid-template-columns: minmax(0, 46%) minmax(0, 54%);
  position: relative; overflow: hidden; background: #fff; border: 1px solid var(--border);
  width: calc(100% - 32px); max-width: 1360px; margin: 8px auto 0;
  border-radius: var(--radius-panel);
}
.hero-noimg { grid-template-columns: 1fr; }
.hero-copy { display: flex; align-items: center; }
.hero-inner { position: relative; max-width: 640px; padding: calc(var(--section-space, 1) * 72px) 48px calc(var(--section-space, 1) * 72px) 56px; color: var(--ink); }
.hero-media { position: relative; min-height: 560px; }
.hero-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.hero .eyebrow-light { color: var(--green-deep); }
.hero .btn-ghost { color: var(--ink); border-color: var(--ink); }
.hero-heading {
  font-size: clamp(36px, 5.5vw, 56px); font-weight: 900; text-transform: uppercase;
  line-height: 1.05; letter-spacing: .5px; margin: 0 0 20px; max-width: 820px;
}
.hero-heading .accent { color: var(--green); }
.hero-sub { font-size: 18px; max-width: 600px; color: var(--body); margin: 0 0 32px; }
.hero-ctas { display: flex; gap: 18px; flex-wrap: wrap; align-items: center; }
.hero-note { margin-top: 26px; font-size: 14px; color: var(--muted); }
.hero-note a { color: var(--ink); font-weight: 700; }

/* stats band — the numbers carry it: big, green, unfenced */
.stats {
  display: grid; grid-template-columns: repeat(3, 1fr);
  max-width: 1200px; margin: 0 auto; padding: calc(var(--section-space, 1) * 72px) 24px calc(var(--section-space, 1) * 24px); gap: 24px;
}
/* The column count is an attribute the renderer sets from the block
   count, read here — never an inline style: the site's CSP admits none. */
.stats[data-cols="1"] { grid-template-columns: 1fr; }
.stats[data-cols="2"] { grid-template-columns: repeat(2, 1fr); }
.stats[data-cols="4"] { grid-template-columns: repeat(4, 1fr); }
.stats[data-cols="5"] { grid-template-columns: repeat(5, 1fr); }
.stats[data-cols="6"] { grid-template-columns: repeat(6, 1fr); }
.stat { text-align: center; padding: 10px 16px; }
.stat-value { font-size: clamp(44px, 5vw, 60px); font-weight: 900; color: var(--green-strong); letter-spacing: -1px; line-height: 1; }
.stat-label { font-size: 11px; font-weight: 700; letter-spacing: 1.6px; text-transform: uppercase; color: var(--muted); margin-top: 10px; }

/* generic bands — white ground; surfaces separate by edge and elevation */
.band { padding: calc(var(--section-space, 1) * 96px) 24px; }
.band > * { max-width: 1200px; margin-left: auto; margin-right: auto; }
.band-white { background: var(--bg); }
.band-paper { background: var(--bg); }
.band-deep {
  background: var(--deep); color: var(--deep-text);
  width: calc(100% - 32px); max-width: 1360px; margin: 0 auto;
  border-radius: var(--radius-panel); padding: calc(var(--section-space, 1) * 88px) 48px;
}
.band-deep .band-heading { color: #fff; }
.band-head { margin-bottom: 52px; }
.band-head.center { text-align: center; }
.band-head.row { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
.band-heading { font-size: clamp(28px, 3.6vw, 40px); font-weight: 900; margin: 0; color: var(--display, var(--heading, var(--deep))); line-height: 1.12; letter-spacing: -.3px; }
.band-sub { color: var(--body); max-width: 620px; margin: 14px 0 0; font-size: 17px; }
.band-head.center .band-sub { margin-left: auto; margin-right: auto; }
.band-link { font-weight: 800; color: var(--red); text-decoration: none; }

/* steps */
.steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px; }
.steps[data-cols="1"] { grid-template-columns: 1fr; }
.steps[data-cols="2"] { grid-template-columns: repeat(2, 1fr); }
.steps[data-cols="3"] { grid-template-columns: repeat(3, 1fr); }
.steps[data-cols="5"] { grid-template-columns: repeat(5, 1fr); }
.steps[data-cols="6"] { grid-template-columns: repeat(6, 1fr); }
.step { background: #fff; border: 1px solid var(--border-soft); border-radius: var(--radius-card); padding: 30px 26px; box-shadow: var(--shadow-card); }
.step-num { font-size: 13px; font-weight: 900; color: var(--green-deep); letter-spacing: 1px; margin-bottom: 12px; }
.step-heading { font-size: 18px; font-weight: 800; margin: 0 0 8px; color: var(--heading, var(--deep)); }
.step-body { font-size: 15px; color: var(--body); margin: 0; }

/* cards */
.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
.card { background: #fff; border: 1px solid var(--border-soft); border-radius: var(--radius-card); padding: 30px 26px; box-shadow: var(--shadow-card); }
.card-heading { font-size: 18px; font-weight: 800; margin: 0 0 8px; color: var(--heading, var(--deep)); }
.card-body { font-size: 15px; color: var(--body); margin: 0; }
/* A card may carry a face and a role (the team pages, 2026-09-21). Both
   are optional and a card with neither is the card above, untouched —
   the renderer emits nothing for an empty one, so removing a photo in
   the editor removes it from the page rather than leaving a gap. */
.card-photo {
  width: 96px; height: 96px; border-radius: 50%; object-fit: cover;
  display: block; margin: 0 0 16px; background: var(--border-soft);
}
.card-role {
  font-size: 12px; font-weight: 800; letter-spacing: 1.5px;
  text-transform: uppercase; color: var(--red); margin: 0 0 10px;
}
.card-heading + .card-role { margin-top: -4px; }

/* split */
.split { display: grid; grid-template-columns: 1.2fr .8fr; gap: 56px; align-items: start; }
/* A prose body is now one <p> PER PARAGRAPH (the renderer's paras()),
   so the gap between them is set here rather than left to the browser's
   default 1em. margin-top and not margin-bottom, so the first paragraph
   keeps exactly the gap it had from the heading when the whole body was
   a single <p> — this rule changes the spacing BETWEEN paragraphs and
   deliberately nothing else. */
.split-body { color: inherit; font-size: 16px; max-width: 62ch; margin: 16px 0 0; }
.band-white .split-body { color: var(--body); }
.person-card {
  background: #fff; border: 1px solid var(--border-soft); border-radius: var(--radius-card);
  padding: 28px; display: flex; gap: 18px; align-items: flex-start; color: var(--ink);
  box-shadow: var(--shadow-card);
}
.person-photo { width: 84px; height: 84px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
.person-name { font-weight: 900; font-size: 18px; color: var(--heading, var(--deep)); }
.person-role { font-size: 12px; font-weight: 800; letter-spacing: 1.5px; text-transform: uppercase; color: var(--red); margin: 2px 0 10px; }
.person-body { font-size: 14px; color: var(--body); margin: 0; }
.person-body + .person-body { margin-top: 10px; }
.aside-note { background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.18); border-radius: 16px; padding: 26px; }
.aside-body { margin: 0; }
.aside-body + .aside-body { margin-top: 12px; }

/* benefits + fee card */
.benefit-list { display: grid; grid-template-columns: 1fr 1fr; gap: 18px 26px; align-content: start; }
.benefit-heading { font-size: 16px; font-weight: 800; margin: 0 0 6px; color: var(--heading, var(--deep)); }
.benefit-body { font-size: 15px; color: var(--body); margin: 0; }
.fee-card { background: #fff; border: 1px solid var(--border-soft); border-radius: var(--radius-card); padding: 32px; box-shadow: var(--shadow-card); }
.fee-heading { font-size: 30px; font-weight: 900; color: var(--display, var(--heading, var(--deep))); margin: 0 0 10px; }
.fee-note { font-size: 13px; color: var(--muted); margin: 0 0 14px; }
.fee-includes { font-size: 14px; color: var(--body); margin: 0 0 20px; }

/* partner logo strip */
.logo-strip { display: grid; grid-template-columns: repeat(6, 1fr); gap: 20px; }
.logo-tile {
  background: #fff; border: 1px solid var(--border-soft); border-radius: 12px;
  display: flex; align-items: center; justify-content: center; padding: 18px; min-height: 96px;
  box-shadow: var(--shadow-card);
}
.logo-tile img { max-height: 60px; object-fit: contain; }

/* CTA pair — an inset rounded panel, like the hero */
.cta-pair {
  display: grid; grid-template-columns: 1fr 1fr;
  width: calc(100% - 32px); max-width: 1360px; margin: 0 auto 96px;
  border-radius: var(--radius-panel); overflow: hidden;
}
.cta { padding: calc(var(--section-space, 1) * 72px) 56px; color: #fff; }
/* A cta panel names what it paints, not what the default palette
   happened to resolve (181): deep is the theme deep, accent the theme
   accent. */
.cta-deep { background: var(--deep); }
.cta-accent { background: var(--red); }
.cta-heading { font-size: 28px; font-weight: 900; margin: 0 0 10px; }
.cta-body { max-width: 460px; opacity: .9; margin: 0 0 24px; }

/* prose */
.prose { max-width: 680px; }
.prose-para { color: var(--body); }
.prose-heading { font-size: 19px; font-weight: 800; color: var(--heading, var(--deep)); margin: 28px 0 6px; }
.prose-meta { font-size: 13px; color: var(--muted); }

/* directory */
.dir { display: grid; grid-template-columns: 260px 1fr; gap: 34px; align-items: start; }
.dir-facets { position: sticky; top: 16px; }
.dir-search {
  width: 100%; padding: 12px 16px; border-radius: 999px; border: 1px solid var(--border);
  font-family: var(--font); font-size: 15px; background: #fff; margin-bottom: 20px;
}
.facet { border: 0; padding: 0; margin: 0 0 20px; }
.facet-title { font-size: 11px; font-weight: 700; letter-spacing: 1.6px; text-transform: uppercase; color: var(--muted); margin-bottom: 8px; }
.facet-opt { display: block; font-size: 14px; color: var(--body); padding: 3px 0; cursor: pointer; }
.dir-count { font-size: 13px; color: var(--muted); margin: 0 0 14px; }
/* The uploaded partner list (214). It sits under the band's heading and
   above the facets, because it is an alternative to searching rather
   than a result of it: somebody who wants the whole list on paper
   should not have to scroll a filtered directory to find that out. The
   date rides with the button and never wraps away from it. */
.dir-download { margin: 22px 0 0; display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.dir-download-note { font-size: 13px; color: var(--muted); }
.band-deep .dir-download-note { color: rgba(255,255,255,.78); }
.dir-list { display: grid; gap: 14px; }
/* The filter script hides cards with the hidden attribute; any author
   display rule (like .dir-card's flex) beats the UA sheet's [hidden],
   so the attribute needs its own author rule to mean anything. */
[hidden] { display: none !important; }
.dir-card {
  display: flex; gap: 18px; background: #fff; border: 1px solid var(--border-soft);
  border-radius: var(--radius-card); padding: 20px 22px; text-decoration: none; align-items: center;
  box-shadow: var(--shadow-card);
}
.dir-card:hover { border-color: var(--green-strong); }
.dir-logo { width: 72px; height: 72px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.dir-logo img { max-height: 64px; object-fit: contain; }
.dir-card-main { min-width: 0; }
.dir-card-head { display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap; }
.dir-name { font-weight: 800; font-size: 17px; color: var(--heading, var(--deep)); }
.dir-meta { font-size: 13px; color: var(--muted); }
.offer-chip {
  display: inline-block; margin-top: 8px; padding: 6px 12px; border-radius: 8px;
  background: var(--chip-bg); color: var(--green-ink); font-size: 13px; font-weight: 700;
}
.dir-tags { margin-top: 8px; }
.tag {
  display: inline-block; font-size: 12px; color: var(--body); background: #F4F6F1;
  border: 1px solid var(--border); border-radius: 999px; padding: 3px 10px; margin: 2px 4px 2px 0;
}

/* partner detail */
/* `margin: 0 auto`, AND THE auto IS LOAD-BEARING. Every direct child of
   a band is centred by `.band > * { max-width: 1200px; margin-left: auto;
   margin-right: auto }`. This rule has the same specificity and comes
   later in the file, so a `margin: 0 0 22px` here silently won that
   fight and pinned the crumbs to the viewport edge while the rest of the
   page sat in the column — Hugh, 2026-09-22: "this weird tucked away
   button on the left". Any margin shorthand on a band child must keep
   the auto. */
.crumbs { font-size: 13px; color: var(--muted); margin: 0 auto 22px; }
.crumbs a { color: var(--red); font-weight: 700; text-decoration: none; }
.back-link { display: inline-flex; align-items: center; gap: 7px; font-size: 14px; }
.back-link:hover { text-decoration: underline; text-underline-offset: 3px; }
.back-arrow { width: 15px; height: 15px; flex: none; transition: transform .12s ease; }
.back-link:hover .back-arrow { transform: translateX(-3px); }
.partner-head { display: flex; gap: 22px; align-items: center; margin-bottom: 30px; }
.partner-logo { max-height: 88px; max-width: 160px; object-fit: contain; }
.partner-name { font-size: clamp(26px, 3.4vw, 38px); font-weight: 900; color: var(--display, var(--heading, var(--deep))); margin: 0; }
.partner-meta { color: var(--muted); font-size: 14px; margin: 6px 0 0; }
.partner-cols { display: grid; grid-template-columns: 1.4fr .6fr; gap: 40px; align-items: start; }
.arrangement { background: #fff; border: 1px solid var(--border-soft); border-left: 4px solid var(--green-strong); border-radius: 0 var(--radius-card) var(--radius-card) 0; padding: 24px 28px; margin-bottom: 24px; box-shadow: var(--shadow-card); }
.arrangement-text { font-size: 18px; font-weight: 700; color: var(--deep); margin: 8px 0; }
.arrangement-note { font-size: 12px; color: var(--muted); margin: 0; }
.partner-desc { color: var(--body); }
.locations { margin-top: 28px; }
.loc-chips { margin-top: 10px; }

/* Locations: the map of Western Australia and the card behind each dot
   (230). The map is an inline SVG because the site's CSP admits no tile
   host and must not be widened to admit one for the sake of a dozen
   dots — see src/pipeline/wa-map.ts. It carries no brand colour of its
   own: the land is a neutral wash and the pins take --red, so the
   published theme moves them with everything else. */
.loc-layout { margin-top: 14px; }
.loc-map { margin: 14px 0 22px; }
.wa-map { display: block; width: 100%; max-width: 300px; height: auto; margin: 0 auto; overflow: visible; }
/* STROKE WIDTHS ARE IN PIXELS BECAUSE THE PATHS CARRY
   vector-effect="non-scaling-stroke" (render.ts): a stroke measured in
   drawing units would thicken with every zoom step until the white ring
   was wider than the dot inside it. 1.3px is what 3 units rendered as at
   the map's 300px width, so the map at rest looks as it did. */
.wa-outline { fill: var(--border-soft); stroke: var(--border); stroke-width: 1.3px; stroke-linejoin: round; }
.wa-pin { fill: var(--red); stroke: #fff; stroke-width: 1.3px; transition: r .12s ease; }
.wa-pin-halo { fill: var(--red); opacity: 0; transition: opacity .12s ease; }
.wa-pin-link { cursor: pointer; }
.wa-pin-link:hover .wa-pin-halo,
.wa-pin-link:focus-visible .wa-pin-halo,
.wa-pin-link[data-loc-open] .wa-pin-halo { opacity: .3; }
/* NO `r` RULE ON HOVER. The script keeps a pin the same size on screen
   at every zoom by setting the r ATTRIBUTE, and a CSS r would override
   it — a hovered pin at 8x would jump to eight times its neighbours.
   The halo below is the hover affordance instead, and it works at every
   zoom because opacity has no size. */
.wa-pin-link:focus-visible { outline: 2px solid var(--deep); outline-offset: 2px; }
/* ZOOM CONTROLS. Built by client.js, never rendered into the page, so a
   visitor without the script sees no buttons rather than dead ones. */
.wa-zoom { display: flex; justify-content: center; gap: 6px; margin: 8px 0 0; }
.wa-zoom-btn {
  font: inherit; font-size: 12px; font-weight: 700; line-height: 1;
  min-width: 30px; padding: 6px 9px; cursor: pointer;
  color: var(--deep); background: #fff;
  border: 1px solid var(--border); border-radius: 6px;
}
.wa-zoom-btn:hover:not(:disabled) { border-color: var(--deep); background: var(--border-soft); }
.wa-zoom-btn:focus-visible { outline: 2px solid var(--deep); outline-offset: 2px; }
.wa-zoom-btn:disabled { opacity: .4; cursor: default; }

/* ONLY ONCE ZOOMED. `overflow: visible` above lets a coastal pin's halo
   sit proud of the edge; zoomed in, the same rule would spill the whole
   state across the page, so the map clips while there is anything
   outside it. touch-action stops a drag scrolling the page instead. */
.loc-map[data-zoomed] .wa-map { overflow: hidden; cursor: grab; touch-action: none; }
.loc-map[data-zoomed] .wa-map:active { cursor: grabbing; }

.loc-map-note { font-size: 13px; color: var(--muted); text-align: center; margin: 10px 0 0; }

.loc-list { border-top: 1px solid var(--border-soft); }
.loc { border-bottom: 1px solid var(--border-soft); }
.loc-summary {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px;
  padding: 14px 4px; cursor: pointer; list-style: none;
}
.loc-summary::-webkit-details-marker { display: none; }
.loc-summary::after {
  content: '+'; margin-left: auto; font-size: 18px; font-weight: 700;
  color: var(--muted); line-height: 1;
}
.loc[open] .loc-summary::after { content: '\2212'; }
.loc-summary:hover .loc-name { color: var(--red); }
.loc-summary:focus-visible { outline: 2px solid var(--deep); outline-offset: -2px; }
.loc-name { font-weight: 700; color: var(--heading, var(--deep)); font-size: 15px; }
.loc-badge {
  display: inline-block; margin-left: 8px; padding: 2px 8px; border-radius: 999px;
  background: var(--border-soft); color: var(--muted);
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
}
.loc-town { font-size: 13px; color: var(--muted); }
/* The open card is marked so a member who arrived by clicking a dot can
   see which one the page moved them to. */
.loc[open] { background: #fff; }
.loc-body { padding: 0 4px 18px; }
.loc-address { font-style: normal; color: var(--body); font-size: 14px; line-height: 1.6; margin: 0 0 12px; }
.loc-facts { display: grid; grid-template-columns: auto 1fr; gap: 6px 16px; margin: 0; font-size: 14px; }
.loc-facts dt { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .04em; padding-top: 2px; }
.loc-facts dd { margin: 0; color: var(--body); }
.loc-facts a { color: var(--deep); font-weight: 700; text-decoration: none; }
.loc-facts a:hover { color: var(--red); }
.loc-member-note { font-size: 13px; color: var(--muted); margin: 16px 0 0; }
.loc-member-note a { color: var(--red); font-weight: 700; text-decoration: none; }
.contact-card, .related { background: #fff; border: 1px solid var(--border-soft); border-radius: var(--radius-card); padding: 24px; margin-bottom: 18px; box-shadow: var(--shadow-card); }
.contact-card p { margin: 8px 0 0; font-size: 14px; }
.contact-card a { color: var(--deep); font-weight: 700; text-decoration: none; }
.contact-card a:hover { color: var(--red); }
.hours { color: var(--body); }
.social a { word-break: break-all; }

/* 232: the four benefit badges, and a location's week.
   The badges reuse .tag's pill shape rather than inventing a second
   one — they sit under the partner's name where the directory's
   category tags sit under its card, and two pill shapes on one page
   reads as two kinds of thing when it is one. */
.badges { list-style: none; display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 0; padding: 0; }
.badge {
  font-size: 12px; font-weight: 700; color: var(--green-ink, var(--deep));
  background: var(--chip-bg); border-radius: 999px; padding: 4px 11px;
}
/* The week is a two-column list and not a <table>: it is a label and a
   value seven times over, which is what .loc-facts already is, and a
   table would announce seven rows of data to a screen reader reading a
   shop's opening times. */
.week { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: auto 1fr; gap: 2px 14px; }
.week li { display: contents; }
.week .day { color: var(--muted); }
.week .when { color: var(--body); font-variant-numeric: tabular-nums; }
.week .day-closed .when { color: var(--muted); }
/* A free-text segment that carries no day label — "Open 24 hours" — runs
   the full width rather than being forced into the day column. `.week li`
   is display:contents, so this one has to become a grid item itself. */
.week li.whole { display: block; grid-column: 1 / -1; }
.related-link { display: block; margin-top: 10px; font-size: 14px; font-weight: 700; color: var(--deep); text-decoration: none; }
.related-link:hover { color: var(--red); }

/* forms */
.form-wrap { max-width: 640px; }
.enquiry { margin-top: 10px; }
.field { display: block; margin-bottom: 16px; font-size: 13px; font-weight: 800; color: var(--deep); }
.field input, .field textarea {
  display: block; width: 100%; margin-top: 6px; padding: 12px 14px; font-size: 15px;
  font-family: var(--font); border: 1px solid var(--border); border-radius: 10px; background: #fff;
}
.field input:focus, .field textarea:focus { outline: 2px solid var(--green); border-color: var(--green); }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.hp { position: absolute; left: -9999px; top: -9999px; }
/* VISUALLY HIDDEN, BUT STILL READ. Not .hp: that moves the honeypot off
   the canvas entirely, which is what we want for a field no human
   should reach, and is exactly wrong for a label a screen reader must.
   The footer's sign-up shows its labels as placeholders, and a
   placeholder is not a label — the input still needs one. */
.vh {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
.consent { display: flex; gap: 10px; align-items: flex-start; font-size: 13px; color: var(--body); margin: 18px 0 22px; }
.consent a { color: var(--red); font-weight: 700; }
.form-status { font-size: 14px; font-weight: 700; margin-top: 14px; min-height: 20px; }
.form-status.ok { color: var(--green-ink); }
.form-status.err { color: var(--red); }

/* the newsletter archive (244). Reuses .cards and .card so an issue
   sits in the same grid as every other card on the page; these rules
   are only what a DOWNLOAD card needs on top of one. */
/* A COLUMN, SO THE LINK SITS ON THE FLOOR OF THE CARD. The summary is
   optional (244), so a row mixing issues that have one with issues that
   do not put "Read this issue" at three different heights — the one
   thing a reader's eye tracks across a row of cards. margin-top: auto
   on the link is what lines them up; `display: block` cannot. */
.news-card { display: flex; flex-direction: column; text-decoration: none; color: inherit; }
.news-card:hover { box-shadow: var(--shadow-card), 0 0 0 2px var(--red); }
.news-date {
  font-size: 11px; font-weight: 700; letter-spacing: 1.2px; text-transform: uppercase;
  color: var(--muted); margin-bottom: 8px;
}
.news-open {
  align-self: flex-start; margin-top: auto; padding-top: 10px; font-size: 14px; font-weight: 800;
  color: var(--red); text-decoration: underline; text-underline-offset: 3px;
}

/* footer */
/* THE FOOTER IS INK, AND THE BRAND COLOUR IS A RULE AND A BAR (reskin,
   2026-09-22, "Option 1b — INK"). Until this, a cooperative's chosen
   footer colour (228) was the ground of the whole band, and GOPP's site
   ended in a wall of orange. It now buys two things instead: the 5px
   rule across the top of the band, and the legal bar underneath it.
   Same colour, same swatch list, less of it.

   EVERY --footer-* BELOW CARRIES A FALLBACK, and that is load-bearing
   rather than tidy: themeCss() emits these only when a theme names a
   footer colour, so a site that names none — and AOPP, which has no
   theme at all — renders from the fallbacks alone. The accent falls
   back to var(--red), which IS the theme's accent on a themed site and
   the stylesheet's own red otherwise; accentSwatches guarantees white
   clears 4.5:1 on either, so the legal bar is readable in every case
   without the theme having said anything.

   THE BODY OF THE FOOTER IS NOT THEMED. #141311 on both sites, by the
   design — a neutral warm black rather than --ink, which is a green
   black and reads as SWOPP's on GOPP's page. */
.site-footer { background: #141311; color: rgba(255,255,255,.92); margin-top: 24px; }

/* The brand's 5px signature across the top of the band. */
.footer-accent { height: 5px; background: var(--footer-accent, var(--red)); }

.footer-body { max-width: 1200px; margin: 0 auto; padding: 48px 24px 38px; }
/* Brand column wide enough for the tagline and the signup row; two link
   columns; the contact column sized to its longest line. */
.footer-cols {
  display: grid; grid-template-columns: minmax(0, 1.7fr) 1fr 1fr minmax(210px, auto);
  gap: 24px 44px; align-items: start;
}
.footer-lockup { display: flex; flex-direction: column; gap: 22px; min-width: 0; }
/* align-self, and it is not optional. The lockup sits in a flex COLUMN,
   whose default align-items is stretch, so a block image with width:auto
   resolves that auto to the full column width and the artwork comes out
   horizontally squashed — SWOPP's 5.3:1 wordmark rendered at 8:1 on
   desktop and 13:1 at tablet width, wrong on every page, and wrong in a
   way that reads as "the logo file is bad" rather than as a CSS bug.
   max-width keeps a very wide lockup inside a narrow column instead of
   overflowing it. */
.footer-logo { height: 46px; width: auto; max-width: 100%; object-fit: contain; align-self: flex-start; display: block; }
/* Only rendered where a cooperative has NO footer artwork — the lockup
   is the name everywhere else (render.ts, footer()). */
.footer-brand { font-weight: 900; font-size: 20px; text-transform: uppercase; letter-spacing: .5px; color: #fff; }
.footer-tagline { font-size: 16px; line-height: 1.5; font-weight: 500; max-width: 330px; margin: 0; color: #fff; }

.footer-col-heading {
  font-size: 11px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase;
  color: #fff; margin: 0 0 14px;
}
.footer-nav { display: grid; gap: 11px; align-content: start; }
.footer-nav a { font-size: 15px; color: rgba(255,255,255,.92); text-decoration: none; }
.footer-nav a:hover { color: #fff; text-decoration: underline; text-underline-offset: 3px; }

/* The contact column: register facts first, then the authored lines. */
/* overflow-wrap, because the longest single word on this page is an
   email address and the column is the narrowest thing on a phone. */
.footer-contact { display: grid; gap: 9px; font-size: 15px; line-height: 1.5; align-content: start; overflow-wrap: anywhere; }
.footer-contact a { color: #fff; text-decoration: underline; text-underline-offset: 3px; }
.footer-contact-quiet { color: rgba(255,255,255,.68); font-size: 14px; white-space: pre-line; }

/* Social tiles. Each network keeps its own colour, per the design — the
   glyphs are white on all four, and each ground clears it. */
.footer-social { display: flex; gap: 10px; flex-wrap: wrap; }
.footer-social a {
  width: 36px; height: 36px; border-radius: 8px; display: inline-flex;
  align-items: center; justify-content: center; background: rgba(255,255,255,.12); color: #fff;
}
.footer-social a:hover { filter: brightness(1.15); }
.footer-social svg { width: 18px; height: 18px; display: block; fill: currentColor; }
.footer-social .soc-facebook  { background: #0C63D4; }
.footer-social .soc-instagram { background: #E1306C; border-radius: 10px; }
.footer-social .soc-linkedin  { background: #0A66C2; }
.footer-social .soc-youtube   { background: #FF0000; }
/* X's own colour is black, which on a #141311 band is a tile you can
   only find by hovering. A hairline gives it an edge without making it
   a colour X does not use. */
.footer-social .soc-x         { background: #000; box-shadow: inset 0 0 0 1px rgba(255,255,255,.34); }

/* The legal bar: the brand colour again, with the ink derived for it. */
/* The newsletter sign-up (244). Three stacked fields with the button
   beside the last — the design draws one row, and the capture needs a
   business and a contact name as well as an address (render.ts says
   why), so the row it draws is the last of three rather than the only
   one. */
.footer-signup { display: grid; gap: 8px; max-width: 360px; margin: -4px 0 0; }
.footer-signup input {
  width: 100%; height: 44px; padding: 0 14px; font-size: 14px; font-family: var(--font);
  color: #fff; background: rgba(255,255,255,.08);
  border: 1px solid rgba(255,255,255,.16); border-radius: 4px;
}
/* The placeholder IS the visible label here, so it has to clear 4.5:1
   on the field's ground rather than being the usual whisper. */
.footer-signup input::placeholder { color: rgba(255,255,255,.66); opacity: 1; }
.footer-signup input:focus { outline: 2px solid var(--footer-accent, var(--red)); outline-offset: 1px; }
.footer-signup-row { display: flex; gap: 8px; }
.footer-signup-row input { min-width: 0; flex: 1; }
.footer-signup-btn {
  height: 44px; padding: 0 22px; border: 0; border-radius: 4px; cursor: pointer;
  background: var(--footer-accent, var(--red)); color: var(--footer-accent-ink, #fff);
  font-family: var(--font); font-size: 14px; font-weight: 700; white-space: nowrap;
}
.footer-signup-btn:hover { filter: brightness(1.08); }
.footer-signup-btn:disabled { opacity: .6; cursor: default; }
.footer-signup-note { margin: 2px 0 0; font-size: 12px; line-height: 1.5; color: rgba(255,255,255,.68); }
.footer-signup-note a { color: rgba(255,255,255,.92); text-decoration: underline; text-underline-offset: 2px; }
/* No reserved height: an empty status left a 40px hole between the
   sign-up and the social tiles on every page, to avoid a shift that
   only ever happens once, on submit, in a footer nobody is reading at
   that moment. The hole was the worse trade. */
.footer-signup-status:empty { display: none; }
.footer-signup-status { margin: 0; font-size: 13px; font-weight: 700; }
.footer-signup-status.ok { color: #9BE8A8; }
.footer-signup-status.err { color: #FFB4AE; }

.footer-legal { background: var(--footer-accent, var(--red)); color: var(--footer-accent-ink, #fff); }
.footer-legal-inner {
  max-width: 1200px; margin: 0 auto; padding: 14px 24px;
  display: flex; justify-content: space-between; align-items: center;
  gap: 16px 36px; flex-wrap: wrap; font-size: 12.5px; line-height: 1.6;
}
.footer-built { white-space: nowrap; }

/* responsive */
@media (max-width: 900px) {
  .band { padding: calc(var(--section-space, 1) * 72px) 24px; }
  .band-deep { padding: calc(var(--section-space, 1) * 64px) 32px; }
  .hero { grid-template-columns: 1fr; }
  .hero-media { order: -1; min-height: 0; aspect-ratio: 4 / 3; }
  .hero-inner { max-width: none; padding: calc(var(--section-space, 1) * 40px) 32px calc(var(--section-space, 1) * 48px); }
  .cta { padding: calc(var(--section-space, 1) * 56px) 36px; }
  .split, .partner-cols, .dir { grid-template-columns: 1fr; }
  .wa-map { max-width: 240px; }
  .loc-facts { grid-template-columns: 1fr; gap: 2px; }
  .loc-facts dt { padding-top: 8px; }
  .dir-facets { position: static; }
  .steps, .steps[data-cols] { grid-template-columns: 1fr 1fr; }
  /* Four or more stats halve at tablet width; three still fit across. */
  .stats[data-cols="4"], .stats[data-cols="5"], .stats[data-cols="6"] { grid-template-columns: repeat(2, 1fr); }
  .cards { grid-template-columns: 1fr; }
  .benefit-list { grid-template-columns: 1fr; }
  .logo-strip { grid-template-columns: repeat(3, 1fr); }
  .cta-pair { grid-template-columns: 1fr; }
  /* Three across is the wrong shape at tablet width: the brand column
     needs its own row, the other three share one. */
  .footer-cols { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px; }
  .footer-lockup { grid-column: 1 / -1; }
  .footer-body { padding: 40px 24px 32px; }
}
@media (max-width: 700px) {
  /* ONE column. At 390px the tablet's three put "Member sign in" on two
     lines and ran the email address off the side of the page. */
  .footer-cols { grid-template-columns: 1fr; gap: 30px; }
  .footer-lockup { grid-column: auto; }
  .footer-body { padding: 36px 20px 30px; }
  .footer-legal-inner { padding: 14px 20px; }
  .band { padding: calc(var(--section-space, 1) * 56px) 20px; }
  .hero, .band-deep, .cta-pair { width: calc(100% - 20px); border-radius: 20px; }
  .hero-inner { padding: calc(var(--section-space, 1) * 36px) 24px calc(var(--section-space, 1) * 44px); }
  .nav-toggle { display: block; }
  .site-nav {
    display: none; position: absolute; top: calc(env(safe-area-inset-top, 0px) + 64px); left: 0; right: 0; z-index: 20;
    flex-direction: column; align-items: stretch; background: var(--bg);
    border-bottom: 1px solid var(--border); padding: 16px 24px; gap: 14px;
  }
  .site-nav.open { display: flex; }
  .site-header { position: relative; }
  .steps, .steps[data-cols] { grid-template-columns: 1fr; }
  .field-row { grid-template-columns: 1fr; }
  /* Phone: 2x2. Four numbers stacked one per row ran 570px tall; two across
     keeps the band to half that with the labels wrapping to two lines. */
  .stats, .stats[data-cols] { grid-template-columns: repeat(2, 1fr); gap: 32px 20px; padding-top: calc(var(--section-space, 1) * 56px); }
  .stats[data-cols="1"] { grid-template-columns: 1fr; }
  .stat-label { letter-spacing: .8px; }
}

/* Section spacing (200). One ratio per named scale, multiplying every
   section wrapper's own vertical padding — the band's 96px and the
   hero's 72px scale together, and the breakpoint bases above scale the
   same way, so "spacious" on a phone is 1.5 × 56px, never 1.5 × 96px.
   Horizontal gutters are deliberately outside the multiplier. The class
   sits on the <section>; the property inherits to .hero-inner and .cta
   inside it. Emitted by portal/theme.ts sectionSpacingClass — the one
   resolver — and no class means the platform rhythm (the multiplier's
   fallback is 1). The names and ratios must agree with
   SECTION_SPACING_RATIOS there; a test holds the two together. */
.sec-sp-compact { --section-space: 0.75; }
.sec-sp-default { --section-space: 1; }
.sec-sp-comfortable { --section-space: 1.25; }
.sec-sp-spacious { --section-space: 1.5; }
/* A dragged size (2026-09-28): the editor's drag stores a multiplier in
   5% steps from 0 to 200%, and each step is a class because the CSP
   drops inline styles. Generated from spacingStepRules() in
   portal/theme.ts; tests/website-section-spacing.test.ts holds the two
   to each other, so do not edit these by hand. */
.sec-sp-p0 { --section-space: 0; }
.sec-sp-p5 { --section-space: 0.05; }
.sec-sp-p10 { --section-space: 0.1; }
.sec-sp-p15 { --section-space: 0.15; }
.sec-sp-p20 { --section-space: 0.2; }
.sec-sp-p25 { --section-space: 0.25; }
.sec-sp-p30 { --section-space: 0.3; }
.sec-sp-p35 { --section-space: 0.35; }
.sec-sp-p40 { --section-space: 0.4; }
.sec-sp-p45 { --section-space: 0.45; }
.sec-sp-p50 { --section-space: 0.5; }
.sec-sp-p55 { --section-space: 0.55; }
.sec-sp-p60 { --section-space: 0.6; }
.sec-sp-p65 { --section-space: 0.65; }
.sec-sp-p70 { --section-space: 0.7; }
.sec-sp-p75 { --section-space: 0.75; }
.sec-sp-p80 { --section-space: 0.8; }
.sec-sp-p85 { --section-space: 0.85; }
.sec-sp-p90 { --section-space: 0.9; }
.sec-sp-p95 { --section-space: 0.95; }
.sec-sp-p100 { --section-space: 1; }
.sec-sp-p105 { --section-space: 1.05; }
.sec-sp-p110 { --section-space: 1.1; }
.sec-sp-p115 { --section-space: 1.15; }
.sec-sp-p120 { --section-space: 1.2; }
.sec-sp-p125 { --section-space: 1.25; }
.sec-sp-p130 { --section-space: 1.3; }
.sec-sp-p135 { --section-space: 1.35; }
.sec-sp-p140 { --section-space: 1.4; }
.sec-sp-p145 { --section-space: 1.45; }
.sec-sp-p150 { --section-space: 1.5; }
.sec-sp-p155 { --section-space: 1.55; }
.sec-sp-p160 { --section-space: 1.6; }
.sec-sp-p165 { --section-space: 1.65; }
.sec-sp-p170 { --section-space: 1.7; }
.sec-sp-p175 { --section-space: 1.75; }
.sec-sp-p180 { --section-space: 1.8; }
.sec-sp-p185 { --section-space: 1.85; }
.sec-sp-p190 { --section-space: 1.9; }
.sec-sp-p195 { --section-space: 1.95; }
.sec-sp-p200 { --section-space: 2; }

/* Image treatments, set by the published theme on <body>. Full bleed is
   the default and needs no class. */
.img-inset .hero-img { inset: 24px; width: calc(100% - 48px); height: calc(100% - 48px); border-radius: var(--radius-card); }
.img-duotone .hero-img { filter: grayscale(1) contrast(1.1); mix-blend-mode: luminosity; opacity: .55; }

/* The display face, set by the published theme on <body>. Archivo is
   the default and needs no class. Anton has one weight, so the 900s
   above simply stop mattering under it; it runs tall and narrow, so it
   gets a breath of tracking and the brand books' uppercase. Display
   type only — body text, buttons and navigation keep the body face. */
.font-anton .hero-heading, .font-anton .band-heading, .font-anton .stat-value,
.font-anton .cta-heading, .font-anton .fee-heading, .font-anton .partner-name {
  font-family: 'Anton', var(--font);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .5px;
}
