/* ==========================================================================
   Dial and Hands
   Palette note: every colour pair below is checked against WCAG AA. The old
   site's green (#66aa33 on white) was 2.85:1 and its hover state 1.88:1, which
   meant pointing at a link made it harder to read. The green now darkens in
   light mode and lightens in dark mode, so hover always gains contrast.
   ========================================================================== */

/* Jost, self-hosted. One variable file per style covers every weight on the
   site (400 body, 500 for the few places that want it), so adding a weight is a
   CSS change rather than another download.

   font-display: swap matches what the Google Fonts URL used to request. With
   the file coming from this origin and preloaded it is rarely visible, but the
   fallback stack in --font is metrically close enough that a swap is not a jolt. */
@font-face {
	font-family: "Jost";
	src: url("/fonts/jost-var.woff2") format("woff2");
	font-weight: 100 900;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "Jost";
	src: url("/fonts/jost-italic-var.woff2") format("woff2");
	font-weight: 100 900;
	font-style: italic;
	font-display: swap;
}

:root {
	color-scheme: light dark;

	/* type */
	--font: "Jost", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
	/* 68ch of Jost is ~800px, which inside the 72rem container gives the same
	   left margin as the first version of this page — the one whose left edge was
	   right, and whose right edge was not. Centring fixes the right. */
	--measure: 68ch;

	/* fluid scale, 1.25 ratio at the small end opening to 1.33 at the large */
	--step--1: clamp(0.83rem, 0.80rem + 0.15vw, 0.92rem);
	--step-0:  clamp(1.05rem, 1.00rem + 0.22vw, 1.18rem);
	--step-1:  clamp(1.30rem, 1.20rem + 0.45vw, 1.56rem);
	--step-2:  clamp(1.60rem, 1.42rem + 0.85vw, 2.07rem);
	--step-3:  clamp(1.95rem, 1.65rem + 1.45vw, 2.76rem);
	--step-4:  clamp(2.35rem, 1.88rem + 2.30vw, 3.67rem);

	/* rhythm

	   One curve, and every vertical space on the page is a fixed multiple of it.

	   This used to be two curves pulling against each other. --gap runs 1rem to
	   2rem: it doubles across the viewport range. Paragraph spacing was set in
	   em, so it tracked --step-0, which runs 1.05rem to 1.18rem — twelve per
	   cent. The space around a figure therefore grew about eight times faster
	   than the space between two paragraphs, and no two elements held the same
	   proportion at any two window widths. That is what reads as spacing that
	   will not sit still: not any single value being wrong, but the ratios
	   sliding continuously as you resize.

	   --space is now the only vertical unit. The multiples below are constants,
	   so every vertical proportion holds at 390px and at 1600px. --gap survives
	   for horizontal gaps — masthead, grid column gaps, gutters — where it is
	   not in competition with anything. Nothing vertical should use it. */
	--space:     clamp(1rem, 0.86rem + 0.7vw, 1.5rem);
	--space-2xs: calc(var(--space) * 0.25);
	--space-xs:  calc(var(--space) * 0.5);
	--space-s:   calc(var(--space) * 0.75);
	--space-m:   var(--space);
	--space-l:   calc(var(--space) * 1.5);
	--space-xl:  calc(var(--space) * 2);
	--space-2xl: calc(var(--space) * 3);

	/* The gap between two paragraphs — the unit the rest of the page is read
	   against. Any flow container can override it locally. */
	--flow-gap:  var(--space-m);

	--gap: clamp(1rem, 0.7rem + 1.5vw, 2rem);
	--gutter: clamp(1.25rem, 5vw, 6rem);

	/* Narrow enough that a comfortable measure nearly fills it. That is what
	   keeps the text margins equal AND lines the text column up with the
	   masthead — the old site had both, and left-aligning a narrow measure
	   inside a wide container gave up the first to get the second. */
	--wrap-max: 72rem;

	/* Palette.

	   Every token is defined once, as light-dark(light, dark), so the two modes
	   cannot drift apart — which they already did once here, leaving the banner
	   at 2.28:1 in dark mode because only one of two duplicated blocks got
	   updated. light-dark() keys off `color-scheme`, declared at the top of
	   :root, so the manual override below sets color-scheme rather than
	   redeclaring colors.

	   The green is a desaturated yellow-green — midcentury machine green rather
	   than the mint it started as. Hue sits around 105, saturation in the 30-45%
	   range in both modes.

	   Contrast ratios in comments are against --bg in the relevant mode. The old
	   site's green was 2.85:1 on white with a 1.88:1 hover state; the green now
	   darkens in light mode and lightens in dark, so hover always gains
	   contrast. */

	--bg:          light-dark(#faf9f8, #1a1918);
	--bg-sunk:     light-dark(#f2efec, #232120);
	--text:        light-dark(#333333, #e8e6e3);   /* 12.4 / 14.1 */
	--text-muted:  light-dark(#6b6b6b, #a09a93);   /*  5.1 /  6.3 */
	--link:        light-dark(#3f7d28, #94b782);   /*  4.8 /  7.8 */
	--link-hover:  light-dark(#2f5e1d, #aecc9e);   /*  7.3 / 10.0 */
	--rule:        light-dark(rgb(0 0 0 / 0.12), rgb(255 255 255 / 0.14));
	--edge:        light-dark(rgb(0 0 0 / 0.08), rgb(255 255 255 / 0.10));

	--footer-bg:   light-dark(#1f1d1b, #131211);
	--footer-text: light-dark(#cfcac4, #b3ada6);
	--footer-link: light-dark(#b0cc9e, #aecc9e);

	/* The banner is the one place green is a field rather than an accent, so it
	   needs its own pair: white reads on the deep green but only hits 2.3:1 on
	   the dark-mode mint, so the text flips to near-black there. */
	--banner-bg:   light-dark(#3f7d28, #94b782);
	--banner-text: light-dark(#ffffff, #1a1918);   /*  5.0 /  7.8 */
}

/* An explicit choice wins over the system preference in both directions. There
   is no toggle in the UI yet; this is the hook for one. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

/* ----------------------------------------------------------------- base -- */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
	margin: 0;
	background: var(--bg);
	color: var(--text);
	font-family: var(--font);
	font-size: var(--step-0);
	font-weight: 400;
	line-height: 1.5;
	font-synthesis-weight: none;
	-webkit-font-smoothing: antialiased;
	display: flex;
	flex-direction: column;
	min-height: 100vh;
	min-height: 100svh;
}

img, video { max-width: 100%; height: auto; display: block; }

a { color: var(--link); text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
a:hover, a:focus-visible { color: var(--link-hover); }

:focus-visible { outline: 2px solid var(--link); outline-offset: 3px; border-radius: 2px; }

/* Headings carry no top border. That single rule in the old theme is most of
   what made the site feel ruled-off; the hierarchy comes from size and space. */
h1, h2, h3, h4, h5, h6 {
	font-weight: 400;
	line-height: 1.15;
	letter-spacing: -0.015em;
	text-wrap: balance;
}

h1, h2 { margin: 0.1em 0; }

h3, h4, h5, h6 { margin: 0.6em 0 0.4em; }

h1 { font-size: var(--step-4); }
h2 { font-size: var(--step-2); }
h3 { font-size: var(--step-1); }
h4 { font-size: var(--step-0); }

/* No text-wrap: pretty here. It rebalances a paragraph to avoid a short last
   line, which on a short paragraph pulls every line in — one paragraph was
   leaving 107px unused while its neighbours left 1px, and the ragged edge read
   as a stray margin. Consistent line length matters more here than orphans. */
p { margin: 0; }

ul { margin: 0; }

/* markdown-it-anchor wraps each heading in a link so it can be deep-linked.
   That is useful, but it should not look like body copy link — headings behave
   like the site title: inherit the colour, no underline, green on hover. */
.prose :is(h1, h2, h3, h4, h5, h6) a {
	color: inherit;
	text-decoration: none;
}

.prose :is(h1, h2, h3, h4, h5, h6) a:hover { color: var(--link); }

hr {
	border: 0;
	border-top: 1px solid var(--rule);
	margin: var(--space-2xl) 0;
}

/* --------------------------------------------------------------- layout -- */

.wrap {
	width: 100%;
	max-width: var(--wrap-max);
	margin-inline: auto;
	padding-inline: var(--gutter);
}

/* The measure is the page. Titles, rules, captions and body copy all sit in one
   centred column, and only photos, embeds and the multi-column blocks are
   allowed out of it — along with the masthead and footer, which live outside
   .prose entirely. Everything collapses to device width on a phone, where the
   distinction stops mattering. */
/* One template at every width, and deliberately no media query. The centre
   track is min(--measure, 100%), so on a phone it is simply 100% and the two
   side tracks collapse to zero of their own accord — a .breakout at 390px ends
   up exactly as wide as body copy, which is what you want there anyway.

   Building it only above a breakpoint, as this did until recently, meant the
   named lines did not exist below it. A grid-column that names a line the grid
   does not have is not ignored: the grid invents an implicit track for it. That
   is what turned every project timeline into three overlapping columns on a
   phone — one .update::before asking for `text` at a width where `text` was not
   a line. Named lines have to exist wherever anything names them.

   A note for the next person who sees this grid overflow horizontally: the
   track sizing is not the problem. Bare text nodes that are direct children of
   a grid become anonymous grid items, and migrated Markdown produced some by
   putting a paragraph and an image shortcode on the same line. Every shortcode
   needs blank lines around it. */
.prose {
	display: grid;
	grid-template-columns:
		[full-start] minmax(0, 1fr)
		[text-start] min(var(--measure), 100%) [text-end]
		minmax(0, 1fr) [full-end];
}

.prose > * { grid-column: text; }

.prose > .breakout,
.prose > .cols,
.prose > .cta,
.prose > .photorow,
.prose > figure { grid-column: full; }

/* Opt-in for a page whose body is all full-width blocks — Services is two
   columns and a CTA with no running prose at all, so a title held to the
   measure was the only thing out of line. The title follows the widest thing on
   its own page. */
.prose--wide > h1 { grid-column: full; }

/* A float cannot escape a grid item, so the portrait stopped wrapping the
   moment .prose became a grid. Keeping the photo and the paragraphs it belongs
   with inside one grid item restores ordinary float behaviour. */
.prose > .lead { grid-column: text; }

.prose > figure { margin: var(--space-l) 0; }

/* A portrait photo at full container width is overwhelming, so height is
   capped and the aspect ratio is left alone — which means a tall photo ends up
   narrower than the prose. That is intended. An earlier version put a min-width
   floor here to stop that, which forced the width without adjusting the height
   and stretched every portrait sideways. */
.prose > figure img {
	max-width: 100%;
	max-height: 80vh;
	width: auto;
	height: auto;
	margin-inline: auto;
}

/* Two or three photos that belong together, side by side — a before and an
   after, two halves of one bench. Stacked below the breakpoint, where half a
   device width is not a photograph. `--photos` is written by the photorow
   shortcode, so the count lives with the content rather than in a modifier
   class here.

   The figures inside give up their own vertical margin to the row; the row
   carries the same gap a lone figure would, so the rhythm of the page does not
   change depending on how many photos are in a line. Their images are not
   height-capped the way a full-width figure is — at half the measure a tall
   photo is already the right size, and capping it would shrink one of a pair
   and break the alignment the row exists to create. */
.photorow {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--space-l) var(--gap);
	margin: var(--space-l) 0;
}

@media (min-width: 48rem) {
	.photorow { grid-template-columns: repeat(var(--photos, 2), minmax(0, 1fr)); }
}

.photorow > figure { margin: 0; }

/* ------------------------------------------------------------------ nav -- */

.masthead {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	justify-content: space-between;
	gap: 0.5rem var(--gap);
	padding-block: clamp(1.5rem, 4vw, 2.75rem) clamp(1.75rem, 4vw, 3rem);
}

.masthead__title {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0 0.45em;
	margin: 0;
	font-weight: 400;
	line-height: 1.15;
}

.masthead__name {
	font-size: var(--step-2);
	letter-spacing: -0.025em;
	text-decoration: none;
	color: var(--text);
}

.masthead__name:hover { color: var(--link); }

/* The business name carries no personal recognition on its own, so the byline
   is doing real work — it is the only place "Luke McKenzie" appears above the
   fold. Sized with the nav so it reads as a subordinate line, not a subtitle. */
.masthead__byline {
	font-size: var(--step-0);
	color: var(--text-muted);
	white-space: nowrap;
}

.masthead__byline a { color: var(--text-muted); text-decoration: none; }
.masthead__byline a:hover { color: var(--link); }

.nav {
	display: flex;
	flex-wrap: wrap;
	gap: clamp(0.9rem, 2.5vw, 1.75rem);
	font-size: var(--step-0);
}

.nav a { color: var(--text-muted); text-decoration: none; }
.nav a:hover { color: var(--link); }
.nav a[aria-current="page"] { color: var(--text); text-decoration: underline; text-underline-offset: 0.3em; }

/* --------------------------------------------------------------- banner -- */

.banner {
	background: var(--banner-bg);
	color: var(--banner-text);
	font-size: var(--step--1);
	line-height: 1.45;
}

/* Hidden unless the page was reached from theclockspot.com. The root class is
   set by the inline script in <head>, before first paint. */
.banner { display: none; }
:root.from-tcs .banner { display: block; }

.banner__inner {
	display: flex;
	align-items: baseline;
	gap: var(--gap);
	padding-block: 0.7rem;
}

.banner p { margin: 0; }
.banner a { color: currentcolor; text-underline-offset: 0.2em; }

.banner__close {
	margin-inline-start: auto;
	flex: none;
	background: none;
	border: 0;
	color: inherit;
	font: inherit;
	line-height: 1;
	padding: 0.35rem 0.5rem;
	cursor: pointer;
	opacity: 0.8;
}

.banner__close:hover { opacity: 1; }

/* The global focus ring is --link, and in light mode --link and --banner-bg are
   the same green: focusing anything in here drew a 1:1 outline, which is to say
   none at all. Inside the banner the ring follows the banner's own text color,
   which is contrast-checked against the banner background by definition. */
.banner :focus-visible { outline-color: currentcolor; }

/* ---------------------------------------------------------------- tiles -- */

.tiles {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	gap: var(--space-xl) var(--gap);
	grid-template-columns: repeat(auto-fill, minmax(min(17rem, 100%), 1fr));
}

.tile { margin: 0; }
.tile > a { text-decoration: none; color: inherit; display: block; }

/* No shadow, no border — a hairline drawn just inside the frame, so a
   blown-out white photo still separates from the page and the grid never
   shifts.

   This has to be `outline` with a negative offset, not `box-shadow: inset`.
   An inset shadow paints *behind* the content of a replaced element, so on an
   <img> it is completely invisible; the earlier version drew nothing at all.
   Outline paints on top, and follows border-radius, so it works on the round
   portrait too. */
.photo {
	width: 100%;
	background: var(--bg-sunk);
	outline: 1px solid var(--edge);
	outline-offset: -1px;
}

.tile__photo { aspect-ratio: 4 / 3; object-fit: cover; }

.tile h2 {
	font-size: var(--step-1);
	margin: 0.75rem 0 0.2rem;
}

.tile > a:hover h2 { color: var(--link); }

.tile p { margin: 0.25rem 0 0; color: var(--text); }

.tile__meta {
	font-size: var(--step--1);
	color: var(--text-muted);
	margin-top: 0.5rem;
}

/* Recency ordering only reads as intentional if the date is visible, so every
   tile states when it last moved and what changed. */
.tile__latest { color: var(--text-muted); }
.tile > a:hover .tile__latest { color: var(--link); }

/* Status was a chip reading ACTIVE or COMPLETE. Dropped: any project here can
   pick up a new update at any time, so the binary was misleading. Whether
   something is in progress is said in the project's own description instead. */

/* --------------------------------------------------------------- filter -- */

/* The filter row sat directly under the nav at a similar size and weight, so it
   read as a second navigation bar. Two changes fix that: a label saying what it
   is, and dropping the outlines so the chips are quiet text until hovered. */
.filterbar {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.35rem 0.9rem;
	margin: 0 0 var(--space-xl);
}

/* No opacity here. --text-muted is 5.07:1 on --bg, which passes AA; knocking it
   back to 0.75 blended it to #8f8f8e and 3.08:1, which does not — and this is
   the smallest text on the page. The uppercase, letterspaced treatment is
   already doing the work of making it read as a label rather than a chip. */
.filterbar__label {
	font-size: var(--step--1);
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--text-muted);
}

.filter {
	display: flex;
	flex-wrap: wrap;
	gap: 0.3rem 0.4rem;
	padding: 0;
	margin: 0;
	list-style: none;
	font-size: var(--step--1);
}

.filter a {
	display: inline-block;
	padding: 0.2rem 0.6rem;
	border: 1px solid transparent;
	border-radius: 100px;
	text-decoration: none;
	color: var(--text-muted);
}

.filter a:hover {
	color: var(--link);
	border-color: var(--rule);
}

.filter a[aria-current="page"] {
	color: var(--text);
	border-color: var(--rule);
	background: var(--bg-sunk);
}

/* -------------------------------------------------------------- project -- */

.project__lede {
	font-size: var(--step-1);
	line-height: 1.4;
	color: var(--text);
	margin-bottom: 1em;
}

.project__meta {
	font-size: var(--step--1);
	color: var(--text-muted);
	margin-bottom: var(--space-l);
}

.project__meta a { color: var(--text-muted); text-decoration-color: var(--rule); }
.project__meta a:hover { color: var(--link); text-decoration-color: currentcolor; }

/* The safety notice on a project page. Deliberately quiet: a red-and-yellow
   warning box would fight the photographs for attention on every electronics
   page, and a warning that is visually shouted on nine pages stops being read on
   any of them. The label and the small muted text are enough to be found by
   anyone looking, and the link carries the weight. Separation is space rather
   than rules — the page already uses a rule to mean "a new update starts here",
   and bracketing this in two more made it read as a section of the timeline. */
.safety {
	margin-block: var(--space-xl) 0;
	font-size: var(--step--1);
	color: var(--text-muted);
}

.safety__heading {
	font-size: var(--step--1);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--text-muted);
	margin: 0 0 var(--space-2xs);
}

/* --link is 4.78:1 on --bg, which passes for body copy, but this text is the
   smallest on the page and sits in a block already set in muted grey. The hover
   green is 7.29:1 and reads as the one actionable thing in the notice. */
.safety a { color: var(--link-hover); }

/* The jump list. Updates are sections of this page, not separate pages, so
   this is in-page navigation rather than a table of contents to elsewhere. */
.jumplist {
	margin: var(--space-xl) 0 0;
}

.jumplist h2 {
	font-size: var(--step--1);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--text-muted);
	margin-bottom: var(--space-s);
}

.jumplist ol { list-style: none; margin: 0; padding: 0; }
.jumplist li { margin-bottom: 0.45em; }
.jumplist a { text-decoration: none; }
.jumplist a:hover { text-decoration: underline; }
.jumplist time { color: var(--text-muted); font-size: var(--step--1); margin-inline-end: 0.6em; }

.update {
	margin-top: var(--space-xl);
	scroll-margin-top: 2rem;
}

.update::before {
	content: "";
	display: block;
	grid-column: text;
	border-top: 1px solid var(--rule);
	margin-bottom: var(--space-l);
}

.update__date {
	display: block;
	font-size: var(--step--1);
	color: var(--text-muted);
	margin-bottom: 0.4em;
}

.update h2 { font-size: var(--step-2); }

/* Deep-linking an update should make it obvious which one you landed on. */
.update:target .update__date { color: var(--link); }

.backtotop {
	margin: var(--space-xl) 0 0;
	padding-top: var(--space-l);
	text-align: center;
	font-size: var(--step--1);
}

.backtotop a { color: var(--text-muted); text-decoration-color: var(--rule); }
.backtotop a:hover { color: var(--link); text-decoration-color: currentcolor; }

figcaption {
	font-size: var(--step--1);
	color: var(--text-muted);
	margin-top: 0.6em;
	max-width: var(--measure);
	margin-inline: auto;
}

/* ------------------------------------------------------------- notfound -- */

/* The clock is the one The Clock Spot used on its own 404 page, hands at 4:04.
   Centred rather than held to the measure: there is no reading to do here, only
   a wrong turn to recover from. */
.notfound {
	text-align: center;
	padding-block: clamp(2rem, 8vw, 5rem);
}

.notfound__clock {
	width: 180px;
	max-width: 50vw;
	height: auto;
	margin: 0 auto var(--space-l);
}

.notfound h1 { font-size: var(--step-2); }

.notfound__lead {
	max-width: 36ch;
	margin-inline: auto;
	font-size: var(--step-1);
	line-height: 1.4;
}

/* A flex row so the links wrap. Separating them with &nbsp;&middot;&nbsp;
   instead glues all three into one unbreakable run, which measures fine at
   360px but has nowhere to go if the text is zoomed. The separators are flex
   items of their own, hidden from assistive tech so they are not read aloud. */
.notfound__links {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 0.3em 0.7em;
	color: var(--text-muted);
}

/* --------------------------------------------------------------- footer -- */

.site-footer {
	margin-top: auto;
	background: var(--footer-bg);
	color: var(--footer-text);
	font-size: var(--step--1);
	padding-block: 2rem;
}

.site-footer a { color: var(--footer-link); }

.site-footer__inner {
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem var(--gap);
	justify-content: space-between;
}

.site-footer p { margin: 0; }

/* ------------------------------------------------------------ utilities -- */

/* A flow container spaces its children with row-gap rather than letting each
   child carry a margin, so the rhythm belongs to the container and does not
   depend on which two elements happen to be adjacent, or on whether their
   margins collapse — which they do in a block box and do not in a grid one.

   Two things follow from that, and both of them bit this stylesheet:

   1. It has to be a grid at every width. .prose only builds its named columns
      above the breakpoint, but paragraphs need separating on a phone too. The
      single-column template sits in :where() so it carries no specificity and
      .prose's own columns win wherever both apply.

   2. A gap only reaches direct children. Any wrapper holding running copy has
      to be a flow container in its own right — that is what broke About and
      Services, whose copy lives inside divs, sections and a <details>. */
.flow { display: grid; row-gap: var(--flow-gap); }
:where(.flow) { grid-template-columns: minmax(0, 1fr); }

/* Off-screen until focused, then pinned to the top-left over everything. It
   deliberately does not use .visually-hidden: that clips to a 1px box, and a
   link the browser has scrolled to but cannot paint is worse than no link. */
.skiplink {
	position: absolute;
	left: var(--space-xs);
	top: -100%;
	z-index: 10;
	padding: 0.6rem 1rem;
	background: var(--bg);
	color: var(--link-hover);
	border: 1px solid var(--rule);
	border-radius: 3px;
	font-size: var(--step--1);
}

.skiplink:focus { top: var(--space-xs); }

/* The date a legal page last changed. Stated in the page source rather than
   taken from build.year: it has to mean "when this text changed", not "when the
   site was last rebuilt". */
.page-date {
	font-size: var(--step--1);
	color: var(--text-muted);
	margin-bottom: var(--space-l);
}

.visually-hidden {
	position: absolute;
	width: 1px; height: 1px;
	padding: 0; margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

main { padding-bottom: clamp(2.4rem, 6.4vw, 4.8rem); }

@media (prefers-reduced-motion: reduce) {
	*, *::before, *::after {
		animation-duration: 0.01ms !important;
		transition-duration: 0.01ms !important;
		scroll-behavior: auto !important;
	}
}

/* ------------------------------------------------------------- services -- */

.cols {
	display: grid;
	gap: var(--space-xl) calc(var(--gap) * 2);
	grid-template-columns: 1fr;
	margin-block: var(--space-xl);
}

@media (min-width: 48rem) {
	.cols { grid-template-columns: 1fr 1fr; }
}

/* Not a flow container, though it looks like the obvious place for one. A
   browser with ::details-content wraps everything after the summary in a box
   of its own, so a grid here has two items — the summary and that box — and
   the gap never reaches the paragraphs. Browsers without it would space them
   fine, which is worse: it would work on the machine you tested on. The copy
   carries its own .flow wrapper in services.md instead. */
.col > details { border-top: 1px solid var(--rule); padding-top: 0.75rem; }

.col summary {
	cursor: pointer;
	list-style: none;
	display: block;
}

.col summary::-webkit-details-marker { display: none; }

.col summary h2 { margin: 0 0 0.25em; }
.col summary:hover h2 { color: var(--link); }

/* The one-line pitch lives in the summary, so a collapsed section still says
   what it is — the disclosure hides the detail, never the offer. */
.col__pitch {
	margin: 1em 0;
	font-size: var(--step-1);
	line-height: 1.4;
	/* font-style: italic;
	color: var(--text-muted); */
	/* margin: 0; */
}

.col summary::after {
	content: "Read more";
	display: inline-block;
	margin-top: 0.6em;
	font-size: var(--step--1);
	color: var(--link);
}

.col details[open] summary::after { content: "Close"; }

/* Above the breakpoint both columns are always open and the affordance is
   meaningless, so it goes away. site.js manages the open attribute to match. */
@media (min-width: 48rem) {
	.col > details { border-top: 0; padding-top: 0; }
	.col summary { cursor: default; }
	.col summary::after { display: none; }
}

.cta {
	/* margin-block: calc(var(--gap) * 2); */
	margin: 0;
	padding: var(--space-l) var(--gap);
	background: var(--bg-sunk);
	border-radius: 3px;
	text-align: center;
}

.cta p { margin: 0; font-size: var(--step-1); line-height: 1.4; }

/* --link is 4.78:1 on --bg but only 4.39:1 on the slightly darker --bg-sunk,
   which misses AA for text this size. The hover green clears it at 6.7:1, so
   the panel starts from there. That leaves the global a:hover with nothing to
   change, so the hover state moves to the underline instead — this is the one
   call to action on the site and it should still answer a pointer. */
.cta a { color: var(--link-hover); }
.cta a:hover, .cta a:focus-visible { text-decoration-thickness: 2px; }

/* ------------------------------------------------------------- portrait -- */

.lead::after {
	content: "";
	display: block;
	clear: both;
}

/* The one container that cannot be a flow container. The portrait floats, and
   a float cannot escape a grid item, so this one keeps margin-based spacing. */
.lead > * + * { margin-top: var(--flow-gap); }


.portrait {
	float: right;
	width: min(13rem, 38%);
	margin: 0.3rem 0 var(--space-m) var(--gap);
	shape-outside: circle();
}

.portrait__img {
	width: 100%;
	aspect-ratio: 1;
	object-fit: cover;
	border-radius: 50%;
}

/* Floating a round photo next to a single short paragraph looks broken on a
   phone, so it centres above the text instead. */
@media (max-width: 40rem) {
	.portrait {
		float: none;
		width: min(11rem, 55%);
		margin: 0 auto var(--space-m);
	}
}

/* The portrait is the first thing in .lead and sits beside the prose, so the
   prose starts level with it rather than a paragraph-gap below. */
.lead > .portrait + p { margin-top: 0; }
