/*
 * Gadgetholic block styles — see DESIGN.md.
 *
 * Achromatic by rule. Six neutrals on a warm bone canvas, weight 400 everywhere, 5px radius
 * everywhere, and one warm brown shadow that appears beneath imagery and nowhere else. Hierarchy
 * comes from size, colour and space, because nothing else is available to it.
 *
 * The rule that costs something: an affiliate CTA is a ghost button, not a coloured one. Colour is
 * the cheapest way to raise a click rate and it is not available here. That is a deliberate trade
 * against the identity, and it is measurable — CTA treatment is an experiment factor in
 * PINTEREST_PIPELINE section 6, so a coloured CTA can arrive as a variant with a result behind it
 * rather than as a redesign that quietly abandons the system.
 *
 * No web fonts and no scripts beyond the click beacon: the reader is on a phone, on mobile data,
 * one tap away from leaving. The palette costs nothing and the type costs nothing, which is why
 * this design fits inside a budget that fails the build on any external subresource.
 */

:root {
  --gh-radius: 5px;
  /* Warm brown, under 15% — it reads as warmth rather than as colour. Imagery only. */
  --gh-shadow: rgba(90, 50, 40, 0.15) 0 5px 15px 0, rgba(80, 40, 30, 0.1) 0 4px 8px 0;
  --gh-section-gap: 122px;
  --gh-element-gap: 20px;
  --gh-tracking-caption: -0.038em;
}

/* --- masthead ------------------------------------------------------------------------------- */
/*
 * Featherlight: no fill, no border, no shadow. It sits on the canvas rather than on top of it.
 * A rule under the header would be the first thing the eye lands on, and the first thing should
 * be the page.
 */
.gh-masthead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--gh-element-gap);
}
.gh-masthead .wp-block-site-title {
  margin: 0;
}
/*
 * The wordmark lockup. The mark is decorative and the text carries the link — two adjacent links
 * to the same place would be read out twice by a screen reader for no gain.
 *
 * The mark is drawn in `currentColor`, which resolves to Ink. Its source file is a warm olive
 * (#4a4636); DESIGN.md permits no chromatic colour except the shadow, and a full-opacity mark is
 * not that exception. Restoring the original is one declaration — see the note below.
 */
.gh-brand {
  gap: 10px;
  align-items: center;
}
.gh-brand__mark {
  width: 2.1rem;
  height: 2.1rem;
  display: block;
  color: var(--wp--preset--color--ink);
  flex: none;
}
/* To use the brand's own olive instead of Ink, this is the whole change:
   .gh-brand__mark { color: #4a4636; } */
.gh-masthead .wp-block-site-title a {
  color: var(--wp--preset--color--ink);
  text-decoration: none;
  letter-spacing: -0.02em;
}
/*
 * The wordmark is set larger than the reference's featherlight bar, at the owner's direction. The
 * restraint the reference is really about lives in the nav and the labels, which stay at 13px in
 * Ash — so the masthead now has a hierarchy of its own instead of one uniform whisper.
 */
.gh-masthead .wp-block-site-title {
  font-size: var(--wp--preset--font-size--subheading);
  line-height: 1.2;
}
/*
 * The one weight above 400 on the site, at the owner's direction. DESIGN.md says hierarchy comes
 * from size and colour rather than weight, and it still does everywhere else — this is the wordmark,
 * which is a mark before it is text.
 */
.gh-masthead .wp-block-site-title a {
  font-weight: 600;
}
/* The tagline is a whisper, not a subtitle. It sits in Ash with the other metadata. */
.gh-masthead__tag {
  color: var(--wp--preset--color--ash);
  margin: 0;
  flex: 1 1 100%;
  order: 3;
}
.gh-nav {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--gh-element-gap);
}
.gh-nav a {
  color: var(--wp--preset--color--ash);
  text-decoration: none;
}
.gh-nav a:hover,
.gh-nav a:focus-visible {
  color: var(--wp--preset--color--ink);
}

/*
 * The masthead on the front page, where the dark band now runs up behind it.
 *
 * Selected with `:has(.gh-hero)` rather than a body class, because the hero IS the condition — the
 * header inverts exactly where there is a dark band under it, and no PHP has to agree with the CSS
 * about which page that is.
 *
 * `position: relative` is load-bearing: the band is a pseudo-element of the hero, which comes later
 * in the DOM, so without a stacking context of its own the masthead would be painted underneath the
 * wood.
 */
body:has(.gh-hero) .gh-masthead {
  position: relative;
  z-index: 1;
}
body:has(.gh-hero) .gh-brand__mark,
body:has(.gh-hero) .gh-masthead .wp-block-site-title a {
  color: var(--wp--preset--color--canvas);
}
body:has(.gh-hero) .gh-nav a {
  /* Ash on walnut reads at 6.7:1, but it belongs to the bone page. On the band the whole masthead
     is one material, so the nav is the canvas colour held back rather than a different grey. */
  color: rgba(244, 243, 241, 0.72);
}
body:has(.gh-hero) .gh-nav a:hover,
body:has(.gh-hero) .gh-nav a:focus-visible {
  color: var(--wp--preset--color--canvas);
}

/* --- editorial section label ------------------------------------------------------------------ */
/*
 * The one place the serif appears in a label role, at 13px with tightened tracking. An
 * anti-type-scale choice: a heading that reads as a caption. Never set larger — above 13px it
 * stops being a signature and becomes an ordinary serif heading.
 */
.gh-label {
  font-family: var(--wp--preset--font-family--serif);
  font-size: var(--wp--preset--font-size--caption);
  line-height: 1.5;
  letter-spacing: var(--gh-tracking-caption);
  color: var(--wp--preset--color--ash);
  margin: 0;
}

/* --- breadcrumbs ---------------------------------------------------------------------------- */
/*
 * The 13px serif label, reused. A breadcrumb is metadata, and metadata on this site is Ash at
 * caption size — giving it its own treatment would make navigation the loudest thing above a title
 * that is supposed to be the first thing read.
 */
.gh-crumbs {
  display: flex;
  flex-wrap: wrap;
  gap: 0 8px;
  margin-bottom: var(--gh-element-gap);
}
.gh-crumbs a {
  color: var(--wp--preset--color--ash);
  text-decoration: none;
}
.gh-crumbs a:hover,
.gh-crumbs a:focus-visible {
  color: var(--wp--preset--color--ink);
  text-decoration: underline;
}
.gh-crumbs__sep {
  color: var(--wp--preset--color--mist);
}

/* --- article -------------------------------------------------------------------------------- */
.gh-article {
  padding-top: var(--wp--preset--spacing--80);
  padding-bottom: var(--gh-section-gap);
}
/*
 * The measure is set once, as theme.json `contentSize`, so every block shares it and the left edge
 * stays straight. Setting a narrower max-width per element instead would fight WordPress's
 * constrained layout, which centres children with `margin: auto !important` — different widths
 * would then centre to different left edges and the column would look broken.
 *
 * Body copy is serif and larger than the reference's 15px. DESIGN.md records why: fifteen hundred
 * words of small system sans is not a pleasant read on a phone, and the article is the product.
 */
.gh-article .wp-block-post-content {
  font-family: var(--wp--preset--font-family--serif);
  font-size: 1.0625rem;
  line-height: 1.7;
  color: var(--wp--preset--color--graphite);
}
.gh-article .wp-block-post-content h2,
.gh-article .wp-block-post-content h3 {
  font-family: var(--wp--preset--font-family--system);
}
/*
 * VERTICAL RHYTHM — the thing that separates a typeset article from a wall of grey.
 *
 * A heading belongs to what FOLLOWS it, so the space above has to be much larger than the space
 * below. theme.json already asked for 65px above an h2 and it never arrived: WordPress emits its
 * element styles as `:root :where(h2)`, which the flow-layout rule beats, so every heading sat on
 * the paragraph above it with the same 20px block gap as everything else. The reader could not find
 * a section boundary because there was not one to find. Third time this specificity fight has cost
 * something here, so the selector is written to win outright.
 */
.gh-article .wp-block-post-content > :is(h2, h3) {
  margin-block-start: var(--wp--preset--spacing--65);
  margin-block-end: 0.45rem;
  text-wrap: balance;
}
.gh-article .wp-block-post-content > h3 {
  margin-block-start: var(--wp--preset--spacing--40);
}
/* Nothing should push the first heading away from the lede it follows. */
.gh-article .wp-block-post-content > :is(h2, h3):first-child {
  margin-block-start: 0;
}
/* A heading directly after an image already has the figure's margin above it. */
.gh-article .wp-block-post-content > .wp-block-image + :is(h2, h3) {
  margin-block-start: var(--wp--preset--spacing--40);
}
/*
 * Lists keep the article's left edge.
 *
 * The default indent pushed every list 32px right of the column, so the page had two left margins
 * and the eye caught the ragged one. The marker hangs into the margin instead and the TEXT stays on
 * the same line as every paragraph — which is what makes a long article look set rather than typed.
 */
.gh-article .wp-block-post-content ul,
.gh-article .wp-block-post-content ol {
  padding-left: 0;
  margin-block: 0.75rem 0;
  list-style-position: outside;
}
.gh-article .wp-block-post-content li {
  padding-left: 1.35em;
  margin-block-start: 0.6rem;
  list-style: none;
  position: relative;
}
.gh-article .wp-block-post-content ul > li::before {
  content: '';
  position: absolute;
  left: 0.25em;
  /* Sits on the first line's centre rather than its top, at any line height. */
  top: calc(0.85em - 2px);
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--wp--preset--color--ash);
}
.gh-article .wp-block-post-content ol {
  counter-reset: gh-item;
}
.gh-article .wp-block-post-content ol > li {
  counter-increment: gh-item;
}
.gh-article .wp-block-post-content ol > li::before {
  content: counter(gh-item) '.';
  position: absolute;
  left: 0;
  color: var(--wp--preset--color--ash);
  font-family: var(--wp--preset--font-family--system);
  font-size: var(--wp--preset--font-size--caption);
}
/*
 * `pretty` over `balance` for running text: it prevents a one-word last line without re-wrapping the
 * whole paragraph, which is the right trade when there are hundreds of them.
 */
.gh-article .wp-block-post-content p {
  text-wrap: pretty;
}

.gh-article > .wp-block-post-title {
  margin-bottom: 0;
  /* A two-line headline that leaves one word on the second line reads as an accident. */
  text-wrap: balance;
}
.gh-lede {
  font-size: var(--wp--preset--font-size--subheading);
  line-height: 1.4;
  color: var(--wp--preset--color--slate);
  margin-top: var(--gh-element-gap);
}
.gh-article blockquote {
  border-left: 1px solid var(--wp--preset--color--ash);
  padding-left: var(--gh-element-gap);
  margin-left: 0;
  font-style: normal;
}
.gh-article blockquote cite {
  display: block;
  margin-top: 0.5rem;
  font-family: var(--wp--preset--font-family--system);
  font-size: var(--wp--preset--font-size--caption);
  font-style: normal;
  color: var(--wp--preset--color--ash);
}

/* --- article meta and what comes after it -------------------------------------------------- */
/*
 * Section, date, reading time. Ash at caption size, like every other piece of metadata on the site,
 * so it sits under the title without competing with the lede that follows.
 */
.gh-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 8px;
  margin: 10px 0 0;
  font-family: var(--wp--preset--font-family--system);
  font-size: var(--wp--preset--font-size--caption);
  color: var(--wp--preset--color--ash);
}
.gh-meta a {
  color: var(--wp--preset--color--slate);
  text-decoration: none;
}
.gh-meta a:hover,
.gh-meta a:focus-visible {
  color: var(--wp--preset--color--ink);
  text-decoration: underline;
}
.gh-meta__sep {
  color: var(--wp--preset--color--mist);
}

/*
 * Read next.
 *
 * The article used to end and stop. This is the only part of the page whose job is the reader who
 * finished — the most valuable one the site has — so it is given real space above it rather than
 * being tucked against the last line.
 */
.gh-next {
  margin-top: var(--gh-section-gap);
  padding-top: var(--wp--preset--spacing--40);
  border-top: 1px solid var(--wp--preset--color--mist);
}
.gh-next__list {
  list-style: none;
  padding: 0;
  margin: var(--wp--preset--spacing--40) 0 0;
  display: grid;
  gap: var(--wp--preset--spacing--40);
}
.gh-next__link {
  display: block;
  text-decoration: none;
  color: inherit;
}
.gh-next__title {
  display: block;
  font-family: var(--wp--preset--font-family--system);
  font-size: var(--wp--preset--font-size--subheading);
  line-height: 1.3;
  color: var(--wp--preset--color--ink);
  text-wrap: balance;
}
.gh-next__link:hover .gh-next__title,
.gh-next__link:focus-visible .gh-next__title {
  text-decoration: underline;
}
.gh-next__excerpt {
  display: block;
  margin-top: 8px;
  font-family: var(--wp--preset--font-family--system);
  font-size: var(--wp--preset--font-size--caption);
  line-height: 1.5;
  color: var(--wp--preset--color--ash);
}

/* --- imagery -------------------------------------------------------------------------------- */
/*
 * The only place the shadow is permitted. Image fills its container edge to edge with no padding —
 * a framed image reads as a slide; an unframed one reads as the thing itself.
 */
.gh-article img,
.wp-block-image img,
.wp-block-post-featured-image img {
  border-radius: var(--gh-radius);
  box-shadow: var(--gh-shadow);
  display: block;
  width: 100%;
  height: auto;
}
/*
 * Article images breathe; grid tiles do not. These margins were global once, which put 40px above
 * every thumbnail in the index and left a tile with no featured image sitting 40px higher than its
 * neighbours — the whole row read as broken for a reason that had nothing to do with the row.
 */
.gh-article .wp-block-image,
.gh-article .wp-block-post-featured-image {
  margin-top: var(--wp--preset--spacing--40);
  margin-bottom: var(--wp--preset--spacing--40);
}
.wp-block-image figcaption,
.gh-article figcaption {
  font-family: var(--wp--preset--font-family--system);
  font-size: var(--wp--preset--font-size--caption);
  line-height: 1.5;
  color: var(--wp--preset--color--ash);
  margin-top: var(--gh-element-gap);
  text-align: left;
}

/* --- index grid ----------------------------------------------------------------------------- */
/* --- front page: a lead story, then a list ---------------------------------------------------- */
/*
 * WHY THIS IS NOT A THREE-UP GRID ANY MORE
 *
 * Three columns and four articles leaves the fourth alone on a second row with two empty cells. That
 * is not a spacing problem to tune — every fixed column count has an article count that breaks it,
 * and the site is going to keep publishing. A lead story followed by a list has no such count: one
 * article looks right, four look right, twenty look right.
 *
 * It is also how a section front actually works on the publications this site is competing with. The
 * newest piece gets the picture and the size; everything else gets a line and a thumbnail, which is
 * enough to choose from.
 *
 * All of it hangs off `:first-child`, so the core Query Loop stays untouched and no PHP has to know
 * which article is the lead.
 */
.gh-grid .wp-block-post-template {
  display: block;
  list-style: none;
  padding: 0;
  margin: 0;
}
.gh-grid .wp-block-post-template > li {
  margin: 0;
}

/* The lead. */
.gh-grid .wp-block-post-template > li:first-child {
  padding-bottom: var(--wp--preset--spacing--65);
  border-bottom: 1px solid var(--wp--preset--color--mist);
}
.gh-grid .wp-block-post-template > li:first-child .wp-block-post-featured-image img {
  aspect-ratio: 16 / 9;
  object-fit: cover;
}
.gh-grid .wp-block-post-template > li:first-child .wp-block-post-title {
  font-size: var(--wp--preset--font-size--large);
  line-height: 1.2;
  letter-spacing: -0.015em;
  max-width: 34rem;
  margin: var(--wp--preset--spacing--40) 0 0;
  text-wrap: balance;
}
.gh-grid .wp-block-post-template > li:first-child .wp-block-post-excerpt {
  max-width: 34rem;
  margin: var(--gh-element-gap) 0 0;
}

/*
 * Everything after it: a row, thumbnail first. `grid-template-columns` with a fixed first track
 * rather than a percentage, so the thumbnail is the same size whatever the column is doing.
 */
.gh-grid .wp-block-post-template > li:not(:first-child) {
  display: grid;
  grid-template-columns: 13rem 1fr;
  gap: 0 var(--wp--preset--spacing--40);
  align-items: start;
  padding-block: var(--wp--preset--spacing--40);
  border-bottom: 1px solid var(--wp--preset--color--mist);
}
.gh-grid .wp-block-post-template > li:not(:first-child) .wp-block-post-featured-image {
  grid-row: span 2;
  margin: 0;
}
.gh-grid .wp-block-post-template > li:not(:first-child) .wp-block-post-featured-image img {
  aspect-ratio: 3 / 2;
  object-fit: cover;
}
.gh-grid .wp-block-post-template > li:not(:first-child) .wp-block-post-title {
  margin: 0;
  align-self: center;
}
.gh-grid .wp-block-post-template > li:not(:first-child) .wp-block-post-excerpt {
  margin: 8px 0 0;
  align-self: start;
}
/* Shared: titles are Ink and unadorned, excerpts are Ash at caption size. */
.gh-grid .wp-block-post-title {
  font-family: var(--wp--preset--font-family--system);
  font-weight: 400;
}
.gh-grid .wp-block-post-title a {
  color: var(--wp--preset--color--ink);
  text-decoration: none;
}
.gh-grid .wp-block-post-title a:hover,
.gh-grid .wp-block-post-title a:focus-visible {
  text-decoration: underline;
}
.gh-grid .wp-block-post-template > li:not(:first-child) .wp-block-post-title {
  font-size: var(--wp--preset--font-size--medium);
  line-height: 1.3;
  text-wrap: balance;
}
.gh-grid .wp-block-post-excerpt {
  font-size: var(--wp--preset--font-size--caption);
  line-height: 1.5;
  color: var(--wp--preset--color--ash);
}
.gh-grid .wp-block-post-excerpt__excerpt {
  font-size: inherit;
}
.gh-grid .wp-block-post-excerpt__more-text {
  display: none;
}
/* The lead's own excerpt is the one line that has to sell the click, so it is not a caption. */
.gh-grid .wp-block-post-template > li:first-child .wp-block-post-excerpt {
  font-size: var(--wp--preset--font-size--medium);
  line-height: 1.5;
  color: var(--wp--preset--color--slate);
}

/* An article with no featured image must not leave a hole where the thumbnail would be. */
.gh-grid .wp-block-post-template > li:not(:first-child):not(:has(.wp-block-post-featured-image)) {
  grid-template-columns: 1fr;
}

/* On a phone the thumbnail column would leave the title three words wide. */
@media (max-width: 40rem) {
  .gh-grid .wp-block-post-template > li:not(:first-child) {
    grid-template-columns: 1fr;
    gap: var(--gh-element-gap);
  }
  .gh-grid .wp-block-post-template > li:not(:first-child) .wp-block-post-featured-image {
    grid-row: auto;
  }
}

/* --- article index (the /setups/ and /guides/ hubs) ------------------------------------------ */
/*
 * The same three-up rhythm as the front page, on plain markup rather than on a core Query Loop.
 * The plugin block states its own query, so the rules cannot be shared by selector — they are
 * shared by shape instead, which is why the column and row measurements are repeated here rather
 * than aliased. Two identical numbers in two places is the honest cost of not making one block
 * pretend to be another.
 */
/*
 * The hub is a narrow introduction over a wide grid.
 *
 * It renders inside the article template, whose measure is set for 1,500 words of prose — right for
 * the paragraph at the top and far too narrow for a three-up grid, which came out as a single
 * column of very large tiles. The grid breaks back out to the same 64rem the front page uses, so
 * the two indexes are the same object at the same width.
 *
 * `.entry-content > .gh-index` rather than `.gh-index`, because WordPress caps every child of a
 * constrained layout at the content size from a rule of equal weight. Same lesson as the hero
 * scene: winning that requires one more class, not a stronger opinion.
 */
.entry-content > .gh-index {
  /*
   * Two attempts are worth recording, because the first two both looked right in the stylesheet.
   *
   * `max-width: 64rem` did nothing: the cap applied and the element still measured 576px, because
   * its PARENT is the constrained content column and a child cannot exceed the box it sits in.
   *
   * A negative `margin-inline` did nothing either, and this one was silent — the computed value came
   * back as `0px`. WordPress sets `margin-left: auto !important` on every child of a constrained
   * layout, and an auto margin cannot go negative. Measured in the browser; from the stylesheet it
   * simply looked like the rule was being ignored.
   *
   * So the offset comes from `left` and `transform`, which that rule does not touch. The width is
   * bounded by the viewport so the grid can never push a horizontal scrollbar onto the page.
   */
  width: min(64rem, calc(100vw - 4rem));
  max-width: none;
  position: relative;
  left: 50%;
  transform: translateX(-50%);
}
.gh-index {
  margin-top: var(--wp--preset--spacing--122);
}
/*
 * The grid is as wide as it has items for.
 *
 * The front page always shows a full row, so equal fractions across three tracks are right there.
 * A hub does not: Guides currently holds one article, and in a full-width grid that single tile sat
 * hard against the left edge under a centred paragraph — a layout that looks like it failed rather
 * than a section with one item in it.
 *
 * `auto-fit` with a capped track was the obvious repair and it was wrong: the track count is worked
 * out from the maximum, so three tiles that would have fitted at 298px came out as two rows.
 * Measured, not assumed. Counting the items with `:has()` says the thing directly instead — one
 * item is one tile wide, two are two, three or more take the full measure and match the front page.
 */
.gh-index__list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
  column-gap: 65px;
  row-gap: var(--wp--preset--spacing--65);
  list-style: none;
  padding: 0;
  margin-inline: auto;
  margin-block: 0;
  max-width: 64rem;
}
.gh-index__list:not(:has(> :nth-child(2))) {
  max-width: 20.5rem;
}
.gh-index__list:has(> :nth-child(2)):not(:has(> :nth-child(3))) {
  max-width: 45rem;
}
.gh-index__list > li {
  margin: 0;
}
@supports (grid-template-rows: subgrid) {
  .gh-index__list {
    grid-auto-rows: auto;
  }
  .gh-index__list > li {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 2;
    row-gap: 0;
  }
}
/*
 * The image and the title are one link. Two adjacent links to the same article would be announced
 * twice by a screen reader and give a keyboard user two stops for one destination.
 */
.gh-index__link {
  display: block;
  text-decoration: none;
  color: inherit;
}
.gh-index__link img {
  aspect-ratio: 3 / 2;
  object-fit: cover;
  margin: 0;
}
.gh-index__title {
  font-size: var(--wp--preset--font-size--small);
  font-weight: 400;
  line-height: 1.35;
  color: var(--wp--preset--color--ink);
  margin: var(--gh-element-gap) 0 0;
}
.gh-index__link:hover .gh-index__title,
.gh-index__link:focus-visible .gh-index__title {
  text-decoration: underline;
}
.gh-index__excerpt {
  font-size: var(--wp--preset--font-size--caption);
  line-height: 1.5;
  color: var(--wp--preset--color--ash);
  margin: 10px 0 0;
}

/* --- hero ----------------------------------------------------------------------------------- */
/*
 * No headline above, no button below. The paragraph is the hero.
 *
 * Scoped to direct children on purpose. The first version styled every `p` inside `.gh-hero`, and
 * the front-page template then wrapped a whole article — every paragraph, list and heading in it
 * came out centred. A centring rule with a loose selector is one of the few CSS mistakes that
 * looks like a layout bug rather than a style bug, so it stays narrow.
 *
 * The hero is a full-bleed dark band at the owner's direction (2026-08-18). See DESIGN.md
 * departure 4: this is the one place on the site that is not bone, and the type in it is the one
 * place that is not Graphite.
 */
main > .gh-hero:first-child {
  /*
   * A small gap, not the 122px section gap. The band is the loudest thing on the page and it wants
   * to sit close under the masthead; 122px of bone above it would read as a mistake rather than as
   * rhythm. The 122px gap returns underneath, where the page goes quiet again.
   */
  margin-block-start: clamp(24px, 3.5vw, 48px);
}
.gh-hero {
  position: relative;
  margin-bottom: var(--gh-section-gap);
  padding: clamp(96px, 12vw, 168px) clamp(20px, 5vw, 56px);
  min-height: clamp(420px, 46vw, 620px);
  display: grid;
  align-content: center;
  /* Keeps the scene's stacking context to itself, so nothing here can slide over the masthead. */
  isolation: isolate;
}
.gh-hero > .wp-block-post-content,
.gh-hero > p {
  position: relative;
  z-index: 1;
}
.gh-hero > .wp-block-post-content > p,
.gh-hero > p {
  font-size: var(--wp--preset--font-size--subheading);
  line-height: 1.35;
  /*
   * Canvas, not Graphite. The band is the inverse of the page: the same two tones, swapped. The
   * darkest region under the paragraph measures about 8:1 against this, and the single brightest
   * pixel anywhere in the band still clears 4.9:1.
   */
  color: var(--wp--preset--color--canvas);
  text-align: center;
  max-width: 34rem;
  margin-left: auto;
  margin-right: auto;
}
.gh-hero a {
  color: var(--wp--preset--color--canvas);
  text-decoration-color: rgba(244, 243, 241, 0.55);
}

/* --- hero scene ----------------------------------------------------------------------------- */
/*
 * A dark cafe table seen from above, full width, with four objects set down on it.
 *
 * WHAT CHANGED WHEN THE TABLE WENT DARK
 *
 * The pale version needed its light falloff flattened, because the darkest patch under the
 * paragraph fell to 2.9:1 against Graphite. Flattening a dark image does the opposite: normalising
 * it to near-white and scaling back down amplifies the noise and threw the walnut olive. So this
 * one is the photograph, resized and brought down 14%, and nothing else. White on it has contrast
 * to spare, which is the whole reason the type could invert.
 *
 * The headphones were re-shot in a pale colourway for the same reason in the other direction:
 * charcoal on dark walnut is two dark things touching, and the object that carries "this site is
 * about gadgets" cannot be the one that disappears.
 */
.gh-hero > .gh-scene {
  max-width: none;
}
/*
 * The table, painted from a seamless 640px grain tile over a flat walnut colour.
 *
 * It was one 1600px photograph stretched across the band, which meant the grain got softer the wider
 * the window — the opposite of what a table should do. A tile repeats at its own resolution, so the
 * detail is pixel-exact at 1440 and at 2560 alike, and it costs the same 56 KB either way.
 *
 * The grain ships GRAYSCALE and the colour lives here. Encoding the walnut into the file made every
 * tonal decision a round trip through ImageMagick, and the first attempts came out olive, then
 * mahogany. As a blend the colour is one hex value that can be judged on the page.
 *
 * Making it seamless: roll by half so the two original edges meet as a cross, paint over that cross
 * with texture cloned from a quarter-tile away, then divide out the low frequencies with a blur that
 * WRAPS (`-virtual-pixel tile`), so the correction is itself seamless. Verified by tiling 2x2 and
 * comparing the pixel-to-pixel delta across the join against the delta everywhere else: 0.88x, i.e.
 * the join varies slightly LESS than the texture does, which is why no line is visible.
 */
.gh-hero::before {
  content: '';
  position: absolute;
  /*
   * Up behind the masthead. 320px is comfortably more than the header is ever tall, including when
   * the nav wraps, and anything above the document top simply is not painted. A tile costs nothing
   * to extend, which is the second reason the band is no longer a photograph.
   */
  inset: -320px 0 0;
  z-index: -1;
  /*
   * Two layers: a soft light, then the grain over the walnut.
   *
   * A tile is evenly lit by construction — that is what makes it tile — and the photograph it
   * replaced got some of its atmosphere from light falling across the table. The gradient puts that
   * back, in CSS, for no bytes and with none of the tiling problems a baked-in gradient causes. It
   * is very slight on purpose: enough that the surface reads as lit, not enough to become a vignette.
   */
  background-image:
    radial-gradient(130% 100% at 66% 4%, rgba(255, 233, 205, 0.22), rgba(255, 233, 205, 0) 58%),
    radial-gradient(90% 80% at 8% 100%, rgba(20, 10, 4, 0.3), rgba(20, 10, 4, 0) 70%),
    url('hero/wood-grain.webp');
  background-color: #4a3524;
  background-repeat: no-repeat, no-repeat, repeat;
  background-blend-mode: screen, multiply, overlay;
  animation: gh-scene-ground 1s ease both;
}
.gh-scene {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
/*
 * The shadow is `drop-shadow`, not `box-shadow`, so it follows the object's silhouette rather than
 * its bounding box — a rectangular shadow under a round saucer is the detail that would give the
 * whole scene away as pasted-on.
 *
 * It is darker and tighter than the DESIGN.md stack because the surface is dark: the warm brown at
 * 15% that reads as a shadow on bone is invisible on walnut. Same idea, same warmth, more of it.
 *
 * `rotate` and `translate` are set as their own properties, not inside `transform`: the entrance
 * animates `translate`, the parallax writes `transform`, and the tilt has to survive both.
 */
.gh-scene__item {
  position: absolute;
  /*
   * Each object is anchored to the edge it is NEAR — left or right, top or bottom — and never to the
   * one it is far from. A percentage offset from the far edge does not know how big the object is,
   * so it drifts off the table as the band resizes: `top: 78%` put the notebook 9px past the bottom
   * at one width and 44px past it at another, and a left-anchored cup ran 32px off the right at
   * 1100. `overflow: hidden` then sliced them, which looks like a rendering fault rather than a
   * layout one.
   */
  left: var(--gh-x, auto);
  right: var(--gh-r, auto);
  top: var(--gh-y, auto);
  bottom: var(--gh-b, auto);
  width: var(--gh-w);
  height: auto;
  rotate: var(--gh-rot);
  /*
   * Two shadows, thrown DOWN AND LEFT, scaled by how tall the thing is.
   *
   * The scene had two light sources disagreeing with each other: the gradient lights the table from
   * the top right, and every object threw its shadow straight down. That is not a subtle error — it
   * is most of why the objects read as pasted on rather than set down. The offsets now point away
   * from the light.
   *
   * A contact shadow, tight and dark, is what makes something touch a surface; an ambient one, wide
   * and soft, is what gives it height. Only having the soft one is why everything floated.
   *
   * `--gh-lift` is how far off the table the object sits — a kettle stands, a notebook lies flat —
   * so a single set of numbers gives each object a shadow of the right size instead of all four
   * sharing one.
   */
  filter:
    drop-shadow(
      calc(-2px * var(--gh-lift, 1)) calc(5px * var(--gh-lift, 1)) calc(6px * var(--gh-lift, 1))
      rgba(10, 5, 1, 0.62)
    )
    drop-shadow(
      calc(-9px * var(--gh-lift, 1)) calc(19px * var(--gh-lift, 1)) calc(28px * var(--gh-lift, 1))
      rgba(10, 5, 1, 0.5)
    );
  transform: translate3d(
    calc(var(--gh-px, 0) * var(--gh-depth) * 1px),
    calc((var(--gh-py, 0) + var(--gh-sy, 0)) * var(--gh-depth) * 1px),
    0
  );
  animation: gh-scene-rise 0.9s cubic-bezier(0.2, 0.7, 0.2, 1) var(--gh-in) both;
}
/*
 * Where each object sits, how far it drifts, and how far it stands off the table.
 *
 * The rule the old arrangement broke: four objects of nearly the same size, one per quadrant, each
 * with the same margin. Every piece carried identical weight and none of them touched anything, so
 * the eye read a layout rather than a table. Three things changed.
 *
 * SCALE. The desk cluster is the anchor at 300px and the headphones are 165px — nearly a factor of
 * two — so there is something to look at first.
 *
 * CROPPING. Each object is offset from the CENTRE so the group stays with the words at any width,
 * but clamped with `max()` to a small NEGATIVE inset. Below about 1250px the outer two run off the
 * edge of the band instead of shrinking away from the text. A thing cut by the frame reads as
 * photographed; a thing with air on all four sides reads as placed.
 *
 * PAIRING. Two on the left and two on the right, but at different heights and overlapping in x, so
 * each side is a pair rather than a corner. The ring around the paragraph is gone.
 *
 * `--gh-depth` is the parallax multiplier and `--gh-lift` the shadow scale. They are not the same
 * number: the desk cluster lies flat on the table (small shadow) but is the furthest thing from the
 * viewer (small parallax), while the cup stands up and is nearest.
 */
.gh-scene__item.is-desk {
  --gh-x: max(-90px, calc(50% - 700px)); --gh-b: 4%;
  --gh-w: 300px; --gh-rot: -4deg; --gh-depth: 0.55; --gh-lift: 0.62; --gh-in: 0.34s;
}
.gh-scene__item.is-kettle {
  --gh-x: max(2%, calc(50% - 560px)); --gh-y: 13%;
  --gh-w: 190px; --gh-rot: -8deg; --gh-depth: 1; --gh-lift: 1.35; --gh-in: 0.05s;
}
.gh-scene__item.is-coffee {
  --gh-r: max(-30px, calc(50% - 620px)); --gh-y: 10%;
  --gh-w: 250px; --gh-rot: 5deg; --gh-depth: 1.5; --gh-lift: 1.1; --gh-in: 0.18s;
}
.gh-scene__item.is-headphones {
  --gh-r: max(-38px, calc(50% - 560px)); --gh-y: 54%;
  --gh-w: 165px; --gh-rot: -10deg; --gh-depth: 1.25; --gh-lift: 0.95; --gh-in: 0.46s;
}
@keyframes gh-scene-ground {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes gh-scene-rise {
  from { opacity: 0; translate: 0 26px; }
  to { opacity: 1; translate: 0 0; }
}

/*
 * WHICH OBJECTS SURVIVE, AND WHY IT IS NOT ABOUT SIZE
 *
 * The first version dropped the gadget first and kept the kettle and the cup, which meant a phone —
 * where the whole Pinterest audience arrives — saw a hero made entirely of coffee gear, under a
 * masthead that says Gadgetholic. The order protects the mix rather than the count: whatever else
 * goes, the coffee cluster and the headphones stay, because that pair is the shortest way to say
 * what the site is. The desk cluster goes first because it is the largest, then the kettle.
 */
@media (max-width: 66rem) {
  .gh-hero { padding-block: clamp(150px, 20vw, 178px) clamp(150px, 22vw, 168px); }
  .gh-scene__item { width: calc(var(--gh-w) * 0.66); }
  .gh-scene__item.is-desk { display: none; }
  .gh-scene__item.is-kettle { --gh-x: 2%; --gh-r: auto; --gh-y: 4%; }
  .gh-scene__item.is-coffee { --gh-x: auto; --gh-r: -22px; --gh-y: 2%; width: calc(var(--gh-w) * 0.56); }
  .gh-scene__item.is-headphones { --gh-x: 4%; --gh-r: auto; --gh-y: auto; --gh-b: 5%; width: calc(var(--gh-w) * 0.62); }
}
@media (max-width: 40rem) {
  /* Room under the paragraph for the one object that stays down there. */
  .gh-hero { padding-block: clamp(158px, 40vw, 180px) clamp(150px, 38vw, 168px); }
  .gh-scene__item { width: calc(var(--gh-w) * 0.54); }
  .gh-scene__item.is-kettle { display: none; }
  .gh-scene__item.is-coffee { --gh-x: 2%; --gh-r: auto; --gh-y: 3%; --gh-b: auto; width: calc(var(--gh-w) * 0.46); }
  .gh-scene__item.is-headphones { --gh-x: 62%; --gh-r: auto; --gh-y: auto; --gh-b: 4%; width: calc(var(--gh-w) * 0.56); }
}

/*
 * Someone who has asked for less motion gets the composition and none of the movement. The parallax
 * script checks the same query and never attaches, so the custom properties stay at 0 too.
 */
@media (prefers-reduced-motion: reduce) {
  .gh-scene__ground,
  .gh-scene__item {
    animation: none;
  }
}

/* --- placement ------------------------------------------------------------------------------ */
/*
 * A product card. No border separating it from the canvas — the Mist surface and the spacing do
 * that, per DESIGN.md. The merchant name is in the CTA before anyone clicks
 * (AFFILIATE_SYSTEM section 11), which is a promise the About page makes in words and this keeps
 * in markup.
 */
.gh-placement {
  background: var(--wp--preset--color--canvas);
  border-radius: var(--gh-radius);
  padding: var(--wp--preset--spacing--40) 0;
  margin: var(--wp--preset--spacing--65) 0;
  border-top: 1px solid var(--wp--preset--color--mist);
  border-bottom: 1px solid var(--wp--preset--color--mist);
  font-family: var(--wp--preset--font-family--system);
}
.gh-placement__role {
  font-size: var(--wp--preset--font-size--caption);
  letter-spacing: var(--gh-tracking-caption);
  font-family: var(--wp--preset--font-family--serif);
  color: var(--wp--preset--color--ash);
  margin: 0;
}
.gh-placement__name {
  font-size: var(--wp--preset--font-size--subheading);
  line-height: 1.2;
  color: var(--wp--preset--color--ink);
  margin: var(--gh-element-gap) 0 0;
}
.gh-placement__reason {
  font-size: var(--wp--preset--font-size--small);
  line-height: 1.5;
  color: var(--wp--preset--color--graphite);
  margin: 0.5rem 0 0;
}
/* Ghost button. No fill, no shadow — the outline is the affordance. */
.gh-placement__cta {
  display: inline-block;
  margin-top: var(--gh-element-gap);
  padding: 10px 15px;
  border: 1px solid var(--wp--preset--color--ink);
  border-radius: var(--gh-radius);
  background: transparent;
  color: var(--wp--preset--color--ink);
  font-size: var(--wp--preset--font-size--small);
  line-height: 1.2;
  text-decoration: none;
}
.gh-placement__cta:hover,
.gh-placement__cta:focus-visible {
  background: var(--wp--preset--color--mist);
  color: var(--wp--preset--color--ink);
}
.gh-placement__note {
  font-size: var(--wp--preset--font-size--caption);
  color: var(--wp--preset--color--ash);
  margin: 0.5rem 0 0;
}
/*
 * A withdrawn offer. No red, no warning colour — the system has none, and a state that shouts is
 * a state a reader distrusts. It reads as absence: the CTA is gone and a sentence says why.
 */
.gh-placement--unavailable,
.gh-placement--stale,
.gh-placement--no_offer {
  opacity: 0.75;
}
.gh-placement__unavailable {
  font-size: var(--wp--preset--font-size--caption);
  color: var(--wp--preset--color--slate);
  margin: var(--gh-element-gap) 0 0;
}
.gh-placement--error {
  display: none;
}

/* --- disclosure ----------------------------------------------------------------------------- */
.gh-disclosure {
  font-family: var(--wp--preset--font-family--system);
  font-size: var(--wp--preset--font-size--caption);
  line-height: 1.5;
  color: var(--wp--preset--color--slate);
  border-top: 1px solid var(--wp--preset--color--mist);
  border-bottom: 1px solid var(--wp--preset--color--mist);
  padding: var(--gh-element-gap) 0;
  margin: var(--wp--preset--spacing--40) 0;
}
.gh-disclosure p {
  margin: 0;
}
.gh-disclosure__required {
  margin-top: 0.5rem !important;
  color: var(--wp--preset--color--ash);
}

/* --- footer --------------------------------------------------------------------------------- */
/*
 * The footer keeps the page's own measure.
 *
 * It is a 64rem group, which lines up with the grid on the front page and sat 100px left of the text
 * on an article — one page with two left edges and nothing else on the narrower one. Whether a
 * footer should span wider than the article is a real choice; here it read as a mistake, because no
 * other element shared that edge. One page, one left edge.
 */
body:has(.gh-article) .gh-footer > * {
  max-width: 36rem;
}
.gh-footer {
  border-top: 1px solid var(--wp--preset--color--mist);
  color: var(--wp--preset--color--ash);
}
.gh-footer p {
  font-size: var(--wp--preset--font-size--caption);
  line-height: 1.5;
}
.gh-footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--gh-element-gap);
}
.gh-footer__links a {
  color: var(--wp--preset--color--ash);
  text-decoration: none;
}
.gh-footer__links a:hover,
.gh-footer__links a:focus-visible {
  color: var(--wp--preset--color--ink);
}

/* --- reading progress ------------------------------------------------------------------------ */
/*
 * Two pixels of Ink, scaled from the left. `transform` rather than `width` so it never triggers
 * layout — the whole point of a bar that updates on every scroll frame.
 */
.gh-progress {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  height: 2px;
  z-index: 20;
  background: var(--wp--preset--color--ink);
  transform: scaleX(0);
  transform-origin: 0 50%;
  will-change: transform;
  pointer-events: none;
}

/* --- motion ---------------------------------------------------------------------------------- */
/*
 * Reveals, and the reason they are this restrained.
 *
 * A list of articles that fades up as it arrives is the one piece of motion an editorial index can
 * carry without becoming a slideshow. It is 12px and 400ms — enough to feel considered, not enough
 * to make anyone wait — and it is driven by scroll position rather than a timer, so nothing moves
 * that the reader has not arrived at.
 *
 * `animation-timeline: view()` does this with no JavaScript at all. Where it is unsupported the
 * items are simply there, which is the correct fallback: the content never depends on the effect.
 */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .gh-grid .wp-block-post-template > li,
    .gh-index__list > li,
    .gh-next__list > li {
      animation: gh-reveal linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 40%;
    }
  }
}
@keyframes gh-reveal {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .gh-progress {
    transition: none;
  }
}

/* --- accessibility floor -------------------------------------------------------------------- */
/*
 * An achromatic system removes colour as a focus signal, so the outline has to do all of it.
 * Ash on canvas is 2.3:1 — fine for a border, not a focus ring — so the ring is Ink.
 */
:where(a, button, input, textarea, select):focus-visible {
  outline: 2px solid var(--wp--preset--color--ink);
  outline-offset: 2px;
  border-radius: var(--gh-radius);
}
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
