/*
 * Modules: the self-contained blocks an entity page is assembled from.
 *
 * ⚠️ A MODULE MUST NOT CARE WHICH COLUMN IT IS IN.
 *
 * These get moved in the builder - one column, two columns, a sidebar, back to
 * full width - and every one of them has to survive that without an edit. So
 * nothing here is sized against the VIEWPORT: the breakpoints are container
 * queries, which ask how wide the module's own box is.
 *
 * A `@media (max-width: 600px)` rule would be a lie in a 300px sidebar on a
 * 1440px screen, and it would be the kind of lie that only shows up on the one
 * layout nobody tested.
 *
 * ⚠️ Layout lives here, never on the elements. A `_display` setting generates a
 * `#brxe-{id}

` rule, which is ID specificity and beats every class in this file.
 * The load more button lost its whole hover state to exactly that.
 */

.oview-module {
	/* The box the queries below measure. */
	container-type: inline-size;
	display: flex;
	flex-direction: column;
	min-width: 0;
	border: 1px solid var(--color-border);
	border-radius: var(--radius-m);
	background: var(--color-surface);
	/* Chips and bars run to the rounded edge, so they are clipped by it. */
	overflow: hidden;
}

/*
 * ⚠️ The header is `baseline`, not `center`. Its two halves are different sizes -
 * a 15px title against a 10px count - and centring them makes the small one
 * float, which reads as misalignment rather than as a deliberate pairing.
 */
.oview-module__head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--space-s);
	padding: var(--space-s) var(--space-m);
	border-bottom: 1px solid var(--color-border);
}

.oview-module__title {
	margin: 0;
	font-size: var(--text-xs);
	font-weight: 700;
	line-height: 1.2;
	color: var(--color-text);
}

/*
 * The small count beside a module title. Tabular figures so a number that
 * changes on click does not shift the text beside it.
 */
.oview-module__meta {
	font-size: var(--text-xs);
	font-variant-numeric: tabular-nums;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--color-text-muted);
	white-space: nowrap;
}

/* --- the notes pyramid --------------------------------------------------- */

.oview-pyramid__tier {
	padding: var(--space-s) var(--space-m);
}

.oview-pyramid__tier + .oview-pyramid__tier {
	border-top: 1px solid var(--color-border);
}

.oview-pyramid__label {
	display: flex;
	align-items: center;
	gap: var(--space-2xs);
	margin-bottom: var(--space-2xs);
	font-size: var(--text-xs);
	font-weight: 600;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--color-text-muted);
}

/*
 * ⚠️ A square with a radius, not a circle. Three of these sit in a vertical
 * line down the card and perfect circles read as radio buttons - something to
 * choose between rather than a key to what is below them.
 */
.oview-pyramid__dot {
	flex: none;
	width: 10px;
	height: 10px;
	border-radius: var(--radius-xs);
	background: var(--color-border-strong);
}

.oview-pyramid--top .oview-pyramid__dot {
	background: var(--color-tier-top);
}

.oview-pyramid--mid .oview-pyramid__dot {
	background: var(--color-tier-mid);
}

.oview-pyramid--base .oview-pyramid__dot {
	background: var(--color-tier-base);
}

.oview-pyramid__notes {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-2xs);
}

/*
 * ⚠️ The chip is a LINK to the note's own page, so it keeps a link's target
 * size. 44px would make three tiers of chips taller than the description beside
 * them, so the floor here is the 24x24 of WCAG 2.5.8 Minimum rather than the
 * 44x44 of Enhanced - which is the same call the facet chips already make.
 */
.oview-pyramid__note {
	display: inline-flex;
	align-items: center;
	min-height: 24px;
	padding: var(--space-2xs) var(--space-xs);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-full);
	background: var(--color-surface-sunk);
	font-size: var(--text-xs);
	font-weight: 500;
	line-height: 1.2;
	color: var(--color-text);
	text-decoration: none;
	transition: border-color 150ms ease;
}

.oview-pyramid--top .oview-pyramid__note {
	background: var(--color-tier-top-tint);
}

.oview-pyramid--mid .oview-pyramid__note {
	background: var(--color-tier-mid-tint);
}

.oview-pyramid--base .oview-pyramid__note {
	background: var(--color-tier-base-tint);
}

/* The same border-only hover the cards and the load more button use. */
.oview-pyramid__note:hover,
.oview-pyramid__note:focus-visible {
	border-color: var(--color-border-strong);
}

/* --- accord bars --------------------------------------------------------- */

.oview-accords {
	display: flex;
	flex-direction: column;
	gap: var(--space-2xs);
	padding: var(--space-s) var(--space-m);
}

/*
 * ⚠️ A FIXED FIRST COLUMN IS THE POINT. The bars only read as a comparison if
 * they start at the same x, so the name column is a fixed width and long names
 * wrap inside it rather than pushing the bars out of line.
 *
 * `minmax(0, ...)` on the bar, because a grid track's default `auto` minimum is
 * its content and a 100%-wide fill would refuse to shrink below it - the row
 * would overflow its card instead of the bar getting shorter.
 */
.oview-accords__row {
	display: grid;
	grid-template-columns: 7.5rem minmax(0, 1fr) 2.75rem;
	align-items: center;
	gap: var(--space-xs);
}

.oview-accords__name {
	font-size: var(--text-xs);
	font-weight: 600;
	line-height: 1.25;
	color: var(--color-text);
}

/* The track. The fill inside it carries the accord's own colour. */
.oview-accords__track {
	display: block;
	height: 10px;
	border-radius: var(--radius-full);
	background: var(--color-surface-sunk);
	overflow: hidden;
}

/*
 * ⚠️ THE COLOUR IS DATA, SO IT IS AN INLINE WIDTH AND AN INLINE BACKGROUND.
 *
 * All 92 accords carry their own hue in `ovf_accords.color` and their own
 * strength per fragrance, so neither can be a class. This is the one place a
 * style attribute is correct rather than lazy: the alternative is 92 generated
 * rules for a value that changes per row.
 *
 * It is also why the label sits OUTSIDE the bar. 64 of the 92 fail 4.5:1 as a
 * text colour and the pale ones - tuberose, coconut, white floral - come out at
 * 1.2:1. As a block of colour beside a label, every one of them reads.
 */
.oview-accords__fill {
	display: block;
	height: 100%;
	border-radius: var(--radius-full);
	background: var(--color-border-strong);
}

.oview-accords__pct {
	font-size: var(--text-xs);
	font-variant-numeric: tabular-nums;
	text-align: right;
	color: var(--color-text-muted);
}

/*
 * ⚠️ Narrow: the name takes the row and the bar sits under it. Three columns in
 * a 280px sidebar leaves the bar about 60px, which is too short to compare
 * anything - at which point the bar is decoration and the number is doing the
 * work.
 */
@container (max-width: 340px) {
	.oview-accords__row {
		grid-template-columns: minmax(0, 1fr) 2.75rem;
	}

	.oview-accords__track {
		grid-column: 1 / -1;
	}
}

/*
 * ⚠️ NARROW MEANS THE MODULE, NOT THE SCREEN. In a sidebar at 280px the chips
 * need to breathe less and the padding has to come in; on a phone at 375px the
 * same module is nearly full width and does not. A viewport media query cannot
 * tell those apart.
 */
@container (max-width: 320px) {
	.oview-module__head,
	.oview-pyramid__tier {
		padding-inline: var(--space-s);
	}

	.oview-pyramid__note {
		font-size: var(--text-xs);
	}
}

/*
 * No container support: fall back to the comfortable layout rather than the
 * cramped one. A module that is slightly roomy in a narrow column is a smaller
 * wrong than one that is cramped at full width.
 */
@supports not (container-type: inline-size) {
	.oview-module {
		container-type: normal;
	}
}

/* --- a row of modules ----------------------------------------------------- */

/*
 * ⚠️ `min(22rem, 100%)`, NOT `22rem`, AND THAT IS THE WHOLE POINT OF THE RULE.
 *
 * `repeat(auto-fit, minmax(22rem, 1fr))` reads as "as many 22rem columns as
 * fit", but when NONE fit it does not fall back to the container width - it
 * lays one 22rem track anyway and overflows. So the pattern breaks precisely
 * where a narrow container needs it most: a module row in a 300px sidebar
 * pushes 52px past its own edge, and the page grows a horizontal scrollbar
 * nobody can trace back to here.
 *
 * `min()` caps the track at the container, so the fallback is a full-width
 * column. `check-mobile.js` asserts no horizontal overflow, which catches it at
 * 375px - but not in a sidebar, because a sidebar is not a viewport.
 *
 * ⚠️ 360px is the minimum column, and it is in PIXELS because the root font
 * size here is 10px. Written as `22rem` it meant 220px, which is small enough
 * to put TWO modules side by side on a phone.
 */
.oview-modules {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
	gap: var(--space-m);
	align-items: start;
}

/* --- the vote cards: performance, when to wear ---------------------------- */

/*
 * ⚠️ NOTHING BELOW RENAMES A VOTE CLASS, and that constraint is the reason this
 * section is short.
 *
 * `votes.js` binds to `.oview-vote__row`, `__step`, `__fill` and the rest by
 * name, and `reference/components.md` writes that down as a contract. So the
 * platform decides what a meters card IS - three metrics, five steps, a track,
 * a value - and every rule here only says how wide it sits and where it breaks.
 * A card whose look needed new markup would be a card the platform got wrong.
 *
 * The two cards are deliberately the same shape as the accord chart above them:
 * a label, a bar, a number. Three modules that read as one family.
 */

.oview-module__body {
	padding: var(--space-s) var(--space-m) var(--space-m);
}

/*
 * ⚠️ `align-items: flex-start` on `.oview-vote` makes it shrink to its content,
 * which is right for a star row floating in a page and wrong for a card filling
 * a module. Widened here rather than there, because the platform's default is
 * the one that suits a card sitting on its own.
 */
.oview-module .oview-vote {
	inline-size: 100%;
	gap: var(--space-xs);
}

/*
 * ⚠️ MEASURED AGAINST THE MODULE, NOT THE SCREEN. Everything from here down is
 * a container query, so a card in a 320px sidebar on a desktop breaks exactly
 * the way it does on a phone. That is the whole reason these are modules.
 */

/* --- when to wear: label, count, bar -------------------------------------- */

.oview-module .oview-vote__row {
	gap: var(--space-xs);
	padding-inline: var(--space-2xs);
}

.oview-module .oview-vote__row-label {
	font-weight: 500;
}

/*
 * ⚠️ PIXELS, NOT REM, AND THAT IS NOT A STYLE PREFERENCE.
 *
 * THE ROOT FONT SIZE ON THIS SITE IS 10px. Every `rem` here is ten pixels, not
 * sixteen, so the first version of this query asked for 26rem meaning "416px"
 * and got 260px. The module is 343px on a phone, so the query simply never
 * matched and the rows never stacked - no error, no warning, and a layout that
 * looked deliberate.
 *
 * Every other container query in this file is already in px. Follow them.
 * `rem` is still right for SIZES, which should scale with the root; it is wrong
 * for a BREAKPOINT, which is a measurement of a box.
 */
@container (max-width: 320px) {
	/*
	 * The three columns become two rows: the label and its count on top, the
	 * bar full width beneath. The label column caps at 80px and the count at
	 * 40px, so the track still gets ~165px in a 343px module - which is why
	 * this threshold is 320px and not the wider one first guessed at. Measured,
	 * not assumed.
	 */
	.oview-module .oview-vote__row {
		grid-template-columns: 1fr auto;
		grid-template-areas: "label count" "track track";
		row-gap: var(--space-2xs);
		padding-block: var(--space-xs);
	}

	.oview-module .oview-vote__row-label { grid-area: label; }
	.oview-module .oview-vote__row-count { grid-area: count; }
	.oview-module .oview-vote__row .oview-vote__track { grid-area: track; }
}

/* --- performance: label, value, bar, steps -------------------------------- */

/*
 * ⚠️ The steps stay full width at every size and are never made smaller. They
 * are 24px on the nose because that is the WCAG 2.5.8 floor, and a container
 * query that shrank them to fit a sidebar would break the one rule in this file
 * that is not a matter of taste.
 */
.oview-module .oview-vote__meter + .oview-vote__meter {
	margin-block-start: var(--space-xs);
	padding-block-start: var(--space-xs);
	border-block-start: 1px solid var(--color-border);
}

.oview-module .oview-vote__meter-label {
	font-weight: 500;
}

.oview-module .oview-vote__meter-value {
	color: var(--color-text-muted);
}

@container (max-width: 200px) {
	/*
	 * Five 24px targets plus their gaps need about 140px, and the module's own
	 * padding takes ~32px. Under 200px they wrap rather than overflow, which
	 * keeps every one of them tappable - a step that has slid outside the card
	 * is a vote nobody can cast.
	 */
	.oview-module .oview-vote__steps {
		flex-wrap: wrap;
	}
}

/* --- the brand's mark, beside the fragrance's name ------------------------ */

/*
 * ⚠️ THE IDENTITY BLOCK AND THE MARK ARE ONE ROW, so the mark sits beside the
 * title rather than above or below it. `align-items: flex-start` and not
 * `center`: the title is three lines on a long name and one on a short one, and
 * centring makes the mark drift down the page as the name grows.
 */
.ov-identity-row {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: var(--space-l);
}

/*
 * ⚠️ IT MUST NOT SHRINK AND IT MUST NOT GROW. A brand mark is a fixed asset at
 * an unknown aspect ratio, so the frame is square and the image fits inside it
 * rather than filling it: a wordmark that is five times wider than it is tall
 * gets letterboxed instead of cropped, and a square logo is unchanged.
 */
.ov-brandmark {
	flex: 0 0 auto;
	inline-size: 8.4rem;
	block-size: 8.4rem;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--space-xs);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-m);
	background: var(--color-surface);
	box-sizing: border-box;
}

.ov-brandmark__img,
.ov-brandmark img {
	max-inline-size: 100%;
	max-block-size: 100%;
	inline-size: auto;
	block-size: auto;
	object-fit: contain;
}

/*
 * ⚠️ A CONTAINER QUERY, NOT A VIEWPORT ONE. This module has to survive being
 * moved into a narrower column, and at that width a mark beside the title
 * leaves the title nowhere to go.
 */
@container (max-width: 30rem) {
	.ov-identity-row {
		gap: var(--space-m);
	}

	.ov-brandmark {
		inline-size: 5.6rem;
		block-size: 5.6rem;
	}
}

/* --- the bottle shot, where the design drew a hatched box ------------------ */

/*
 * ⚠️ THE FRAME KEEPS ITS SIZE AND THE PICTURE FITS INSIDE IT.
 *
 * A source bottle shot is an unknown aspect ratio on a white background.
 * `contain` letterboxes a tall bottle rather than cropping its cap off, and the
 * frame stops the layout moving when one image is 375x500 and the next is not.
 */
.ov-shot__img,
.ov-shot__img img {
	inline-size: 100%;
	block-size: 100%;
	object-fit: contain;
}

.ov-similar__img,
.ov-similar__img img {
	inline-size: 100%;
	block-size: 100%;
	object-fit: contain;
	border-radius: inherit;
}

/*
 * ⚠️ THE "+N more" CHIP IS NOT A NOTE, so it does not wear a note's colour.
 *
 * A tier's chips are tinted by tier, and a chip that is a count rather than an
 * ingredient reading as one more note is the whole problem with it. Outlined and
 * uncoloured, it reads as the end of the list.
 */
/*
 * Three classes, because the promoted part tints every chip at two
 * (`.ov-pyramid .ov-note-chip`) and loads after this file. Matched at two this
 * loses on order and the count wears the tier's colour, which is the one thing
 * it must not do.
 */
.ov-chips .ov-note-chip.ov-note-chip--more {
	background: transparent;
	border-color: var(--color-border);
	color: var(--color-text-muted);
	font-variant-numeric: tabular-nums;
}

/*
 * ⚠️ A LOOP PUTS A WRAPPER ROUND EVERY ROW, AND THE WRAPPER IS A FLEX ITEM.
 *
 * `.ov-chips` is a wrapping flex row, so chips should flow four or five to a
 * line. What it actually contains is one Bricks `block` per note, each a
 * full-width flex item with one chip inside, so every note took a line of its
 * own and a base tier of five was five lines tall.
 *
 * The mockup did not show this because its chips were stamped out statically at
 * wiring time and sit directly in the container. The bug arrived with the loop.
 *
 * `display: contents` drops the wrapper out of layout and lets the chip itself
 * be the flex item. It is safe here precisely because the wrapper carries no
 * styling and no semantics: it exists only to be repeated.
 */
.ov-chips > .brxe-block {
	display: contents;
}

/* --- latest additions: its own module, on the site's scale ---------------- */

/*
 * ⚠️ A MODULE NEEDS A HOME, AND THIS ONE DID NOT HAVE ONE.
 *
 * "Latest perfumes added" was drawn for the homepage, so its rules live in that
 * mockup's stylesheet, which is enqueued on `mockup-` pages and nowhere else.
 * Moved into the shared sidebar it kept its markup and lost its styling, and two
 * attempts to patch that from outside made it worse: first a file of overrides
 * losing an argument with specificity, then renaming its classes into the module
 * beside it, which matched the sizes by deleting the design.
 *
 * It is a different module. It has a separator between rows, a timestamp at the
 * end of each one, and a smaller thumbnail than the card below it, and all three
 * are deliberate: this is a feed and that is a list of four.
 *
 * So it gets what any module gets: rules in a stylesheet the site ships, written
 * against declared tokens. Nothing here overrides anything.
 */
/*
 * ⚠️ THE TITLE OF A CARD IN A COLUMN, defined once, here.
 *
 * The drawings each had their own name for this and their own token with it:
 * `ov-side-head-title` at --text-s, `ov-side-title` at --text-m-2, and
 * `ov-pyramid__title` - the notes pyramid's class - borrowed onto a module with
 * nothing to do with notes. Four names, three sizes, one job, and every one of
 * them used a token so no gate saw it.
 *
 * `design/class-map.json` now puts THIS class on those blocks at staging time,
 * whatever the drawing called them, so the rule lives here rather than three
 * times in two vendors' stylesheets. A drawing's naming is input, not ours.
 */
/*
 * ⚠️ A KICKER ROW: a tag and its timestamp, above a title.
 *
 * Three names for this rendered identically - `ov-hero-flag`,
 * `ov-news-kicker-row` and `ov-news-item__head` - which is how
 * `check-class-drift.js` found them. Their only disagreement was the space
 * underneath, at three sizes nobody chose deliberately, so the row keeps none
 * of them: spacing below a header belongs to what follows it, not to the
 * header.
 */
.ov-kicker-row {
	margin-block-end: var(--space-xs);
}

/*
 * ⚠️ ONE ROW, FOR ANY LIST OF THINGS IN A COLUMN.
 *
 * A thumbnail, a name, the house under it. Two modules drew that same row and
 * each brought its own names and its own values: 12px against 13.5px for the
 * name, 11px against 12px for the house in two different greys, a 34x44
 * thumbnail against a 42x54 one. Nothing was hardcoded - every one of those was
 * a token - so the CSS gate passed the whole time. The drift was in the names,
 * which is exactly what that gate cannot see.
 *
 * These values are the ones from `More from this brand`, because that is the
 * module that had been looked at hardest.
 *
 * ⚠️ THE TRAILING SLOT IS DELIBERATELY NOT HERE. One list ends in a release
 * year and the other in a same-house chip; those are different things and keep
 * their own classes. Sharing a row is not sharing everything in it.
 */
.ov-card-row {
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: var(--space-m);
	/*
	 * ⚠️ THE ROW IS NOW THE ONLY THING HOLDING THE INSET, so it has to
	 * hold the WHOLE inset.
	 *
	 * `.ov-card--clip` stopped padding itself, which is what lets a divider
	 * reach both borders. The side effect was that the row's own 10px became
	 * the entire gap between a name and the card edge, and beside a
	 * `--pad-s` card at 18px it read as a mistake rather than a density.
	 * Matching that 18px puts the two side by side without either looking
	 * like the odd one.
	 *
	 * Vertical stays smaller: 18px above AND below every row would make a
	 * four-row card tall for no gain, and the divider already separates them.
	 */
	padding: var(--space-m) var(--space-xl);
	border-top: 1px solid var(--color-border);
}

/* The first row sits under the card's own heading, which is its rule. */
.ov-card-row:first-child {
	border-top: 0;
}

.ov-card-row:hover {
	background: var(--color-surface-sunk);
}

.ov-card-row__shot,
.ov-card-row__shot img {
	inline-size: 4.2rem;
	block-size: 5.4rem;
	border-radius: var(--radius-s);
	object-fit: contain;
	flex-shrink: 0;
	background: repeating-linear-gradient(135deg, var(--color-surface-hover) 0 0.6rem, var(--color-surface-sunk) 0.6rem 1.2rem);
}

/*
 * ⚠️ THE HATCH IS THE PLACEHOLDER, SO A REAL PICTURE TAKES IT OFF. The rule
 * above was written when nothing filled this slot, and once the ratings rows
 * got their bottles every one of them sat on a hatched panel.
 *
 * ⚠️ AND THEN MULTIPLY, WHICH IS THE ARCHIVE LIST VIEW'S OWN ANSWER TO THE
 * SAME PROBLEM. Transparent is not enough: the bottles are photographed ON
 * white, so on a white row that is invisible and the moment the row greys on
 * hover each one draws a white tile around its bottle. Multiply drops the
 * white into whatever is behind it.
 *
 * The grid view rejects multiply because its panel is permanently grey, so the
 * cost of lightening a dark bottle is permanent too. Here, as in the list view,
 * the resting row is white - where multiply is a no-op - and it only does any
 * work on the one row being pointed at. `.ov-view-list .oview-card__media`
 * carries the same two lines and the same reasoning.
 */
/*
 * ⚠️ AND THE SAME LIST IS DRAWN TWO WAYS, WHICH IS WHY THIS NEEDS TWO
 * SELECTORS.
 *
 * "Newest releases" puts the photograph in the slot: `<img class="shot">`.
 * "More from this brand" WRAPS it: `<div class="shot"><img class="similar__img">`.
 * So the rule below matched one module and not the other, and on the brand
 * module every bottle kept a white tile around it on hover while the module
 * directly above it did not. One reader, two behaviours, no reason.
 *
 * This is the sidebar's recurring fault and the reason `check-class-drift.js`
 * exists: two drawings of one list, each bringing its own vocabulary. The gate
 * reads rendered appearance, and both of these render a bottle in a box, so
 * only a person pointing at one of them would ever have caught it.
 */
img.ov-card-row__shot,
.ov-card-row__shot > img {
	background: transparent;
	mix-blend-mode: multiply;
}

/*
 * ⚠️ AND THE WRAPPER'S OWN HATCH COMES OFF TOO. In the wrapped shape
 * the hatch is on the DIV, so clearing it on the image leaves the placeholder
 * pattern sitting behind a real photograph. `:has()` rather than a class, for
 * the same reason as the card media above: whether a picture arrived is a fact
 * about the row, not something the template knows when it renders.
 */
.ov-card-row__shot:has(> img) {
	background: transparent;
}

/*
 * ⚠️ A PICTURE KEEPS ITS OWN SHAPE, AND EVERY BOX BELOW WAS DRAWN WITHOUT ONE.
 *
 * Each of these started in the mockup as a hatched placeholder with an aspect
 * ratio and nothing in it. Wired to real pictures, the box kept its ratio and
 * gained no `object-fit`, which defaults to `fill`: the whole picture is drawn
 * into the whole box whatever the box's shape. The homepage's lead story was a
 * 4:3 photograph stretched a third wider, the thumbnails beside it squashed
 * into squares, and a square picture in a story card came out 78% too wide.
 * `check-mobile.js` now fails on it (D180).
 *
 * Two answers, and the difference is what may be cut off:
 *
 *   cover     a photograph. It fills the box and loses a strip at the edges,
 *             which is how every news site frames a picture it did not take
 *             for that box.
 *   contain   a bottle. Cropping one takes off its cap or its base, so it fits
 *             inside the box instead and multiplies onto the ground, the same
 *             answer as the ratings rows above.
 *
 * ⚠️ And the hatch comes off, for the reason given above: it is what an EMPTY
 * box looks like.
 */
img.ov-news-lead__shot,
img.ov-news-item__thumb,
img.ov-figure-shot,
img.ov-story__shot {
	object-fit: cover;
	background: none;
}

img.ov-hero-shot,
img.ov-release-shot {
	object-fit: contain;
	background: transparent;
	mix-blend-mode: multiply;
}

/* Takes the slack, so anything trailing lands at the end of the row. */
.ov-card-row__body {
	min-inline-size: 0;
	flex: 1;
}

.ov-card-row__name {
	display: block;
	/*
	 * --text-m, not the --text-m-2 the drawing used for this. That token is not
	 * in the design package at all: it comes from the mockup's own scale and
	 * lives only inside the generated part files, scoped to their root. Reaching
	 * for it here is how a vendor's off-scale step becomes ours by accident, and
	 * check-css-tokens.py said so on the first run.
	 */
	font-size: var(--text-m);
	font-weight: 600;
	line-height: 1.3;
}

.ov-card-row__house {
	display: block;
	font-size: var(--text-xs);
	font-weight: 400;
	color: var(--color-text-muted);
}

/* The first row sits under the heading's own rule, so it needs no second one. *//* Takes the slack, so the timestamp lands at the end of the row. */.ov-latest__added {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	color: var(--color-success-ink);
	flex-shrink: 0;
}

/*
 * ⚠️ A LINK IN AN ARTICLE BODY, WHICH THE RESET WOULD OTHERWISE HIDE.
 *
 * The frame carries `a { color: var(--color-text); text-decoration: none; }

`,
 * which is right for a nav bar and a card and wrong for prose: it makes a link
 * inside a paragraph identical to the words around it. Nothing in the drawing
 * styled one, and nothing would have caught it - the page renders, every link
 * is simply invisible.
 *
 * Underlined rather than only coloured, because colour alone is not a
 * distinction for a reader who cannot see this one (WCAG 1.4.1), and because
 * an underline is what a reader is looking for.
 */
.ov-prose a,
.ov-prose-flow a {
	/* ⚠️ The ink: the fill is 3.72:1 on white and a body link is words. */
	color: var(--color-accent-ink);
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

.ov-prose a:hover,
.ov-prose-flow a:hover {
	color: var(--color-accent-hover);
}

/*
 * ⚠️ PROSE SETS ITS OWN TYPE, and it has to.
 *
 * The site's `--text-m` is 1.3rem, which is right for an interface: a card, a
 * row, a facet count, a label. It is small for reading a thousand words. The
 * article was drawn against 1.6rem and rendered at 1.3rem, which is the whole
 * of why the built page looked cramped beside the mockup.
 *
 * ⚠️ REDECLARING THE TOKEN, NOT OVERRIDING THE RULE. The article's rules say
 * `font-size: var(--text-m)` and live in a promoted frame that loads after this
 * file, so any rule here would lose on order. A custom property does not care
 * about order: it resolves from the nearest ancestor that sets it, which is the
 * prose container itself.
 *
 * So the drawing's rules are untouched and still say `--text-m`. Inside prose,
 * that means something else - which is what a token is for, and the reason a
 * reskin of this is one edit rather than a hunt.
 */
.ov-prose,
.ov-prose-flow {
	/*
	 * ⚠️ THE WHOLE STEP, NOT JUST THE BODY. Raising `--text-m` alone left the
	 * blockquote (`--text-xl`, 1.6rem) SMALLER than the paragraphs around it and
	 * a heading barely above them: a hierarchy inverted by fixing half of it.
	 *
	 * These are the values the article was drawn against, taken from the drawing
	 * itself and scoped here rather than declared globally - the interface keeps
	 * its own smaller scale, which is right for a card and wrong for a thousand
	 * words.
	 */

	/*
	 * ⚠️ THE SITE'S READING SIZE, NOT A NUMBER. `--text-xl-2` is what the
	 * fragrance page sets its description in - the one place on this site that
	 * already asks somebody to read a paragraph - so an article is set in the
	 * same thing rather than in whatever its own drawing happened to choose.
	 *
	 * Pointing at the token rather than copying its value means the day that
	 * reading size changes, both move together. That is the entire argument for
	 * tokens, applied to the one measurement a reader actually notices.
	 */
	--text-m: var(--text-xl);

	/*
	 * ⚠️ AND THE RHYTHM, FOR THE SAME REASON AND BY THE SAME MEANS.
	 *
	 * The gaps down a rendered article measured 10, 18, 23, 25 and 38 pixels,
	 * with nothing behind the differences: the container contributes a flex
	 * `gap` and each element contributes its own margins, and the sum was
	 * whatever those two happened to add to.
	 *
	 * The drawing's rules already spend the space tokens - `gap: var(--space-m)`
	 * between blocks, `margin: var(--space-l) 0` around a figure, a heading, a
	 * quote. Setting those tokens here sets the whole rhythm at once, without
	 * touching a rule, and keeps it proportional to the reading size rather than
	 * to the interface around it.
	 *
	 *   space-m   between any two blocks. The base beat.
	 *   space-l   added above a heading, and around anything set apart from the
	 *             text: a figure, a quote, a rule.
	 *   space-s   added above a subheading, which belongs to what follows it.
	 *   space-xs  a caption to its own image, which is not a gap but a join.
	 */
	/* One step up from the interface, said as the scale rather
	   than as a number, so it follows when the scale moves. */
	--space-xs: var(--space-s);
}

/*
 * ⚠️ A RULE AND AN IMAGE ARE BLOCKS LIKE ANY OTHER.
 *
 * The drawing styled the figure and forgot the `<hr>`, which is fair: nobody
 * draws one. WordPress emits them, so the template has to answer for it, and a
 * browser default rule is an inset 3D groove from 1996.
 *
 * The caption is the other half: it belongs to its image rather than to the
 * text below it, so it sits tight under the picture and the figure's own
 * margin does the separating.
 */
.ov-prose hr,
.ov-prose-flow hr {
	border: 0;
	border-block-start: 1px solid var(--color-border);
	margin-block: var(--space-l);
}

/*
 * ⚠️ AND THE FIGURE, because WordPress brings its own margin.
 *
 * `.wp-block-image` carries `margin: 0 0 1em`, and 1em on a figure is whatever
 * the figure's font-size happens to be - 13px here, where every other set-apart
 * block sits at 16. It measured as the one element out of step in an otherwise
 * even column, which is exactly how a rhythm stops looking like one.
 *
 * The font-size is set too, so the caption inside it reads at the prose scale
 * rather than at whatever the figure inherited.
 */
.ov-prose figure,
.ov-prose-flow figure,
.ov-prose .wp-block-image,
.ov-prose-flow .wp-block-image {
	font-size: var(--text-m);
	margin-block: var(--space-l);
	margin-inline: 0;
}

/*
 * ⚠️ ONE CAPTION, AND THERE WERE THREE.
 *
 * A caption under the lead image, a caption under an inline figure, and the
 * one WordPress writes for a figure in the article body. All three say the
 * same kind of thing and all three looked different: 12px against 14px,
 * `--color-text-faint` against `--color-text-muted`, and no space above the
 * lead one at all, so it sat flush against the picture while the other two had
 * room.
 *
 * The lead and inline captions were drawn as `.ov-caption` with a top margin
 * and mapped onto the `note` role, which is a quieter thing with no margin. A
 * caption is not a note: it belongs to the picture above it, which is why it
 * needs the small gap and why the gap has to be smaller than the one below.
 */
.ov-caption,
.ov-prose figcaption,
.ov-prose-flow figcaption,
.ov-figure figcaption {
	font-size: var(--text-xs);
	line-height: 1.5;
	color: var(--color-text-muted);
	margin: var(--space-xs) 0 0;
	text-align: start;
}

/*
 * ⚠️ A HEADING BELONGS TO WHAT FOLLOWS IT, so the space above it has to be
 * larger than the space below. With a flex `gap` the space below is the gap and
 * cannot be reduced, so the heading takes its lead-in from the margin instead -
 * which is what the drawing already does, and what the tokens above size.
 *
 * Stated here because it is the one rule of the rhythm that is not obvious
 * from the numbers, and the first thing somebody will undo by making them
 * symmetrical.
 */

/*
 * ⚠️ THE COLUMN IS A CONTAINER, AND IT WAS BUILT AS TEN CHILDREN AGREEING.
 *
 * An article is a measured column: everything sits in the same 72rem and the
 * same rhythm runs down it. The drawing said so by putting
 * `max-width: 72rem; width: 100%; margin-inline: auto` on each of the ten
 * blocks it drew. That reads as a column and is not one. There is no container
 * holding a measure, only ten classes that happen to hold the same number.
 *
 * So a block from any other drawing lands full width. The accord chart, the
 * releases card and both section heads came from the fragrance and home
 * drawings, were placed in the article by wiring, and none of them had any way
 * to learn what column they were in: they ran the full 1425px while the text
 * beside them ran 720px. Nothing was misconfigured. Nobody had ever written the
 * column down.
 *
 * Here it is written down. The measure and the beat belong to the container,
 * every child inherits both by being a child, and a block that genuinely wants
 * the full width says so with `ov-bleed` rather than by omission.
 */
/*
 * ⚠️ TWO CLASSES, AND IT HAS TO BE TWO.
 *
 * `modules.css` loads BEFORE every promoted part, so a rule of one class here
 * loses every tie to a rule of one class in a drawing's frame. `.ov-page-section`
 * is declared `display: block` by the fragrance frame; `.ov-article-flow` asking
 * for `display: flex` at the same specificity was simply ignored, and the column
 * had no flex container, no beat, and no alignment - while `getComputedStyle`
 * still reported `gap: 24px`, because a gap on a block element is set and inert.
 *
 * The element carries both classes, so naming both is not a specificity trick:
 * it is the accurate selector for what this is, a page section that is an
 * article column.
 */
.ov-page-section.ov-article-flow {
	--measure: 72rem;

	display: flex;
	flex-direction: column;

	/*
	 * ⚠️ CENTRED BY ALIGNMENT, NOT BY MARGIN.
	 *
	 * `margin-inline: auto` on the children is the obvious way and it loses:
	 * it is one class of specificity, and so is every heading class that
	 * declares `margin: 0`. `.ov-sec-title` does, loads later, and the section
	 * heading alone sat hard against the left edge of a column everything else
	 * was centred in.
	 */
	align-items: center;

	/*
	 * The same beat as the prose inside it. An article reading at one rhythm
	 * between its paragraphs and another between its blocks is the thing this
	 * is for.
	 */
	gap: var(--space-xl);

	/*
	 * ⚠️ THE GUTTER, WHICH THIS COLUMN NEVER HAD.
	 *
	 * `.ov-page-frame` carries `padding-inline: var(--gutter)` and every archive
	 * and entity page gets its inset from there. An article is the one page that
	 * does not use that frame: it is a section holding this column directly. So
	 * it inherited `.ov-page-section`'s `padding: 0` and nothing put it back, and
	 * the title, the standfirst and every paragraph rendered at x=0, hard against
	 * both edges of the phone.
	 *
	 * ⚠️ AND IT LOOKED CORRECT ON A DESKTOP FOR THE WHOLE TIME. The
	 * children are capped at the 72rem measure and centred, so above about 800px
	 * there is white space either side whether or not anything asked for it. The
	 * fault only appears once the viewport is narrower than the measure, which is
	 * every phone and no monitor anybody built this on.
	 *
	 * ⚠️ ON THE COLUMN, NOT ON ITS CHILDREN. Padding on `> *` would put
	 * the gutter INSIDE each child's box, and one of them - the author card - has
	 * a background and a border. Its white panel would have run to both edges of
	 * the screen with its text inset, which is a different bug wearing the same
	 * measurement.
	 */
	padding-inline: var(--gutter);
}

/*
 * ⚠️ TWO MEASURES IN ONE CENTRED COLUMN SHOW UP AS AN INDENT.
 *
 * `.ov-prose > p` caps a paragraph at `68ch`, which is a reading measure, and
 * `.ov-article-flow > *` caps every child at `--measure`, which is a column
 * width. Here those compute to 792px and 720px, and the column centres what is
 * in it, so every heading sat 36px in from the text it belongs to, exactly half
 * the difference. Measured on the privacy policy.
 *
 * ⚠️ AND THE FIX HAS TO BE A FONT-INDEPENDENT UNIT. Capping the headings at
 * `68ch` instead would resolve `ch` against each element's own font, so a 36px
 * serif heading would come out far wider than the text under it. One column,
 * one width, said in rem.
 */
.ov-article-flow.wordpress > p {
	max-width: var(--measure);
}

/*
 * ⚠️ A HEADING BELONGS TO WHAT FOLLOWS IT, so it wants more space above than
 * below, and a flex `gap` alone gives it the same on both sides - which reads
 * as belonging to neither.
 *
 * ⚠️ SCOPED TO THE UN-TEMPLATED PAGE, which is what `.wordpress` marks: in a
 * drawn article every heading is an element carrying margins of its own, and
 * this would add to them.
 */
.ov-article-flow.wordpress > h2 {
	margin-block-start: var(--space-l);
}

.ov-article-flow.wordpress > h3 {
	margin-block-start: var(--space-m);
}

.ov-article-flow.wordpress > h2:first-child,
.ov-article-flow.wordpress > h3:first-child {
	margin-block-start: 0;
}

.ov-article-flow > * {
	max-width: var(--measure);
	width: 100%;
	margin-inline: auto;
}

/*
 * The opt-out, so full width is a decision rather than an oversight.
 *
 * ⚠️ IT HAS TO CLIMB BACK OUT OF THE GUTTER. The column is padded
 * now, so `max-width: none` alone reaches the padding edge rather than the
 * screen edge, which is an element declaring full bleed and not getting it.
 */
.ov-article-flow > .ov-bleed {
	max-width: none;
	width: calc(100% + 2 * var(--gutter));
	margin-inline: calc(-1 * var(--gutter));
}

/*
 * ⚠️ A RULE NEEDS AIR ON BOTH SIDES OF IT.
 *
 * The byline draws a line above and below itself, so the gap either side is
 * measured to the line and the padding inside it is measured from the line. At
 * the drawn 10px the name sat closer to the rules than the rules sat to the
 * article, which reads as the byline belonging to neither block. Matching the
 * padding to the beat puts the same 24px on all four sides of both rules.
 */
.ov-article-flow > .ov-byline {
	padding-block: var(--space-xl);
}

/*
 * The head is one block, so its parts sit closer to each other than the head
 * sits to the article: kicker, title and standfirst at half the beat.
 */
.ov-article-flow > .ov-head {
	gap: var(--space-m);
}

/*
 * ⚠️ A WRAPPER THE DRAWING DID NOT DRAW MUST NOT CHANGE THE LAYOUT.
 *
 * The drawing marked the loop on the tag itself, which is the pattern the
 * read-next cards use and it works. Wiring a link that also carries an href
 * puts the query on a generated block around it instead, and a Bricks block is
 * a full-width flex column: five pills that were drawn as a row came out as
 * five rows, each 720px wide.
 *
 * `display: contents` makes the generated wrapper transparent, so the tags are
 * flex children of the list the drawing drew them in.
 */
.ov-tag-list > .brxe-block:not([class*="ov-"]) {
	display: contents;
}

/*
 * ⚠️ A SECTION HEAD IS A ROW; A SECTION TITLE IS THE HEADING INSIDE IT.
 *
 * The drawing gave both `data-ov-role="section-title"`, so both were given the
 * serif heading class and `Read next` and `All stories` stacked instead of
 * sitting at either end of a row. Two different things cannot share one role.
 */
.ov-sec-head {
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	align-items: flex-end;
	justify-content: space-between;
	gap: var(--space-s);
}

/*
 * ⚠️ AN EYEBROW IS A CHIP; A LABEL IS NOT.
 *
 * `Written by` and the category over a read-next card are quiet mono labels in
 * the drawing. Mapped onto `eyebrow` they took a card eyebrow's fill and, being
 * block-level, stretched: a full-width purple bar reading WRITTEN BY across the
 * author card and across every teaser.
 *//*
 * ⚠️ AND A CHIP FITS ITS CONTENTS WHEREVER IT IS PUT. A chip is only a chip
 * because it is the size of its word; in a block context it filled the line.
 */
.ov-eyebrow {
	width: fit-content;
	max-width: 100%;
}

/*
 * ⚠️ A CARD IS A STACK, AND THE SITE ONLY LOOKED THAT WAY BY ACCIDENT.
 *
 * Until the author card's stylesheet was rescoped off `.ov-card`, its private
 * `display: flex; flex-direction: column; gap: var(--space-s)` was reaching
 * every card on every page. Taking the leak away moved the fragrance page: both
 * cards there closed up by 8px under their titles, because that 8px had been
 * coming from a component about an article.
 *
 * Measured before deciding. The 8px is right - a card's title should not sit on
 * its contents - and it is what the site has been reviewed and approved
 * looking like. So it stays, declared here as the card's own rule rather than
 * arriving from somewhere that has nothing to do with cards.
 */
.ov-card {
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: var(--space-s);
}

/*
 * The author card reads as a row: portrait beside the writing, not above it.
 * Its layout is the component's own, in `parts/site-author-card-article.css`;
 * what belongs here is the one thing the component cannot know, which is how
 * its body behaves next to a portrait that will not shrink.
 */
.ov-card--byline > .ov-card-row__body {
	min-width: 0;
	flex: 1 1 28rem;
}

/*
 * ⚠️ ONE STORY CARD, AND THE SITE HAD DRAWN IT TWICE.
 *
 * A picture, a category chip and a headline, linking to an article. The home
 * drawing called it `ov-news-lead`; the article drawing called it `ov-teaser`.
 * They are the same card and they disagreed about it: an 18px corner against a
 * 14px one, no border against a 1px border, a 24px headline against a 13px one.
 * Neither is wrong. There are just two of them, which is the thing this file
 * exists to stop.
 *
 * The values below are the HOME drawing's, unchanged, because that is the one
 * on a page that has been reviewed and liked. Nothing about the home page moves
 * by declaring them here.
 *
 * ⚠️ The home page still carries its own scoped copy in
 * `parts/site-news-block-homepage.css` and that copy still wins there, so this
 * is one definition and one duplicate rather than one definition. Retiring the
 * duplicate means unscoping a component, which is the same job as the four
 * components sharing `.ov-card`, and it is the next one.
 */
.ov-story {
	display: block;
	box-sizing: border-box;

	/*
	 * ⚠️ THE CARD CARRIES ITS OWN SPACING, because the page underneath it
	 * does not agree with any other page about what a space is.
	 *
	 * D115 gave the site one scale and stopped a FRAME bringing its own. Every
	 * promoted PART still brings one, scoped to its root, and they disagree:
	 * `--space-m` is 1rem in the site's tokens, 1.6rem inside
	 * `site-news-block-homepage.css`, and a fluid 1.6rem-to-2.2rem clamp inside
	 * `site-read-next-article.css`. Three values, one name, and the card picks
	 * up whichever part it happens to be standing in - so the same card put in
	 * two places was 16px under the picture on the home page and 22px in the
	 * article.
	 *
	 * Pinned here to the home page's values, which are the ones on the page
	 * that has been reviewed. That is a stopgap and it says so: the real fix is
	 * the same one D115 applied to frames, applied to the twenty-three parts,
	 * and it is too large to do as a side effect of a card.
	 */
	/* One step up from the interface, said as the scale rather
	   than as a number, so it follows when the scale moves. */
	--space-xs: var(--space-s);
}

.ov-story__shot {
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
	aspect-ratio: 16 / 9;
	border-radius: 1.8rem;
	background: repeating-linear-gradient(135deg, var(--color-surface-sunk) 0 0.8rem, var(--color-surface) 0.8rem 1.6rem);
	margin-bottom: var(--space-m);
	overflow: hidden;
}

.ov-story__title {
	display: block;
	font-size: var(--text-xl);
	font-weight: 700;
	line-height: 1.25;
	letter-spacing: -0.04rem;
	margin-bottom: var(--space-xs);
}

.ov-story__copy {
	display: block;
	font-size: var(--text-xs);
	line-height: 1.6;
	color: var(--color-text-muted);
	max-width: 56ch;
}

/*
 * The category chip, unscoped so both cards can wear it. `fit-content` for the
 * same reason every chip needs it: a chip is only a chip because it is the size
 * of its word, and in a block context it fills the line.
 */
.ov-kicker-tag {	/*
	 * ⚠️ THE INK, NOT THE FILL. `--color-tier-mid` is oklch lightness 0.72 and
	 * white on it is 2.64:1, under the 4.5 floor (D117). Components used to
	 * override the token locally to a darker purple and this read as fine; with
	 * the scale unified (D124) the compensation went and the failure showed.
	 *
	 * `--color-tier-mid-ink` is the darker one the palette already carries for
	 * exactly this, so the tag says which it wants rather than the component
	 * redefining what mid means.
	 */

	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.11rem;
	text-transform: uppercase;
	color: var(--color-on-accent);
	background: var(--color-tier-mid-ink);
	border-radius: var(--radius-xs);
	padding: 0.2rem var(--space-2xs);
	width: fit-content;
	max-width: 100%;
}

/*
 * ⚠️ THE CHIP CARRIES NO MARGIN, and giving it one moved the home page.
 *
 * On the home page the chip sits in a flex row that spaces it; in the story
 * card it is a block-level child that has to space itself. Putting the 8px on
 * the chip served the card and added 8px to a home page nobody had touched.
 * Space belongs to the arrangement, not to the thing being arranged.
 */
.ov-story > .ov-kicker-tag {
	margin-bottom: var(--space-xs);
}

/*
 * ⚠️ A BYLINE THAT IS A LINK HAS TO LOOK LIKE ONE, at least on approach.
 *
 * The name keeps its weight and colour, because a byline underlined at rest
 * reads as a correction rather than a credit. The hover is the affordance, and
 * the pointer does the rest.
 */
a.ov-byline-name:hover {
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

/*
 * ⚠️ A LINK HAS TO BE BIG ENOUGH TO HIT, and two of these were made links this
 * week without being made targets.
 *
 * WCAG 2.5.8 puts the minimum at 24px in both directions. A line of 12px text
 * is about 19px tall, so any text that becomes a link and nothing else needs
 * the difference adding back.
 *
 * `width: fit-content` matters as much as the height: the byline name was a
 * block, so the whole 325px row was clickable including the empty part after
 * the name. Wide and short is not a bigger target, it is a confusing one.
 */
a.ov-byline-name,
a.ov-note,
a.ov-card-row__house {
	display: flex;
	align-items: center;
	width: fit-content;
	max-width: 100%;
	min-height: var(--tap-min);
}

/*
 * ⚠️ A SECTION IS A BAND; A PAGE FRAME IS THE COLUMN INSIDE IT. The vocabulary
 * had one word for both, and an archive came out full bleed.
 *
 * Every drawing so far has expressed this the same way and had no role to say
 * it with: an outer element running the full width of the viewport, and an
 * inner one holding the site's measure and the rhythm between bands. The brands
 * drawing wrote `.ov-section` and `.ov-page` and marked both `role="section"`,
 * because `section` was the only word available.
 *
 * So both became `.ov-page-section`, which declares `padding: 0` and no measure
 * on purpose, and the page lost its container: a 1425px sort control beside a
 * heading centred inside a 720px box it inherited from the article.
 *
 * ⚠️ AND THE DRAWING'S OWN RULES COULD NOT SAVE IT. `.ov-section` and `.ov-page`
 * are both declared by another drawing's frame, so the collision filter dropped
 * them exactly as it is meant to. Two drawings, one class name, two meanings:
 * mock4's `.ov-page` is a flex ROW for the fragrance page's two columns.
 */
.ov-page-frame {
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: var(--space-2xl);

	/*
	 * ⚠️ THE GUTTER, FOR THE SAME REASON `.ov-page` USES IT. This was 40px of
	 * inline padding inside a 1280px cap, which put a note, brand or accord
	 * page's content 40px from the edge under a header starting at 75px. The
	 * fragrance page had the same fault through `.ov-page`; these three had it
	 * through here, which is why fixing one left three behind.
	 *
	 * ⚠️ AND `section.ov-page-frame` BELOW ZEROES THE BLOCK AXIS AND THE CAP,
	 * so on a section this line contributes the inline gutter and nothing else.
	 * Naming the axes rather than using the `padding` shorthand is what lets
	 * that stay true.
	 */
	max-width: calc(var(--width-page) + 2 * var(--gutter));
	margin-inline: auto;
	padding-block: var(--space-xxl) var(--space-4xl);
	padding-inline: var(--gutter);
	box-sizing: border-box;
}

/*
 * A page's heading block. `head` means the ARTICLE head, which carries the
 * 72rem reading measure, and an archive heading that inherits it sits in a
 * column two thirds the width of everything under it.
 */
.ov-page-head {
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: var(--space-xs);
}

/*
 * ⚠️ THE HATCH IS THE EMPTY STATE, NOT THE FRAME.
 *
 * A drawing gives `.oview-card__media` a hatched fill so a card with no picture
 * reads as a picture that is missing rather than as a card that is broken. That
 * is right, and it is what the box should look like when it is EMPTY. Applied
 * unconditionally it showed through and around every logo on the brands archive:
 * every house had a striped mat behind it, which reads as a deliberate texture
 * and is not one.
 *
 * `:has()` rather than a class, because whether a picture arrived is a fact
 * about the row and not something the template can know when it renders. It also
 * out-specifies the frame's own `.oview-card__media`, so this holds wherever a
 * frame loads and whichever order they load in.
 */
.oview-card__media:has(img) {
	background: var(--color-surface);
}

/*
 * ⚠️ THE `All` IN AN A-Z STRIP IS THE FIRST ROW OF THE QUERY, NOT A BUTTON
 * BESIDE IT.
 *
 * The brands drawing drew both: a dark `All` pill outside the strip with
 * `href="#"`, and the letters loop, whose own first row is All and which has a
 * real href and knows when it is the current one. Two All buttons, and the one
 * that looked right did nothing.
 *
 * So the drawn pill goes (`remove` in the map) and the row that works wears it.
 * First child, because the query builds All first and its letter is deliberately
 * empty - which is the same reason `oview_letters` keys its rows on `label`.
 *//*
 * ⚠️ AN EMPTY STATE THAT IS ALWAYS ON SCREEN IS NOT AN EMPTY STATE.
 *
 * A drawing has to draw "no houses match those filters" to show what it looks
 * like, and there is no way for it to draw the condition. Imported, the panel
 * rendered under every result set: 3,190 houses, and underneath them a box
 * saying none of them matched.
 *
 * `:has()` rather than a Bricks condition, because the question is about what
 * the grid ACTUALLY rendered rather than about a count computed a second time
 * somewhere else - and because the no-reload filter replaces the grid in place,
 * so the answer has to be re-asked without anything running.
 */
/*
 * ⚠️ AND IT NEEDED A LOOK, NOT JUST A CONDITION.
 *
 * The rule below has hidden this panel since the brands drawing was adopted,
 * and nothing ever rendered one, so the class was styled for exactly half of
 * what it does. A message with no treatment reads as a page that broke rather
 * than as an answer.
 */
.ov-empty {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--space-2xs);
	padding: var(--space-2xl) var(--space-l);
	text-align: center;
	border: var(--border-hair) dashed var(--color-border);
	border-radius: var(--radius-m);
	background: var(--color-surface-sunk);
}

.ov-empty-title {
	font-size: var(--text-l);
	font-weight: 700;
	color: var(--color-text);
	margin: 0;
}

.oview-card-grid:has(.oview-card) ~ .ov-empty {
	display: none;
}

/*
 * The pager has nothing to page when nothing matched, and a Load more under an
 * empty grid is a button that appends nothing.
 */
.oview-card-grid:not(:has(.oview-card)) ~ .bricks-pagination {
	display: none;
}

/*
 * ⚠️ CLEAR ALL COMES FIRST, and the element renders it last.
 *
 * It is the one control in the row that is not a filter, so reading left to
 * right it wants to be the handle on the group rather than the last chip in it.
 * `order` rather than moving the markup: the element emits the chips in the
 * order it applies them, which is the order that should reach a screen reader.
 */
/*
 * ⚠️ THE ORDER ONLY. THE ROW'S OWN LAYOUT IS `filters.css`.
 *
 * This restated the display, the direction, the wrap and the gap, all of which
 * `.oview-chips` in filters.css already says. It never showed, because the
 * promoted brands frame loaded after this file and won with the drawing's
 * larger gap - so two files disagreed for weeks and the third one settled it.
 *
 * Adopting the frame into filters.css moved that answer EARLIER than this one,
 * this rule started winning, and the chips closed from 10px to 6px. Which
 * exposed the real problem: the gap had two homes. It has one now, and this
 * keeps only the thing that is genuinely about this site's reading order.
 */
.oview-chips .oview-chips__clear {
	order: -1;
}

/*
 * An archive's vertical rhythm.
 *
 * ⚠️ THE FRAME IS ALREADY THERE AND ONLY THE BEAT WAS MISSING. `ov_wrap_sections()`
 * puts a container inside every top-level section, so the measured column and the
 * centring are handled. What it does not do is space the page out: the fragrance
 * archive's title sat 8px above its own sidebar while the brands page, which is
 * the same page with different rows, had a whole `.ov-page-frame` of air.
 *
 * A gap and a top inset, matching `.ov-page-frame`, so the two archives keep the
 * same beat without the second one growing a container inside a container.
 */
.ov-archive-section > .brxe-container {
	display: flex;
	flex-direction: column;
	gap: var(--space-2xl);
	padding-block: var(--space-xxl) var(--space-4xl);
}

/*
 * ⚠️ THE ARTICLE'S TITLE CLASS FOLLOWS THE GLOBAL h1, IT DOES NOT COMPETE WITH IT.
 *
 * `.ov-article-title` is promoted from a drawing with a fixed 5.4rem and its own
 * media query. The theme style now carries the h1 treatment for every page, so
 * two definitions of the same thing exist - and the promoted one is a fixed
 * size, so on a phone it would override the fluid step that replaced its media
 * query.
 *
 * One value. `h1.ov-article-title` is a tag and a class, which is enough to beat
 * the promoted rule wherever a drawing still applies the class.
 */
/*
 * ⚠️ NO FONT-SIZE, for the same reason as .ov-sec-title: --text-h1 is what an
 * h1 already is. The comment above this rule described beating a promoted
 * drawing rule, and no element in the database wears the class any more.
 */
h1.ov-article-title {
	line-height: 1.05;
}

/*
 * The page title's face.
 *
 * ⚠️ HERE AND NOT IN THE THEME STYLE, because Bricks' font-family control
 * expects a family NAME: `var(--font-serif)` written there is emitted literally
 * and the browser hunts for a typeface with that name. The size applied and the
 * face did not, which reads as "the token is wrong" rather than "that control
 * does not take a token".
 *
 * A tag selector, so an h1 gets it by being one, and a token, so a reskin is one
 * value in tokens.css.
 */
h1 {
	font-family: var(--font-serif);
	letter-spacing: -0.12rem;
}

/* --- the archive frame ---------------------------------------------------- */

/*
 * One archive skeleton, and the facets are a REGION rather than a fixture.
 *
 * ⚠️ THE SIDE IS A CLASS, so moving the filters is a declaration in the builder
 * and a rule here, not a second template. `ov_archive_page()` emits
 * `--left`, `--right` or `--none`, and an archive with no facets simply has one
 * column rather than an empty gutter.
 */
/*
 * ⚠️ THE PAGE FRAME, AND IT WAS THE ARCHIVE'S ALONE.
 *
 * Scoped to `.ov-archive`, this gave the four archives 34px above their h1 and
 * a 28px rhythm down the page, and left every entity single page without it:
 * the same heading sat 34px higher on `/notes/musk/` than on `/notes/`, which
 * is one page type looking like a different site.
 *
 * A page frame is not a property of being an archive. `.ov-page-frame` is the
 * shared name and the archive keeps its own selector only so an already-built
 * template that has not been rebuilt yet still gets it.
 */
/*
 * ⚠️ THE SECTION FRAMES NOTHING. ITS CONTAINER DOES.
 *
 * A Bricks section takes the theme style's 40px inset and 1280px width by
 * default, and the container inside it adds the same again - so an entity page
 * was inset 80px while an archive was inset 40, and the same heading started
 * 40px further right on `/accords/woody/` than on `/accords/`.
 *
 * The archives avoid it because `ov_wrap_sections()` zeroes the section. This
 * says the same thing for any page carrying the shared frame.
 */
/*
 * ⚠️ THE BLOCK AXIS ONLY. An earlier version zeroed `padding-inline` here too,
 * and four archives plus the forum went to a 0px left edge on a phone - card
 * borders against the glass. `tokens.css` removes the CONTAINER's gutter
 * precisely because ACSS pads the SECTION, so the section's inline padding is
 * the one gutter this site has. Taking it away leaves none.
 */
section.ov-page-frame,
section.ov-archive,
section.oview-prose {
	max-width: none;
	/*
	 * ⚠️ AND `padding-block: 0`, SO THE CONTAINER IS THE ONLY THING SPACING THE
	 * PAGE. Bricks' theme style puts `padding-top: var(--space-l)` on every
	 * `.brxe-section`, which stacked on top of the container's own
	 * `--space-2xl-3`: an archive title sat 48px down and a page with no
	 * section - the forum - sat at 34px, and no single rule said what the
	 * number was. This is the same call `ov_wrap_sections()` already made for
	 * `padding-inline`, said for the other axis.
	 */
	padding-block: 0;
}

.ov-page-frame > .brxe-container,
.ov-archive > .brxe-container {
	display: flex;
	flex-direction: column;
	gap: var(--space-2xl);
	padding-block: var(--space-xxl) var(--space-4xl);
}

/*
 * ⚠️ A PAGE BRICKS DID NOT BUILD IS STILL A PAGE ON THIS SITE.
 *
 * Asgaros renders `/forum/` as ordinary WordPress content, and Bricks wraps
 * that in an `<article id="brx-content">` rather than the `<main>` it wraps a
 * template in. So the forum got none of the frame: the same 51px serif h1 sat
 * 48px higher there than on every archive, which is one site looking like two.
 *
 * `article#brx-content` is exactly "a page with no template", so this reaches
 * the next third-party page as well without naming it.
 */
article#brx-content {
	padding-block: var(--space-xxl) var(--space-4xl);
	/*
	 * ⚠️ AND THE GUTTER HERE, because this is not a section and ACSS never
	 * inset it. `--gutter` is ACSS's own responsive value - 18px on a phone,
	 * 80px on a wide screen - so the forum breathes exactly as every framed
	 * page does rather than to a number this file invented.
	 *
	 * ⚠️ AND THE CAP GROWS BY TWO GUTTERS, which is D165's rule and was missed
	 * here. Bricks caps this box at the page width, so the gutter came out of
	 * the content: above 1280px the forum sat 80px inside the header on each
	 * side, 1120px of forum under a 1280px frame. At 1280 exactly the two
	 * agree, which is the width every check looks at.
	 */
	padding-inline: var(--gutter);
	box-sizing: border-box;
}

/*
 * ⚠️ `.wordpress` BECAUSE BRICKS' THEME STYLE SAYS IT. The content width is
 * written as `#brx-content.wordpress { width: 1280px }`, one class more than the
 * rule above, so a `max-width` there lost to it. The box is the page plus a
 * gutter each side; `max-width: 100%` keeps it inside a narrower screen.
 */
article#brx-content.wordpress {
	width: calc(var(--width-page) + 2 * var(--gutter));
	max-width: 100%;
}

.ov-archive-cols {
	display: grid;
	column-gap: var(--space-xl);
	row-gap: var(--space-l);
}

.ov-archive-cols--left {
	grid-template-columns: minmax(0, var(--width-sidebar)) minmax(0, 1fr);
}

/*
 * ⚠️ THE ASIDE STAYS FIRST IN THE MARKUP. Reversing the DOM would put the
 * filters after the results for anyone reading in order, and the whole point of
 * a filter is that you meet it before the thing it filters. `order` moves the
 * picture; the document is unchanged.
 */
.ov-archive-cols--right {
	grid-template-columns: minmax(0, 1fr) minmax(0, var(--width-sidebar));
}

.ov-archive-cols--right > .ov-archive-aside {
	order: 2;
}

.ov-archive-cols--none {
	grid-template-columns: minmax(0, 1fr);
}

/*
 * A phone has no room for a column beside anything, whichever side it was on.
 */
@media (max-width: 767px) {
	.ov-archive-cols,
	.ov-archive-cols--left,
	.ov-archive-cols--right {
		grid-template-columns: minmax(0, 1fr);
	}

	.ov-archive-cols--right > .ov-archive-aside {
		order: 0;
	}
}

.ov-archive-aside {
	display: flex;
	flex-direction: column;
	/*
	 * --space-m, not --space-l. Each facet is a bordered card, so the border
	 * does the separating the gap used to do alone, and 24-35px between them
	 * read as four unrelated widgets.
	 */
	gap: var(--space-m);
}

/*
 * ⚠️ WHERE THE ROW SITS, NOT HOW THE CHIPS INSIDE IT ARE SPACED.
 *
 * This set a column-gap and a row-gap, and while the brands frame loaded last
 * that lost to the drawing's own `.oview-chips { gap: var(--space-m) }

` and
 * nobody saw it. Adopting the frame into `filters.css` moved it EARLIER in the
 * cascade, this rule started winning, and the chips closed up by four pixels.
 *
 * The gap belongs to the chip control, which is what `.oview-chips` is. An
 * archive says where the row goes; the control says what it looks like, on
 * every page it appears on rather than only on the ones with an aside.
 */
.ov-archive-chips {
	display: flex;
	align-items: center;
	margin-top: var(--space-s);
}

/*
 * The results column's rhythm.
 *
 * ⚠️ ONE GAP, NOT A MARGIN PER REGION. The toolbar, the A-Z, the chips, the grid
 * and the pager each used to space themselves, so adding a region meant finding
 * out afterwards that it had no spacing at all - which is how the A-Z arrived
 * flush against the toolbar above it.
 *
 * A flex gap only separates things that exist, so a page with no A-Z and no
 * chips reads exactly like one that has both.
 */
.ov-archive-main {
	display: flex;
	flex-direction: column;
	gap: var(--space-l);
	min-width: 0;
}

/*
 * Their own margins would sit on top of the gap and double it. The grid's comes
 * from cards.css, where it is right for a grid that is not in this column.
 */
.ov-archive-main > .oview-card-grid,
.ov-archive-main > .ov-archive-chips {
	margin-top: 0;
}

/*
 * The filter column follows the scroll.
 *
 * ⚠️ `align-self: start` IS LOAD-BEARING. A grid item stretches to its row by
 * default, so the column is already as tall as the results beside it and
 * `position: sticky` has nothing left to travel - it applies, changes nothing,
 * and reads as the property not working.
 *
 * ⚠️ AND IT SCROLLS ITSELF. Four open facets are taller than a laptop viewport,
 * so without a ceiling the bottom facet is unreachable the moment the column
 * stops moving. `overscroll-behavior` keeps that scroll from continuing into the
 * page once it reaches its end.
 */
/*
 * ⚠️ IT SCROLLS WITH THE PAGE AND LOCKS. IT DOES NOT SCROLL ITSELF.
 *
 * This carried a `max-block-size` of one viewport and `overflow-y: auto`, on
 * the reasoning that four open facets are taller than a laptop and the column
 * would otherwise clip its own contents. That reasoning was about the CONTENT
 * and the cost was to the INTERACTION: a second scrollbar inside the page, so a
 * wheel over the sidebar moved the filters and a wheel two inches right moved
 * the results. Two scroll contexts in one view is worse than a tall column.
 *
 * So the aside travels with the page and pins at the top. A sidebar taller than
 * the viewport keeps its foot below the fold while pinned, which is the trade
 * being made deliberately: reaching the last facet costs a scroll back up,
 * where before it cost knowing there was a second scrollbar.
 */
.ov-archive-aside--sticky {
	position: sticky;
	top: var(--sticky-top);

	/*
	 * ⚠️ LOAD-BEARING. A grid item stretches to its row by default, so a sticky
	 * one is already as tall as the column and has nothing left to travel
	 * within. Nothing looks wrong; it simply never sticks.
	 */
	align-self: start;
}

/*
 * Never on a phone. The aside is a full-width band above the results there, so
 * pinning it would cover them.
 */
@media (max-width: 767px) {
	.ov-archive-aside--sticky {
		position: static;
	}
}

/* ==========================================================================
 * The site modules, adopted from 26 promoted stylesheets.
 *
 * ⚠️ MERGED, THEN SPLIT. These 376 rules were part of 101,894 bytes adopted
 * out of 26 promoted stylesheets, appended in the order those files used to
 * load. That preserved every collision exactly, which was the right thing to do
 * during the move and the wrong thing to leave behind: 64 selectors were
 * declared more than once and .ov-card was declared by NINE different drawings,
 * so what the site looked like depended on a filename sort.
 *
 * One selector, one home, one value. Merged property by property with the last
 * declaration winning, which is what the browser was already computing - so the
 * pages render identically and the file now says what it means.
 *
 * ⚠️ Classified on the LEFTMOST class. `.ov-col-side .ov-card` is a card seen
 * inside a sidebar, so it belongs with the sidebar rather than with the card.
 * ========================================================================== */

.ov-page-head {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--space-2xs);
}

.ov-flag {
	display: block;
	width: 2rem;
	height: auto;
	border-radius: var(--radius-2xs);
	flex-shrink: 0;
}

.bricks-pagination {
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: var(--space-l);
	padding-top: var(--space-xs);
}

.bricks-pagination-list {
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: var(--space-xs);
}

.ov-block--accords {
	display: block;
}

.ov-block--accords .ov-accord-tile:hover {
	border-color: var(--color-border-strong);
	color: var(--color-text);
}

.ov-head .ov-kicker-row {
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--space-xs);
}

.ov-head .ov-eyebrow {	/*
	 * ⚠️ THE INK, NOT THE FILL. `--color-tier-mid` is oklch lightness 0.72 and
	 * white on it is 2.64:1, under the 4.5 floor (D117). Components used to
	 * override the token locally to a darker purple and this read as fine; with
	 * the scale unified (D124) the compensation went and the failure showed.
	 *
	 * `--color-tier-mid-ink` is the darker one the palette already carries for
	 * exactly this, so the tag says which it wants rather than the component
	 * redefining what mid means.
	 */

	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.11rem;
	text-transform: uppercase;
	color: var(--color-on-accent);
	background: var(--color-tier-mid-ink);
	border-radius: var(--radius-xs);
	padding: 0.2rem 0.7rem;
	margin: 0;
}

.ov-head .ov-note {
	font-size: var(--text-xs);
	color: var(--color-text-muted);
	line-height: 1.5;
	margin: 0;
}

.ov-head .ov-article-title {
	font-family: var(--font-serif);
	font-size: 5.4rem;
	font-weight: 400;
	line-height: 1;
	letter-spacing: -0.12rem;
	margin: 0;
	text-wrap: pretty;
}

.ov-card--byline {
}

.ov-card--byline .ov-note {
	font-size: var(--text-xs);
	color: var(--color-text-muted);
	line-height: 1.5;
	margin: 0;
}

.ov-card--byline .ov-byline-avatar {
	width: 5.6rem;
	height: 5.6rem;
	border-radius: var(--radius-full);
	background: var(--color-accent-soft);
	flex-shrink: 0;
}

/*
 * ⚠️ THE NAME WAS THE SAME SIZE AS THE SENTENCE UNDER IT. Both were
 * `--text-xs`, so a bold 12px name sat on a 12px bio and the card had no
 * heading. It is the reading size now, a clear step above the bio.
 */
.ov-card--byline .ov-byline-name {
	display: block;
	font-size: var(--text-xl);
	font-weight: 700;
}

/*
 * "WRITTEN BY" is an eyebrow like "MY COLLECTION" and the NEWS tag: capitals
 * and the eyebrow's spacing. A style, because the label is a component's text.
 */
.ov-card--byline .ov-meta--mono {
	letter-spacing: 0.13rem;
	text-transform: uppercase;
}

.ov-card--byline {
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	align-items: flex-start;
	gap: var(--space-m);
	background: var(--color-surface);
	border: 1px solid var(--color-border);
	border-radius: 18px;
	padding: var(--space-l);
	box-sizing: border-box;
	container-type: inline-size;
}


.ov-card--contributors {
	border: 0.1rem solid var(--color-border);
	border-radius: 2rem;
	padding: var(--space-l);
	box-sizing: border-box;
	container-type: inline-size;
}

.ov-card--contributors .ov-heading {
	font-size: var(--text-m);
	font-weight: 700;
	margin: 0;
}


.ov-card--forum {
	border: 0.1rem solid var(--color-border);
	border-radius: 2rem;
	padding: var(--space-l);
	box-sizing: border-box;
	container-type: inline-size;
}

.ov-card--forum .ov-heading {
	font-size: var(--text-m);
	font-weight: 700;
	margin: 0;
}

.ov-card--forum .ov-thread {
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: var(--space-s);
	padding: var(--space-s);
	border-radius: var(--radius-m);
	background: var(--color-surface-sunk);
}

.ov-card--forum .ov-thread:hover {
	background: var(--color-surface-sunk-2);
	color: var(--color-text);
}

.ov-card--forum .ov-thread__avatar {
	width: var(--control-h-s);
	height: var(--control-h-s);
	border-radius: var(--radius-full);
	background: var(--color-avatar-purple);
	flex-shrink: 0;
}

.ov-card--forum .ov-thread__replies {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	color: var(--color-on-accent);
	background: var(--color-tier-mid-ink);
	border-radius: var(--radius-full);
	padding: 0.2rem var(--space-xs);
	flex-shrink: 0;
}

.ov-crumb {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.1rem;
	text-transform: uppercase;
	color: var(--color-text-muted);
	min-height: 24px;
	min-width: 24px;
	display: inline-flex;
	flex-direction: row;
	align-items: center;
}

.ov-head {
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: var(--space-s);
	container-type: inline-size;
}

.ov-standfirst {
	font-size: var(--text-l);
	line-height: 1.5;
	color: var(--color-text-body);
	margin: 0;
	max-width: 54ch;
	text-wrap: pretty;
}

.ov-byline {
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--space-s);
	padding: var(--space-m) 0;
	border-top: 1px solid var(--color-border);
	border-bottom: 1px solid var(--color-border);
	container-type: inline-size;
}

.ov-byline-avatar {
	width: 3.6rem;
	height: 3.6rem;
	border-radius: var(--radius-full);
	background: var(--color-accent-soft);
	flex-shrink: 0;
}

.ov-byline-name {
	display: block;
	font-size: var(--text-xs);
	font-weight: 700;
}

/*
 * ⚠️ A LINK ON ITS OWN LINE IS A TARGET. WCAG 2.2 SC 2.5.8 exempts a link
 * INSIDE a sentence, because the line-height around it is not the author's to
 * choose. A brand's website sits alone in its own row, so nothing exempts it and
 * it measured 19.2px tall.
 */
.ov-meta a {
	display: inline-flex;
	align-items: center;
	min-block-size: var(--tap-min);
}

.ov-share-link {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.08rem;
	text-transform: uppercase;
	white-space: nowrap;
	color: var(--color-text-muted);
	background: var(--color-surface);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-s);
	padding: 0 var(--control-px-s);
	min-height: var(--control-h-s);
	min-width: 24px;
	display: inline-flex;
	flex-direction: row;
	align-items: center;
	box-sizing: border-box;
}

.ov-share-link:hover {
	background: var(--color-surface-sunk);
	color: var(--color-text);
}

/*
 * ⚠️ BRICKS' SOCIAL SHARING, IN THE SITE'S CONTROL. Where the second byline was
 * (D183). The element's own look is a row of brand-coloured tiles with black
 * glyphs, which beside "Copy link" read as a different site's toolbar. So each
 * link is the copy link's box - border, radius, height - with its glyph in the
 * text colour, and the row takes the slack so the two links sit at the end.
 */
.ov-byline > .ov-share-social {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-xs);
	margin: 0 auto 0 0;
	padding: 0;
	list-style: none;
}

.ov-share-social li {
	margin: 0;
}

.ov-share-social a {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	inline-size: var(--control-h-s);
	block-size: var(--control-h-s);
	background: var(--color-surface);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-s);
	color: var(--color-text-muted);
	transition: background-color 150ms ease, color 150ms ease;
}

.ov-share-social a:hover,
.ov-share-social a:focus-visible {
	background: var(--color-surface-sunk);
	color: var(--color-text);
}

.ov-share-social svg {
	inline-size: 1.4rem;
	block-size: 1.4rem;
	fill: currentColor;
}

.ov-figure {
	display: block;
	container-type: inline-size;
}

.ov-figure-shot {
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
	aspect-ratio: 16 / 9;
	border-radius: 14px;
	background: repeating-linear-gradient(135deg, var(--color-surface-sunk) 0 0.8rem, var(--color-surface) 0.8rem 1.6rem);
	border: 1px solid var(--color-border);
	box-sizing: border-box;
}

.ov-caption {
	line-height: 1.5;
}

.ov-figure-inline {
	display: block;
	float: right;
	width: 26rem;
	max-width: 100%;
	margin: 0.6rem 0 var(--space-s) var(--space-l);
	box-sizing: border-box;
}

.ov-sec-head {
	display: flex;
	flex-direction: row;
	align-items: flex-end;
	justify-content: space-between;
	flex-wrap: wrap;
	gap: var(--space-s);
	margin-bottom: var(--space-m);
}

/*
 * ⚠️ NO FONT-SIZE. An h2 IS this size, and this class is only ever on an h2 -
 * five of them, and nothing else wears it.
 *
 * It used to restate the site's own 2.8rem because a bare h2 did not get it:
 * tokens.css had a heading scale of its own and ACSS declares the same property
 * on the same selector afterwards, so the site's rule lost and the class was
 * the only thing holding the size. ACSS carries the site's heading scale now,
 * and tokens.css reads `--h2` from it, so the element is correct on its own and
 * restating it here would be a second place to change it.
 *
 * What is left is what a class should carry: the face, not the scale.
 */
.ov-sec-title {
	font-family: var(--font-serif);
	font-weight: 400;
	margin: 0;
	letter-spacing: -0.04rem;
}

.ov-sec-sub {
	font-size: var(--text-m);
	color: var(--color-text-muted);
	margin: 0;
}

.ov-sec-link {
	font-size: var(--text-xs);
	font-weight: 600;
	color: var(--color-text-muted);
	min-height: var(--tap-min);
	min-width: var(--tap-min);
	display: inline-flex;
	flex-direction: row;
	align-items: center;
}

.ov-arrow {
	width: var(--control-h);
	height: var(--control-h);
	min-width: var(--tap-min);
	min-height: var(--tap-min);
	border-radius: var(--radius-m);
	border: 0.1rem solid var(--color-border);
	background: var(--color-surface);
	cursor: pointer;
	font-size: var(--text-m);
	color: var(--color-text-body);
	display: inline-flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
	box-sizing: border-box;
}

.ov-arrow:hover {
	background: var(--color-surface-sunk);
	color: var(--color-text-body);
}

/*
 * THE BADGE, WRITTEN ONCE.
 *
 * ⚠️ IT WAS TWO CLASSES WITH ONE BODY. `.ov-tag-live` and
 * `.ov-tag-week` declared the same nine properties and differed in the
 * background, and a third copy sat in `pages.css` differing only in padding.
 * Three definitions of one object, each free to drift, and two of them already
 * had. The comment on the old rule even said so.
 *
 * So it is a block and its colour is a modifier, which is what the shape was
 * asking for. A new badge is one line.
 *
 * ⚠️ UPPERCASE BELONGS TO THE BLOCK. A mono face with wide tracking
 * only reads as a badge in capitals - "Live poll" in it looks like a mistake -
 * and that was true of both copies while being declared on one of them, three
 * thousand lines away from where the badge is defined.
 *
 * ⚠️ AND IT IS SMALLER NOW. `--text-xs` is already the floor of the
 * type scale, so the chip shrinks by spending less space around the text and
 * less tracking inside it, rather than by inventing a size below the scale.
 */
.ov-tag {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.08rem;
	text-transform: uppercase;
	color: var(--color-on-accent);
	background: var(--color-text-muted);
	border-radius: var(--radius-s);
	padding: var(--space-3xs) var(--space-xs);
	margin: 0;
}

.ov-tag--live {
	background: var(--color-error-ink);
}

.ov-tag--week {
	background: var(--color-warning-ink);
}

.ov-chip-accord {
	font-size: var(--text-xs);
	font-weight: 600;
	color: var(--color-on-accent);
	background: var(--color-error-ink);
	border-radius: var(--radius-full);
	padding: 0.7rem var(--space-s);
}

.ov-chip-accord--neutral {
	color: var(--color-text-body);
	background: var(--color-surface-sunk-2);
}

.ov-stat-card {
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-l-2);
	padding: var(--space-m);
	box-sizing: border-box;
}

.ov-stat-dot {
	width: var(--space-xs);
	height: var(--space-xs);
	border-radius: var(--radius-xs);
	background: var(--color-accent);
}

.ov-stat-value {
	font-size: var(--text-xl);
	font-weight: 800;
	letter-spacing: -0.1rem;
	margin: 0;
}

.ov-slider {
	display: flex;
	flex-direction: row;
	align-items: stretch;
	gap: var(--space-m);
	overflow-x: auto;
	/*
	 * ⚠️ WITHOUT THIS THE WHOLE PAGE DRAGS SIDEWAYS ON A PHONE.
	 *
	 * A rail holds about 2,700px of cards in a 341px box. Swipe it to its end
	 * and the default `auto` CHAINS the rest of the gesture outward: the page
	 * has no horizontal scroll of its own, so the browser rubber-bands the
	 * entire document instead and the site appears to come loose. Reported from
	 * a phone, and the page was right all along - measured at 375px, the
	 * document's scrollWidth equals the viewport exactly. The overflow was never
	 * the problem; the chaining was.
	 *
	 * `contain` keeps the gesture in the rail. The rail still has its own bounce,
	 * which is the feedback that tells a reader they have reached the last card.
	 *
	 * ⚠️ `contain` HERE RATHER THAN `none` ON THE ROOT. Locking the document
	 * would also work and would take the browser's own horizontal gestures with
	 * it, back-swipe among them. The rail is where the scroll starts, so the rail
	 * is where it should stop.
	 */
	overscroll-behavior-x: contain;
	scroll-behavior: smooth;
	padding-bottom: var(--space-s);
}

.ov-release-card {
	flex: 0 0 21.6rem;
	border: 0.1rem solid var(--color-border);
	border-radius: 1.8rem;
	padding: var(--space-s);
	box-sizing: border-box;
}

.ov-release-card:hover {
	border-color: var(--color-border-strong);
	color: var(--color-text);
}

.ov-release-shot {
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
	aspect-ratio: 1 / 1;
	border-radius: var(--radius-m);
	background: repeating-linear-gradient(135deg, var(--color-surface-hover) 0 0.8rem, var(--color-surface-sunk) 0.8rem 1.6rem);
	margin-bottom: var(--space-s);
}

.ov-news-row {
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	gap: var(--space-l);
	align-items: flex-start;
	container-type: inline-size;
}

.ov-news-lead {
	flex: 2 1 34rem;
	min-width: 0;
	box-sizing: border-box;
}

/*
 * The lead story's headline, one step above the list beside it. It shared
 * `.ov-story__title`'s `--text-xl` with every teaser on the site, so the main
 * story of the page was set like a card. `--h4` is the next step, and a
 * headline is what it is. Phones keep their own step (below).
 */
.ov-block--news .ov-news-lead__title {
	font-size: var(--h4);
}

.ov-news-lead__shot {
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
	aspect-ratio: 16 / 9;
	border-radius: var(--radius-l-2);
	background: repeating-linear-gradient(135deg, var(--color-surface-hover) 0 0.8rem, var(--color-surface-sunk) 0.8rem 1.6rem);
	margin-bottom: var(--space-m);
}

.ov-kicker-tag {	/*
	 * ⚠️ THE INK, NOT THE FILL. `--color-tier-mid` is oklch lightness 0.72 and
	 * white on it is 2.64:1, under the 4.5 floor (D117). Components used to
	 * override the token locally to a darker purple and this read as fine; with
	 * the scale unified (D124) the compensation went and the failure showed.
	 *
	 * `--color-tier-mid-ink` is the darker one the palette already carries for
	 * exactly this, so the tag says which it wants rather than the component
	 * redefining what mid means.
	 */

	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.11rem;
	text-transform: uppercase;
	color: var(--color-on-accent);
	background: var(--color-tier-mid-ink);
	border-radius: var(--radius-xs);
	padding: 0.2rem var(--space-2xs);
}


.ov-news-list {
	flex: 1 1 28rem;
	min-width: 0;
}

/*
 * The rest of an archive's feed, under the news block: the same item in two
 * columns. `auto-fill` with a floor rather than a fixed two, so a phone gets
 * one column without a breakpoint and the item never squeezes under its
 * thumbnail's width.
 */
.ov-news-grid {
	display: grid;
	/* 44rem: two columns in the 1280px frame, never three, and one on a phone. */
	grid-template-columns: repeat(auto-fill, minmax(min(100%, 44rem), 1fr));
	column-gap: var(--space-xl);
}

.ov-news-grid:empty {
	display: none;
}

.ov-news-item {
	display: flex;
	flex-direction: row;
	align-items: flex-start;
	gap: var(--space-m);
	padding: var(--space-m) 0;
	border-top: var(--border-hair) solid var(--color-border);
}

.ov-news-item__thumb {
	width: 6.4rem;
	height: 6.4rem;
	border-radius: var(--radius-m);
	background: repeating-linear-gradient(135deg, var(--color-surface-hover) 0 0.6rem, var(--color-surface-sunk) 0.6rem 1.2rem);
	flex-shrink: 0;
}

.ov-news-item__body {
	min-width: 0;
}

.ov-news-item__kicker {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.1rem;
	text-transform: uppercase;
	color: var(--color-success-ink);
}

/*
 * ⚠️ `.ov-news-item__time` WAS `.ov-note` UNDER A SECOND NAME, and the class
 * map already points `note`-named blocks at `.ov-note`. It said `--text-xs` in
 * `--color-text-muted` where `.ov-note` says `--text-xs` in
 * `--color-text-faint`, and those two TOKENS resolved to the same colour - so
 * the two classes were identical and `check-class-drift.js` said so the moment
 * a normalise pass made both of them render.
 *
 * ⚠️ THE TOKENS ARE THE REAL DUPLICATE. `--color-text-muted` and
 * `--color-text-faint` were one value under two names, which is the same shape
 * as `--color-accent-hover` and `--color-accent-ink` - and that pair cost every
 * button on this site its hover.
 *
 * ⚠️ RESOLVED SINCE: `--color-text-faint` is gone and its 22 uses are
 * `--color-text-muted`. It could not be made genuinely lighter - the step
 * below it on ACSS's neutral ladder is 3.4:1 on white and every one of those
 * uses is 12px - so a name promising a quieter grey than the palette can carry
 * was a promise to delete rather than to keep.
 */

/*
 * ⚠️ A HEADLINE, SO NOT THE SIZE OF ITS OWN BYLINE. This was `--text-xs`, the
 * step the time above it uses, so each story's title and its "2 weeks ago" read
 * as one grey-and-black paragraph beside a 64px thumbnail. `--text-xl` is the
 * step under the lead headline's `--h4`: the list reads as the lead's smaller
 * siblings rather than as captions.
 */
.ov-news-item__title {
	display: block;
	font-size: var(--text-xl);
	font-weight: 600;
	line-height: 1.3;
}

.ov-btn--poll {
	font-size: var(--text-m);
	font-weight: 700;
	border-radius: var(--radius-m);
	padding: var(--space-s) var(--space-l);
}

.ov-tile-row {
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	align-items: stretch;
	gap: var(--space-s);
}

.ov-accord-tile {
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: var(--space-s);
	border-radius: var(--radius-m);
	padding: var(--space-s) var(--space-m);
	border: var(--border-hair) solid var(--color-border);
	background: var(--color-surface);
	min-height: var(--tap-min);
	box-sizing: border-box;
}

.ov-accord-tile:hover {
	border-color: var(--color-border-strong);
	color: var(--color-text);
}

.ov-accord-tile__dot {
	width: var(--space-s);
	height: var(--space-s);
	border-radius: var(--radius-xs);
	background: var(--color-error);
	flex-shrink: 0;
}

.ov-accord-tile__name {
	font-size: var(--text-xs);
	font-weight: 600;
}

.ov-accord-tile__count {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	color: var(--color-text-muted);
}

.ov-duo {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(32rem, 1fr));
	gap: var(--space-l);
	align-items: stretch;
}

.ov-card-link {
	font-size: var(--text-xs);
	color: var(--color-text-muted);
	min-height: var(--tap-min);
	min-width: var(--tap-min);
	display: inline-flex;
	flex-direction: row;
	align-items: center;
}


.ov-contributor__rank {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	color: var(--color-text-muted);
	width: 1.8rem;
	margin: 0;
}

.ov-contributor__avatar {
	width: 3.2rem;
	height: 3.2rem;
	border-radius: var(--radius-full);
	background: var(--color-avatar-green);
	flex-shrink: 0;
}

.ov-contributor__body {
	min-width: 0;
	flex: 1;
}

.ov-contributor__points {
	font-size: var(--text-xs);
	font-weight: 700;
	color: var(--color-on-accent);
	background: var(--color-success-ink);
	border-radius: var(--radius-full);
	padding: var(--space-2xs) var(--space-s);
	flex-shrink: 0;
}

.ov-side-head-row {
	padding: var(--space-m);
	border-bottom: var(--border-hair) solid var(--color-border);
}

.ov-latest__added {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	color: var(--color-success-ink);
	flex-shrink: 0;
}

.ov-side-foot-link {
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
	text-align: center;
	padding: var(--space-s);
	min-height: var(--tap-min);
	border-top: var(--border-hair) solid var(--color-border);
	font-size: var(--text-xs);
	font-weight: 600;
	color: var(--color-text-muted);
	box-sizing: border-box;
}

.ov-side-foot-link:hover {
	background: var(--color-surface-sunk);
	color: var(--color-text-muted);
}

.ov-page-section {
	display: block;
	align-items: stretch;
	padding: 0;
}

.ov-count {
	font-size: var(--text-xs);
	font-weight: 700;
	color: var(--color-on-accent);
	background: var(--color-error-ink);
	border-radius: var(--radius-xl);
	padding: var(--space-3xs) var(--space-xs);
}

.ov-btn {
	display: inline-flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
	font-family: var(--font-sans);
	font-weight: 600;
	white-space: nowrap;
	min-height: var(--tap-min);
	color: var(--color-on-accent);
	background: var(--color-accent-ink);
	border-radius: var(--radius-m);
	cursor: pointer;
	box-sizing: border-box;
}

/*
 * ⚠️ `--color-accent-ink-hover`, BECAUSE THE BUTTON RESTS AT `--color-accent-ink`.
 *
 * This said `--color-accent-hover`, and `--color-accent-hover` and
 * `--color-accent-ink` are BOTH `--primary-hover`. So every filled button on
 * this site hovered to exactly the colour it already was: a rule that is
 * present, correct-looking and does nothing, on Post review, Submit your vote,
 * Start the conversation and the header CTA at once. It reads as "this button
 * has no hover", which is what it was reported as.
 *
 * `--color-accent-ink-hover` is `--primary-semi-dark`, one step down, and it is
 * what `.oview-btn--primary` in the control layer has always used. The pink
 * variant was right all along - `--color-error` resting, `--color-error-hover`
 * on - so only the accent pair was mismatched.
 */
.ov-btn:hover {
	background: var(--color-accent-ink-hover);
	color: var(--color-on-accent);
}

/*
 * ⚠️ AND EIGHT SCOPED RULES THAT RESTATED THE RESTING COLOUR ARE GONE.
 *
 * `.ov-poll .ov-btn`, `.ov-card--review .ov-btn` and six more each said
 * `background` set to the `--color-accent-strong` token, which WAS
 * `--color-accent-ink` - the value `.ov-btn` above already sets. So they added
 * nothing except a second class, which tied with `.ov-btn:hover` on
 * specificity and won on source order. Every one of them cancelled the hover
 * in its own corner of the site, which is why Post review still looked dead
 * after the token was corrected.
 *
 * A rule that only restates an inherited value is not neutral: it outranks
 * whatever depends on that value changing.
 */

.ov-btn--header {
	font-size: var(--text-l);
	padding: var(--space-s) var(--control-px);
}

.ov-divider-v {
	width: var(--border-hair);
	height: var(--control-h-s);
	background: var(--color-border);
}

.ov-page {
	/* The container `.ov-col-side` measures: see the sidebar's own rule. */
	container-type: inline-size;
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	gap: var(--space-3xl);
	align-items: flex-start;
	padding-block: var(--space-xxl) var(--space-4xl);
	/*
	 * ⚠️ THE SAME FRAME AS A SECTION, WHICH IS WHAT THE HEADER USES.
	 *
	 * This was `padding-inline: var(--space-3xl)` (40px) inside a 1280px cap,
	 * while `.ov-header`, `.ov-archive` and `.oview-footer` are all a section
	 * with `padding-inline: var(--gutter)` and a 1280px inner. At a 1280px
	 * viewport that put a fragrance page's content 40px from the edge under a
	 * nav bar starting at 75px - a 35px step between the header and everything
	 * below it, on every single page, and none of the archives.
	 *
	 * Adding the gutter to the cap rather than subtracting it from the content
	 * makes the two identical at every width: gutter-bound while the viewport is
	 * narrow, 1280px-bound once it is wide. Measured equal at 375, 1280 and 1920.
	 */
	padding-inline: var(--gutter);
	max-width: calc(var(--width-page) + 2 * var(--gutter));
	margin: 0 auto;
	box-sizing: border-box;
}

.ov-col-main {
	flex: 4 1 62rem;
	min-width: 0;
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: var(--space-3xl);
}

.ov-card {
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-2xl);
	padding: var(--space-xl);
	box-sizing: border-box;
	container-type: inline-size;
}

.ov-card--pad-s {
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-l-2);
	padding: var(--space-xl);
	box-sizing: border-box;
}

.ov-card--clip {
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-l-2);
	overflow: hidden;
	box-sizing: border-box;
	/*
	 * ⚠️ NO PADDING, WHICH IS THE WHOLE POINT OF CLIPPING.
	 *
	 * This card holds `ov-card-row` children, and every row carries its own
	 * inset and a divider along its top. Inheriting `.ov-card`'s 24px held
	 * each divider 24px short of the border at both ends, so a card whose
	 * reason to exist is rows running edge to edge drew them not doing it.
	 */
	padding: 0;
}

.ov-eyebrow {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.14rem;
	color: var(--color-text-muted);
	margin: 0 0 var(--space-m);
}

/*
 * ⚠️ ONE STEP UP, IN THE SCALE RATHER THAN PER BLOCK. A card heading at
 * `--text-l` was the same size as the body text under it, so "Leave a review"
 * and the sentence beneath it read as one paragraph. `--text-xl` gives the
 * heading its own weight in the page without a second class.
 *
 * ⚠️ AND EVERY CARD HEADING IS THIS ONE, WITH NO MARGIN OF ITS OWN (D187).
 * "Main accords" and "When to wear" were a step smaller than "Performance"
 * beside them, because each wore a modifier that restated the size a step
 * down and added a margin. The space under a heading is its card's gap now,
 * the way ACSS spaces anything, and the community reviews map their title
 * here too.
 */
/*
 * ⚠️ THE TAG DECIDES THE SIZE, AND THIS CLASS DECIDES NOTHING ABOUT IT.
 *
 * It was `--text-xl`, which drew a template's `<h2>` at the size of the
 * paragraphs under it. Then it was `--h2`, which was right about the scale and
 * still wrong about the mechanism: every card heading was the same size because
 * every card heading wore the same class, whatever level it actually was.
 *
 * So it sets no size at all. ACSS already emits `h1` to `h6` against `--h1` to
 * `--h6`, this class is one specificity point above nothing, and an element
 * that changes level changes size by doing so. Fixing an outline and fixing a
 * type scale become the same edit, which is the point.
 *
 * ⚠️ THE NAME NOW LIES. `.ov-heading` sits on an `h3`, an `h4` and an `h5`,
 * and it means "a card heading" rather than a level. Renaming it touches
 * `compare.css`, three declarations in `oview-site-one.php`,
 * `bricks-card-headings.php`, the parts list and the Bricks data, so it is a
 * deliberate rename rather than something to slip into a styling change.
 */
.ov-heading {
	font-weight: 700;
	margin: 0;
}

/*
 * ⚠️ `--quiet` IS THIS SITE'S WORD FOR "THE SAME THING, LESS LOUD",
 * AND IT ALREADY EXISTED. `.ov-btn--quiet` is a button that does not want the
 * eye. Rather than invent a second vocabulary, anything that needs to stand
 * down takes the same modifier, so the name means one thing everywhere and a
 * reader of the markup can guess what it does before reading the CSS.
 *
 * On a heading it means: keep the LEVEL, drop the VOICE. The element stays
 * where it belongs in the outline and is drawn on the interface scale instead
 * of the heading scale.
 *
 * ⚠️ THIS IS WHY THE OUTLINE AND THE TYPE SCALE STOPPED FIGHTING. A
 * fragrance page's readouts are sections of the page, so they are `h2`; they
 * are also small, quiet things beside a bottle, so they are `--quiet`. Trying
 * to express the second with the first is what produced an `h1` followed by an
 * `h5` and a failing accessibility gate.
 */
.ov-heading--quiet {
	font-size: var(--text-xl);
}

/*
 * A card's label, for a block that leads a card and is NOT a section of the
 * page. "Leave a review" and "Start a conversation" are invitations to use a
 * control, not places in the document, and an outline that lists them is an
 * outline nobody can skim.
 */
.ov-card-label {
	font-size: var(--text-xl);
	font-weight: 700;
	margin: 0;
}

.ov-card--vote-performance,
.ov-card--vote-occasion {
	gap: var(--space-xxl);
}

.ov-note {
	font-size: var(--text-xs);
	color: var(--color-text-muted);
	margin: 0;
}

.ov-accords {
	display: flex;
	flex-direction: column;
	gap: var(--space-m);
	container-type: inline-size;
}

.ov-chips {
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--space-xs);
}

/*
 * ⚠️ 36px COMPETED WITH THE PAGE'S OWN TITLE. `--text-title` is the
 * size a hero or an article title falls back to in a NARROW container, which is
 * not a statement about a poll: it was borrowed because the number looked right
 * once. On the heading scale a poll asks its question at the size of any other
 * card heading, which is what it is.
 */
.ov-poll__title {
	font-family: var(--font-serif);
	font-size: var(--h2);
	font-weight: 400;
	margin: 0 0 var(--space-2xs);
	letter-spacing: -0.04rem;
}

.ov-poll__byline {
	font-size: var(--text-l);
	color: var(--color-text-muted);
	margin: 0 0 var(--space-xl);
}

.ov-poll-option {
	text-align: left;
	font-family: var(--font-sans);
	cursor: pointer;
	background: var(--color-surface);
	border: var(--border-thick) solid var(--color-border);
	border-radius: var(--radius-l);
	padding: var(--space-l) var(--space-xl);
	display: block;
	width: 100%;
	box-sizing: border-box;
	box-shadow: var(--shadow-card);
}

.ov-poll-option:hover {
	border-color: var(--color-border-strong);
	color: var(--color-text);
}

.ov-poll-option__head {
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: space-between;
	gap: var(--space-m);
	margin-bottom: var(--space-m);
}

.ov-poll-option__label {
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: var(--space-m);
	font-size: var(--text-xl);
	font-weight: 600;
}

.ov-poll-option__radio {
	width: 1.7rem;
	height: 1.7rem;
	border-radius: var(--radius-full);
	border: var(--border-mark) solid var(--color-border-strong);
	background: var(--color-surface);
	display: inline-block;
	flex-shrink: 0;
}

.ov-poll-option__pct {
	font-size: var(--text-xl);
	font-weight: 800;
}

.ov-bar--poll {
	height: var(--space-s);
	border-radius: var(--radius-full);
	background: var(--color-track);
	overflow: hidden;
	display: block;
	margin-bottom: var(--space-s);
}

.ov-bar__fill--poll {
	display: block;
	height: 100%;
	border-radius: var(--radius-full);
	width: var(--sample-w-poll);
	background: var(--color-success);
	transition: width 0.4s ease;
}

.ov-poll-option__votes {
	font-size: var(--text-xs);
	color: var(--color-text-muted);
	display: block;
}

/*
 * ⚠️ `background: none` IS THE WHOLE FIX, AND IT WAS A CONTRAST FAILURE.
 *
 * This is a `<button>` styled as a link. Its border was reset and its background
 * was not, so it drew the browser's own `buttonface` grey - `rgb(240,240,240)`,
 * a colour nobody chose and no token names. The blue sat on it at 4.18:1 while
 * the same blue on the white behind it is 4.77:1, so the only reason "Copy link"
 * failed is a default nobody had noticed. It is the one control on the page
 * still wearing UA chrome.
 */
.ov-share__link {
	background: none;
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: var(--space-m);
	min-height: var(--tap-min);
	font-size: var(--text-l);
	font-weight: 600;
	color: var(--color-accent-ink);
}

.ov-card-icon--pink {
	background: var(--color-error-soft);
	color: var(--color-error-ink);
}

.ov-tag {
	font-family: var(--font-sans);
	font-size: var(--text-xs);
	font-weight: 600;
	cursor: pointer;
	min-height: var(--tap-min);
	padding: var(--space-xs) var(--space-m);
	border-radius: var(--radius-xl);
	border: var(--border-hair) solid var(--color-border);
	background: var(--color-surface);
	color: var(--color-text-body);
	display: inline-flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
	box-sizing: border-box;
}

.ov-tag:hover {
	border-color: var(--color-border-strong);
	color: var(--color-text);
}

.ov-thread {
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: var(--space-m);
	padding: var(--space-m) var(--space-m);
	border-radius: var(--radius-m-3);
	background: var(--color-surface-sunk);
}

.ov-thread:hover {
	background: var(--color-surface-sunk-2);
	color: var(--color-text);
}

.ov-thread__avatar {
	width: var(--control-h-s);
	height: var(--control-h-s);
	border-radius: var(--radius-full);
	background: var(--color-avatar-purple);
	flex-shrink: 0;
}

.ov-thread__body {
	min-width: 0;
	flex: 1;
}

.ov-thread__replies {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	font-weight: 500;
	color: var(--color-on-accent);
	background: var(--color-tier-mid-ink);
	border-radius: var(--radius-xl);
	padding: var(--space-3xs) var(--space-s);
	flex-shrink: 0;
}

.ov-side-head {
	padding: var(--space-l) var(--space-xl);
	border-bottom: var(--border-hair) solid var(--color-border);
	font-size: var(--text-m);
	font-weight: 700;
	margin: 0;
}

.ov-contrib {
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: var(--space-m);
	padding: var(--space-m) var(--space-xl);
	border-top: var(--border-hair) solid var(--color-hairline);
}

.ov-contrib:hover {
	background: var(--color-surface-sunk);
	color: var(--color-text);
}

.ov-contrib__icon {
	width: var(--space-2xl);
	height: var(--space-2xl);
	border-radius: var(--radius-m);
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
	font-size: var(--text-l);
	font-weight: 700;
	background: var(--color-accent-soft);
	color: var(--color-on-accent-soft);
	flex-shrink: 0;
}

.ov-contrib__body {
	min-width: 0;
}

.ov-similar__match {
	margin-top: var(--space-2xs);
}

/*
 * ⚠️ A DEFINITE WIDTH, BECAUSE THE ROW INSIDE IS A SIZE CONTAINER.
 *
 * `.ov-news-row` is `container-type: inline-size`, so it reports no width of
 * its own. In the news archive's column this block was then measured for
 * height as if it had none - every word on its own line - and kept that
 * height: 2,174px around 649px of stories on a phone, the rest blank. On the
 * homepage the column happened to hand it a width; `100%` says it in both.
 */
.ov-block--news {
	display: block;
	width: 100%;
}

.ov-block--news .ov-news-list {
	flex: 1 1 28rem;
	min-width: 0;
	display: flex;
	flex-direction: column;
	align-items: stretch;
}

.ov-teaser-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
	gap: var(--space-l);
	container-type: inline-size;
}

.ov-slider-wrap .ov-arrow:hover {
	background: var(--color-surface-sunk);
	color: var(--color-text-body);
}

.ov-slider-wrap {
	container-type: inline-size;
}

.ov-slider-wrap .ov-release-card:hover {
	border-color: var(--color-border-strong);
	color: var(--color-text);
}

.ov-slider-wrap .ov-release-foot {
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: space-between;
	gap: var(--space-xs);
}


/*
 * ⚠️ THE SHORTCODE IS ONE FLEX CHILD HOLDING TWO THINGS. The card spaces its
 * own children by `--space-l`, but the textarea and the button that submits it
 * are both inside the shortcode's wrapper, so the card's gap never fell
 * between them and the button sat hard against the box. `ov-push-top` could
 * not help either: `margin-top: auto` needs a column with room in it.
 */
.ov-card--review > .brxe-shortcode {
	display: flex;
	flex-direction: column;
	gap: var(--space-l);
	flex: 1;
}

.ov-card--review {
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: var(--space-l);
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-xl);
	padding: var(--space-xl);
	box-sizing: border-box;
	container-type: inline-size;
}

.ov-card--review .ov-tag:hover {
	border-color: var(--color-border-strong);
	color: var(--color-text);
}

.ov-col-side .ov-card--pad-s {
	border: 0.1rem solid var(--color-border);
	border-radius: 1.8rem;
	padding: var(--space-m);
	box-sizing: border-box;
}


.ov-col-side .ov-card--clip {
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-l-2);
	overflow: hidden;
	box-sizing: border-box;
}

.ov-col-side .ov-side-head-row {
	display: flex;
	flex-direction: row;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--space-s);
	padding: var(--space-m);
	border-bottom: var(--border-hair) solid var(--color-border);
}

.ov-col-side .ov-side-foot-link:hover {
	background: var(--color-surface-sunk);
	color: var(--color-text-muted);
}


/*
 * ⚠️ THE CAP IS FOR SIDE BY SIDE, AND IT WAS APPLIED STACKED TOO.
 *
 * `max-width: 32rem` keeps the sidebar a sidebar beside the main column. Below
 * the width where the two fit - main 62rem + sidebar 28rem + the 4rem gap, so
 * 94rem of `.ov-page` - the row wraps and the sidebar lands under the main
 * column, still capped: every card in it stopped 2rem short of every card
 * above it, on every phone, on the homepage and every fragrance page.
 *
 * A container query on the page states the real condition. A viewport
 * breakpoint would only approximate it, because the gutter between the
 * viewport and the page is fluid. `.ov-page` is the container (above).
 */
.ov-col-side {
	flex: 1 1 28rem;
	min-width: 0;
	position: sticky;
	top: var(--sticky-top);
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: var(--space-l);
	container-type: inline-size;
}

@container (min-width: 94rem) {
	.ov-col-side {
		max-width: 32rem;
	}
}

.ov-col-side .ov-card--pad-s {
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-l-2);
	padding: var(--space-xl);
	box-sizing: border-box;
}

.ov-col-side .ov-card--clip {
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-l-2);
	overflow: hidden;
	box-sizing: border-box;
}

.ov-col-side .ov-note--link {
	min-height: var(--tap-min);
	display: inline-flex;
	flex-direction: row;
	align-items: center;
}

.ov-col-side .ov-contrib:hover {
	background: var(--color-surface-sunk);
	color: var(--color-text);
}


.ov-col-side .ov-btn--no:hover {
	color: var(--color-text);
	background: var(--color-surface-hover);
}


.ov-stat-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
	gap: var(--space-m);
	container-type: inline-size;
}

/*
 * ⚠️ ONE QUIET MONO LABEL, NOT THREE.
 *
 * `.ov-eyebrow`, `.ov-eyebrow--tight` and this were the same idea at 0.14rem,
 * 0.13rem and 0.11rem of letter-spacing, in two different greys - close enough
 * that nobody would call any of them wrong and far enough that the account
 * page's tiles and the fragrance page's "MY COLLECTION" plainly did not match.
 * `check-class-drift.js` exists for exactly this and cannot see it: every value
 * came from a declared token.
 *
 * ⚠️ AND THE CASE IS A STYLE, not something typed into a template. The eyebrows
 * are written in capitals in their own build scripts; the tiles take their
 * words from `VoteSpec`, which cannot shout.
 */
.ov-stat-grid .ov-stat-label {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.13rem;
	text-transform: uppercase;
	color: var(--color-text-muted);
	margin: 0;
}


.ov-card--thread {
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: var(--space-l);
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-xl);
	padding: var(--space-xl);
	box-sizing: border-box;
	container-type: inline-size;
}

.ov-card--thread .ov-btn--pink:hover {
	background: var(--color-error-hover);
}

.ov-card--thread .ov-card-icon--pink {
	background: var(--color-error-soft);
	color: var(--color-error-ink);
}

.ov-card--thread .ov-tag:hover {
	border-color: var(--color-border-strong);
	color: var(--color-text);
}

.ov-card--thread .ov-thread:hover {
	background: var(--color-surface-sunk-2);
	color: var(--color-text);
}


/*
 * ⚠️ THE SCOPE WENT, like the collection button's. This was the same
 * declaration as `.ov-eyebrow--tight` below, keyed on the mockup's provenance
 * attribute, so the label above the collection buttons and the label above
 * anything else were two rules that had to be kept in step by hand.
 */

@container (max-width: 34rem) {
	.ov-accords .ov-accord-row {
		grid-template-columns: 8.6rem minmax(4rem, 1fr) 4.2rem;
		gap: var(--space-m);
	}
}

@container (max-width: 40rem) {
	.ov-head .ov-article-title {
		font-size: var(--text-title);
	}
}

@container (max-width: 32rem) {
	.ov-figure-inline {
		float: none;
		width: 100%;
		margin: 0 0 var(--space-m);
	}
}

@media (max-width: 47.9rem) {
	.ov-duo {
		grid-template-columns: 1fr;
	}

	.ov-news-lead__title {
		font-size: var(--text-l);
	}

	.ov-page {
		/*
		 * ⚠️ THE BLOCK AXIS ONLY. This was a `padding` shorthand, which also set
		 * the inline axis to `--space-l` (11.9px) and so re-created on phones the
		 * exact 4px step from the header that the gutter fixes everywhere else.
		 * The narrow end of `--gutter` is 16px and the header uses it, so naming
		 * the axis is the whole fix. Same shape as `.ov-archive`, which learned
		 * it the other way round: zeroing `padding-inline` there took the only
		 * gutter four archives had.
		 */
		padding-block: var(--space-xl) var(--space-xxl);
		gap: var(--space-xxl);
	}

	.ov-block--news .ov-news-lead__title {
		font-size: var(--text-xl);
	}
}

@container (max-width: 26rem) {
	.ov-contrib {
		padding: var(--space-m) var(--space-l);
	}

	.ov-col-side .ov-contrib {
		padding: var(--space-m) var(--space-l);
	}
}

/* ==========================================================================
 * RECOVERED FROM THE DELETED PARTS, 2026-08-30.
 *
 * ⚠️ THESE ARE ROLES THE CLASS MAP STILL OFFERS A DESIGNER, and adopting the
 * promoted parts dropped them.
 *
 * The adoption kept a rule if its selector matched something on eleven pages.
 * That is the right test for a rule and the wrong one for a ROLE: a pullquote,
 * a teaser and a bar chart are all things a drawing may ask for, and none of
 * them is on a page today. So they matched nothing, went, and
 * `check-class-drift.js` caught it - the map pointed at twelve classes with no
 * rule behind them.
 *
 * A role offered with no CSS is worse than no role. The designer draws it, it
 * imports, and it renders as an unstyled div.
 * ========================================================================== */

.ov-prose-lede::first-letter {
	font-family: var(--font-serif);
	font-size: 5.2rem;
	line-height: 0.8;
	float: left;
	padding: 0.6rem var(--space-s) 0 0;
	color: var(--color-text);
}

.ov-card--fill {
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: var(--space-l);
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-xl);
	padding: var(--space-xl);
	box-sizing: border-box;
	container-type: inline-size;
}

/* ==========================================================================
 * THE ENTITY PAGES, RECOVERED 2026-08-31.
 *
 * ⚠️ THE ADOPTION PROBED ELEVEN PAGES AND NOT ONE OF THEM WAS AN ENTITY
 * SINGLE PAGE.
 *
 * `/fragrances/<slug>/`, `/brands/<slug>/`, `/notes/<slug>/` and
 * `/accords/<slug>/` are four page TEMPLATES serving 142,000 URLs between them,
 * and the probe list had the four ARCHIVES instead. So every rule that only
 * appears on a single-entity page matched nothing on eleven pages and was
 * dropped as dead.
 *
 * 106 rules: the whole fragrance page. The two-column hero, the product shot
 * and its 32rem cap, the rating block, the accord bars, the notes pyramid, the
 * title and subtitle scale.
 *
 * ⚠️ IT SHOWED UP AS AN IMAGE 2.2x TOO BIG. `.ov-shot-frame` carried
 * `max-width: 32rem` and nothing else did, so a 375x500 bottle rendered at
 * 825x1100 and filled the column. Everything else that went with it was quieter
 * and would have sat there indefinitely.
 *
 * `snap-styles.js` did not catch it either, for the same reason: it was pointed
 * at the same eleven pages.
 * ========================================================================== */

.ov-accords .ov-count {
	background: var(--color-on-error-soft);
	font-size: 1.2rem;
}


.ov-poll {
	border-radius: var(--radius-2xl);
	background: var(--color-surface-sunk);
	border: var(--border-hair) solid var(--color-border-soft);
	padding: var(--space-xxl);
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	gap: var(--space-xxl);
	align-items: flex-start;
	box-sizing: border-box;
	container-type: inline-size;
}

.ov-poll__main {
	flex: 3 1 38rem;
	min-width: 0;
}

.ov-poll__side {
	flex: 1 1 25rem;
	max-width: 29.2rem;
	min-width: 0;
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: var(--space-l);
}

.ov-poll .ov-count {
	background: var(--color-on-error-soft);
	font-size: 1.2rem;
}

/*
 * ⚠️ A SECOND `.ov-row` LIVED HERE AND HAD NEVER APPLIED.
 *
 * It said `gap: var(--space-3xl)`, `flex-wrap: wrap` and `align-items:
 * flex-start` - 40px, which is what the hero was drawn against. The system's
 * own `.ov-row` is declared further down this file with `--space-xs` and
 * `align-items: center`, and being later it won every time. So the fragrance
 * hero laid its image frame 5px from the details column: the frame looked like
 * it was pressing into the accords, and nothing anywhere said why.
 *
 * The 40px version was not wrong, it was a different row - a two-column split
 * rather than an inline group - so it is a modifier now and the hero asks for
 * it by name. See `.ov-row--split` below.
 */

.ov-grid-2 {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(33rem, 1fr));
	gap: var(--space-xl);
	align-items: start;
}

.ov-grid-2--stretch {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(32rem, 1fr));
	gap: var(--space-xl);
	align-items: stretch;
}

.ov-group-baseline {
	display: flex;
	flex-direction: row;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--space-m);
}

.ov-group-between {
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: space-between;
	gap: var(--space-m);
}

.ov-push-top {
	margin-top: auto;
}

.ov-shot-frame {
	flex: 1 1 28rem;
	max-width: 32rem;
	min-width: 25rem;
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-xl);
	padding: var(--space-xl);
	box-sizing: border-box;
}

.ov-shot {
	aspect-ratio: 3 / 4;
	border-radius: var(--radius-m-2);
	background: var(--hatch-l);
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
}

.ov-shot__thumbs {
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: var(--space-s);
	margin-top: var(--space-l);
}

.ov-hero-col {
	flex: 3 1 40rem;
	min-width: 0;
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: var(--space-xl);
}

.ov-eyebrow--tight {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.13rem;
	color: var(--color-text-muted);
	margin: 0 0 var(--space-m);
}

.ov-title {
	font-family: var(--font-serif);
	font-size: var(--text-display);
	font-weight: 400;
	line-height: 0.94;
	margin: 0 0 var(--space-m);
	letter-spacing: -0.1rem;
}

.ov-subtitle {
	font-size: var(--text-l);
	color: var(--color-text-quiet);
	margin: 0;
}

.ov-rating {
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--space-xl);
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-l);
	padding: var(--space-l) var(--space-xl);
	container-type: inline-size;
}

.ov-score {
	display: flex;
	flex-direction: row;
	align-items: baseline;
	gap: var(--space-s);
}

.ov-score__value {
	font-size: var(--text-stat);
	font-weight: 800;
	letter-spacing: -0.1rem;
	margin: 0;
}

.ov-score__meta {
	font-size: var(--text-m);
	color: var(--color-text-muted);
	margin: 0;
}

.ov-rule-v {
	width: var(--border-hair);
	align-self: stretch;
	background: var(--color-border);
}

.ov-accord-row {
	display: grid;
	grid-template-columns: 10.8rem minmax(7rem, 1fr) 4.2rem;
	align-items: center;
	gap: var(--space-l);
}

.ov-accord-row__name {
	font-size: var(--text-l);
	font-weight: 600;
	margin: 0;
}

.ov-bar {
	/* A bar's thickness, not a space: see `.ov-brand__mark` in header.css. */
	height: 1.2rem;
	border-radius: var(--radius-full);
	background: var(--color-track);
	overflow: hidden;
	display: block;
}

.ov-bar__fill {
	display: block;
	height: 100%;
	border-radius: var(--radius-full);
	width: var(--sample-w-accord);
	background: var(--color-error);
}

.ov-pyramid {
	flex: 1 1 26rem;
	max-width: 32rem;
	min-width: 25rem;
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-l-2);
	overflow: hidden;
	box-sizing: border-box;
	container-type: inline-size;
}

.ov-pyramid__head {
	display: flex;
	flex-direction: row;
	/* ⚠️ `center`, NOT `baseline`, SINCE THE MARK ARRIVED. A picture has no
	 * baseline, so a baseline row hangs it off the bottom of the text. */
	align-items: center;
	/* ⚠️ AND NOT `space-between`, WHICH WAS RIGHT FOR TWO CHILDREN AND WRONG
	 * FOR THREE. With the title and the count it put one at each end; with the
	 * mark in front it spread all three, leaving the logo alone on the left
	 * with a gap the width of the card beside it. The count pushes itself to
	 * the end instead, so the mark and the title stay together however many
	 * things sit in this row. */
	justify-content: flex-start;
	gap: var(--space-s);
	padding: var(--space-l) var(--space-xl);
	border-bottom: var(--border-hair) solid var(--color-border);
}


.ov-pyramid__count {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	color: var(--color-text-dim);
	margin: 0;
	/* The one thing in the row that belongs at the far end. */
	margin-left: auto;
}

.ov-tier {
	padding: var(--space-l) var(--space-xl);
	border-top: var(--border-hair) solid var(--color-hairline);
}

.ov-tier__head {
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: var(--space-m);
	margin-bottom: var(--space-m);
}

.ov-tier__swatch {
	width: var(--space-m);
	height: var(--space-m);
	border-radius: var(--radius-xs);
	background: var(--color-tier-top);
}

/*
 * ⚠️ THE TIER NAMES SHOUT, like every other quiet mono label on this site.
 * "Top", "Heart" and "Base" arrived in sentence case beside "15 NOTES" in
 * capitals, in the same face at the same size, one line apart.
 */
.ov-tier__label {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.12rem;
	text-transform: uppercase;
	color: var(--color-text-muted);
	margin: 0;
}

.ov-tier__count {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	color: var(--color-text-dim);
	margin: 0;
}

.ov-note-chip {
	font-size: var(--text-m);
	font-weight: 500;
	padding: var(--space-xs) var(--space-m);
	border-radius: var(--radius-xl);
	border: var(--border-hair) solid var(--color-tier-top-line);
	background: var(--color-tier-top-tint);
	margin: 0;
}

/* --- a note that has a picture -------------------------------------------
 *
 * The pyramid drew each note as a tinted pill with the name inside it. Now
 * that the notes have art, the picture takes the tier's colour and the name
 * goes back to being plain text beside it.
 *
 * ⚠️ EVERY RULE HERE IS SCOPED TO THE PICTURE BEING THERE, and that is not
 * defensive styling. `.ov-note-chip` and `.ov-chips` are shared with the
 * COMPARISON card, which draws ten of the same chips and has no pictures: a
 * bare `.ov-note-chip { background: none }` would strip the pills off a page
 * nobody was looking at. `:has()` and the adjacent sibling say "only where a
 * picture actually sits", so the compare card keeps exactly what it had.
 */

.ov-note-icon {
	flex: 0 0 auto;
	/*
	 * ⚠️ 4.4rem IS 44px HERE. ACSS sets the root to 10px so rem maths is
	 * decimal, and a value written as 2.75rem expecting the browser default
	 * lands at 27px - which is what the first version of this rule did, and it
	 * looked deliberate rather than broken.
	 */
	inline-size: 4.4rem;
	block-size: 4.4rem;
	border-radius: var(--radius-m);
	/*
	 * ⚠️ THE COLOUR IS DELIBERATELY ABSENT. The tint and the line are written
	 * inline per tier by bricks-pyramid-notes.php, which reads them off the
	 * chip it replaces - three tiers, three colours, and a fourth would work
	 * without touching this file. Naming one here would win over none of them
	 * and mislead the next reader about where the colour comes from.
	 */
	border: var(--border-hair) solid transparent;
}

/*
 * The row: picture, then name, in the order the eye reads them.
 *
 * ⚠️ THIS OVERRIDES `display: contents` ON THE LOOP WRAPPER, which was right
 * when the wrapper held one chip and had to get out of the way of the flex
 * row. It holds two things now and has to be the row itself.
 */
.ov-chips > .brxe-block:has(> .ov-note-icon) {
	display: flex;
	/*
	 * ⚠️ THE DIRECTION IS NOT OPTIONAL. A Bricks `block` is a flex COLUMN by
	 * default, so `display: flex` alone changed nothing visible and the picture
	 * sat above its name, centred, looking like a deliberate card. Setting the
	 * direction is the whole rule; the display was already right.
	 */
	flex-direction: row;
	align-items: center;
	gap: var(--space-s);
	min-inline-size: 0;
}

/*
 * Two columns, by measure rather than by count.
 *
 * `auto-fit` with a floor means the tier lays out two across in the card it
 * lives in and one across when the card is narrow, without a breakpoint. A
 * hard `repeat(2, 1fr)` would put "Jasmine Sambac" and "Mandarin Orange" in a
 * 9rem column on a phone.
 */
.ov-chips:has(.ov-note-icon) {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
	gap: var(--space-s) var(--space-m);
	align-items: center;
}

/* The name stops being a pill the moment a picture sits beside it. */
.ov-note-icon + .ov-note-chip {
	padding: 0;
	border: 0;
	background: none;
	border-radius: 0;
	min-inline-size: 0;
}

.ov-body-copy {
	flex: 3 1 40rem;
	min-width: 0;
	font-size: var(--text-xl);
	line-height: 1.68;
	color: var(--color-text-body);
	margin: 0;
	text-wrap: pretty;
	max-width: 66ch;
}

.ov-btn--block {
	font-size: var(--text-l);
	font-weight: 700;
	border-radius: var(--radius-m-4);
	padding: var(--space-l);
	width: 100%;
}

.ov-btn--card {
	font-size: var(--text-l);
	font-weight: 700;
	border-radius: var(--radius-m-2);
	padding: var(--space-m) var(--space-xl);
}

/*
 * ⚠️ THE INK STEP, BECAUSE THIS BUTTON CARRIES WHITE TEXT.
 *
 * It rested on `--color-error` and hovered to `--color-error-hover`, which are
 * 2.70:1 and 3.39:1 under white - so "Start the conversation" failed the floor
 * in BOTH states. [D158] checked that the hover CHANGED and not that either end
 * was readable, which is how a colour pair passes a contrast fix and fails
 * contrast.
 *
 * Every other filled button on this site rests on an `-ink` step and hovers one
 * darker: `.ov-btn` is `--color-accent-ink` to `--color-accent-ink-hover`, 4.77
 * to 6.17. Pink was the only one reaching for the family's BASE, which is a fill
 * tuned for a tint rather than for text.
 */
.ov-btn--pink {
	background: var(--color-error-ink);
}

.ov-btn--pink:hover {
	background: var(--color-error-ink-hover);
}

.ov-total {
	background: var(--color-surface);
	border: var(--border-hair) solid var(--color-border-soft);
	border-radius: var(--radius-l);
	overflow: hidden;
}

.ov-total__pad {
	padding: var(--space-l) var(--space-xl);
}


.ov-total__value {
	font-size: var(--text-stat-l);
	font-weight: 800;
	letter-spacing: -0.14rem;
	margin: 0;
}

.ov-share {
	border-top: var(--border-hair) solid var(--color-hairline);
	padding: var(--space-l) var(--space-xl);
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: var(--space-m);
}

.ov-share__link--green {
	color: var(--color-success-text);
}

.ov-share__link--purple {
	color: var(--color-purple-text);
}

.ov-share__mark {
	/* ⚠️ A SIZE, NOT A SPACE. The old importer tokenised every length it met
	 * into the spacing scale, so this borrowed a spacing step and would have
	 * turned fluid with it. A mark's dimension has no scale to follow. */
	width: 2.2rem;
	height: 2.2rem;
	border-radius: var(--radius-s);
	background: var(--color-accent);
	flex-shrink: 0;
}

.ov-share__mark--green {
	background: var(--color-success);
}

.ov-share__mark--purple {
	background: var(--color-purple);
}

.ov-voters {
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: var(--space-s);
	padding-left: var(--space-2xs);
}

.ov-voters__face {
	width: var(--control-h-s);
	height: var(--control-h-s);
	border-radius: var(--radius-full);
	border: var(--border-mark) solid var(--color-surface-sunk);
	background: var(--color-avatar-pink);
	flex-shrink: 0;
	box-sizing: border-box;
}

.ov-voters__face--yellow {
	background: var(--color-avatar-yellow);
	margin-left: -1.4rem;
}

.ov-voters__face--blue {
	background: var(--color-avatar-blue);
	margin-left: -1.4rem;
}

.ov-voters__text {
	font-size: var(--text-xs);
	color: var(--color-text-muted);
	margin: 0 0 0 var(--space-2xs);
}

.ov-card-head {
	margin-bottom: var(--space-xs);
}

.ov-card-icon {
	width: var(--control-h-s);
	height: var(--control-h-s);
	border-radius: var(--radius-m);
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
	font-size: var(--text-l);
	font-weight: 700;
	background: var(--color-accent-soft);
	color: var(--color-on-accent-soft);
	flex-shrink: 0;
}

.ov-textarea {
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-m-4);
	padding: var(--space-l) var(--space-l);
	background: var(--color-surface-sunk);
	font-size: var(--text-l);
	color: var(--color-text-muted);
	line-height: 1.5;
	min-height: 7.6rem;
	margin: 0;
	box-sizing: border-box;
}

.ov-link-quiet {
	font-size: var(--text-xs);
	color: var(--color-text-muted);
	min-height: var(--tap-min);
	display: inline-flex;
	flex-direction: row;
	align-items: center;
}

.ov-btn--yes {
	flex: 1;
	font-size: var(--text-m);
	font-weight: 700;
	padding: var(--space-s);
	border-radius: var(--radius-m);
	background: var(--color-success);
}

.ov-btn--yes:hover {
	background: var(--color-success-hover);
}

.ov-btn--no {
	flex: 1;
	font-family: var(--font-sans);
	font-size: var(--text-m);
	font-weight: 700;
	min-height: var(--tap-min);
	padding: var(--space-s);
	border-radius: var(--radius-m);
	border: var(--border-hair) solid var(--color-trend-border-strong);
	cursor: pointer;
	background: var(--color-surface);
	color: var(--color-text);
	display: inline-flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
	box-sizing: border-box;
}

.ov-btn--no:hover {
	color: var(--color-text);
	background: var(--color-surface-hover);
}

.ov-pyramid .ov-count {
	background: var(--color-on-error-soft);
	font-size: 1.2rem;
}


.ov-card--review .ov-count {
	background: var(--color-on-error-soft);
	font-size: 1.2rem;
}

.ov-col-side .ov-group-baseline {
	display: flex;
	flex-direction: row;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--space-m);
}

.ov-col-side .ov-count {
	background: var(--color-on-error-soft);
	font-size: 1.2rem;
}

.ov-card--thread .ov-count {
	background: var(--color-on-error-soft);
	font-size: 1.2rem;
}

[data-ov-part="engine:vote/collection"] .ov-count {
	background: var(--color-on-error-soft);
	font-size: 1.2rem;
}

[data-ov-part="engine:vote/collection"] .ov-btn {
	background: var(--color-accent-ink);
}

.ov-card--vote-performance .ov-count {
	background: var(--color-on-error-soft);
	font-size: 1.2rem;
}


.ov-rating .ov-count {
	background: var(--color-on-error-soft);
	font-size: 1.2rem;
}


@media (max-width: 47.9rem) {
	.ov-poll {
		padding: var(--space-xl);
	}

	.ov-shot-frame {
		flex: 1 1 100%;
		max-width: 100%;
		min-width: 0;
	}

	.ov-pyramid {
		flex: 1 1 100%;
		max-width: 100%;
		min-width: 0;
	}

	.ov-grid-2 {
		grid-template-columns: 1fr;
	}

	.ov-grid-2--stretch {
		grid-template-columns: 1fr;
	}

	.ov-title {
		font-size: var(--text-title);
	}
}

@container (max-width: 34rem) {
	.ov-accord-row {
		grid-template-columns: 8.6rem minmax(4rem, 1fr) 4.2rem;
		gap: var(--space-m);
	}
}

/* ==========================================================================
 * THE LAYOUT AND TEXT PRIMITIVES.
 *
 * ⚠️ 125 OF 167 SITE CLASS FAMILIES HAD EXACTLY ONE MEMBER, and measured across
 * every rendered page they were not 125 different things: 37 flex classes were
 * 13 shapes, and 25 text classes were mostly two styles.
 *
 * Nine names meant "a column with a gap". Seven meant "a centred row". Fifteen
 * meant "small muted text". They differed only in the gap, and a gap is already
 * a token, so it belongs in a modifier rather than in the name of the thing.
 *
 * ⚠️ THE POINT IS THE NEXT DRAWING, NOT THE COUNT. A designer handed
 * `.ov-stack--m` uses it. A designer handed nine names for a column invents a
 * tenth, which is how this list grew in the first place.
 * ========================================================================== */

.ov-stack {
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: var(--space-m);
}

.ov-stack.ov-stack--flat { gap: 0; }
.ov-stack.ov-stack--s { gap: var(--space-xs); }
.ov-stack.ov-stack--l { gap: var(--space-l); }

/*
 * ⚠️ ONE STEP ABOVE `--l`, FOR SEPARATING SECTIONS RATHER THAN ROWS. The
 * account page stacked four headed sections at `--l`, the same gap it uses
 * between a heading and the list under it - so "Ratings you gave" sat as close
 * to the card above it as to its own. A heading belongs to what follows, and
 * the spacing has to say so. It is the next step on the same scale, not a new
 * number.
 */
.ov-stack.ov-stack--xl { gap: var(--space-2xl); }

.ov-row {
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: var(--space-xs);
}

/*
 * ⚠️ TWO CLASSES ON THE MODIFIERS, so a base declared later in the file cannot
 * beat them. `.ov-btn` against `.ov-btn--card` cost an afternoon on exactly
 * this (D124), and these are the same shape: one class each, order deciding.
 */
.ov-row.ov-row--s { gap: var(--space-2xs); }
.ov-row.ov-row--m { gap: var(--space-s); }
.ov-row.ov-row--l { gap: var(--space-m); }
.ov-row.ov-row--wrap { flex-wrap: wrap; }
.ov-row.ov-row--between { justify-content: space-between; }
.ov-row.ov-row--baseline { align-items: baseline; }
.ov-row.ov-row--center { justify-content: center; }

/*
 * ⚠️ A SPLIT IS NOT AN INLINE GROUP. Every other modifier here nudges the gap
 * between things sitting on one line; this is the two-column shape the hero
 * needs - a picture beside a column of facts, top-aligned, wrapping to one
 * column when there is no room. It is the rule the dead `.ov-row` above was
 * trying to be.
 */
.ov-row.ov-row--split,
/*
 * ⚠️ AND THE FRAGRANCE PAGE'S DESCRIPTION ROW IS ONE, WITHOUT SAYING SO.
 *
 * It holds the body copy beside the notes pyramid and came out of the design
 * import wearing a bare `.ov-row`, which centres. Two columns of very different
 * heights then centre against each other: with a long description the pyramid
 * floated in the middle of it, and with a short one the TEXT floated beside the
 * pyramid. Reported from a phone, where both columns are narrow and the effect
 * is at its worst.
 *
 * Matched by what it contains rather than given a class by a build script,
 * because that would be a twenty-eighth script to re-run after every design
 * import to restate something this file can simply see.
 *
 * `flex-wrap` earns its place here too: `.ov-body-copy` has a 40rem basis, so
 * below roughly 400px the row breaks to one column instead of squeezing the
 * description into a 181px gutter, which is what a 375px phone was getting.
 */
.ov-row:has(> .ov-pyramid) {
	gap: var(--space-3xl);
	align-items: flex-start;
	flex-wrap: wrap;
}

/* Small, quiet, and the thing fifteen classes were each saying alone. */
.ov-meta {
	font-size: var(--text-xs);
	color: var(--color-text-muted);
	margin: 0;
}

.ov-meta.ov-meta--mono {
	font-family: var(--font-mono);
	letter-spacing: 0.11rem;
}

/*
 * ⚠️ A META LINE WITH NOTHING TO SAY TAKES NO ROOM.
 *
 * Bricks drops a text element whose text is empty, but not one whose TAG
 * resolves empty: that renders as an empty tag, and in a flex row it still
 * holds a gap open. Hiding how many voted (D179) empties the vote line under
 * every card, so the empty tag goes rather than the row carrying a hole.
 */
.ov-meta:empty,
.ov-score__meta:empty,
.ov-caption:empty,
.ov-card--byline .ov-note:empty {
	display: none;
}

/*
 * ⚠️ AND A FIGURE WITH NO PICTURE IS NOT A FIGURE. A post with no featured
 * image (a scraped draft, for one) drops the `<img>`, but the frame around it
 * stays with only its empty caption inside, and keeps the space a picture
 * would take between the share bar and the text.
 */
.ov-figure:not(:has(img, picture, video, iframe)) {
	display: none;
}

/*
 * ⚠️ THE STAR IS THE SITE'S, SO AN UNRATED FRAGRANCE HAS NONE. It was typed into
 * the template before the number, and stayed alone when there was no number:
 * a rail of new releases, mostly unrated, printed a column of bare stars.
 */
.ov-star-score::before {
	content: "\2605\00a0";
}

.ov-star-score:empty {
	display: none;
}

/*
 * The heading on a card: "Notes pyramid", "Newest releases", "Your sign-in".
 *
 * ⚠️ NAMED FOR WHAT IT HEADS, NOT FOR HOW BIG IT IS. This was `ov-title-m`,
 * and the m stopped being true the moment the scale moved: it carried
 * `--text-m`, then `--text-l`, under a name still saying medium. A size in a
 * class name is a fact that goes stale silently, and this file already makes
 * the same argument about the stack gaps - "a gap is already a token, so it
 * belongs in a modifier rather than in the name of the thing".
 *
 * ⚠️ AND IT STEPPED UP TO `--text-l` at the same time. At `--text-m` a card
 * title was body size, which is why these read as labels rather than headings.
 * A block heading is `--text-xl`, a card heading `--text-l`: a step apart.
 */
.ov-card-title {
	font-size: var(--text-l);
	font-weight: 700;
	margin: 0;
}

/*
 * ⚠️ THE SPACE BETWEEN A COMPONENT'S CHILDREN BELONGS TO THE COMPONENT.
 *
 * Four classes existed to carry one margin each: `ov-stat-head`,
 * `ov-release-meta`, `ov-card-head-row` and `ov-kicker-row`. Every value was
 * already a token, so the scale was never the problem - the problem was that
 * changing the space under a card's first row meant finding which of four
 * names that card happened to use.
 *
 * ⚠️ AND A CLASS IS IN THE DATABASE WHILE A RULE IS IN A FILE. Most of this
 * site's markup came from a drawing months ago and nothing regenerates it, so
 * a name in the markup costs a rename script and a database write to change,
 * while a selector here costs an edit. That is the whole argument for putting
 * the spacing on the parent: the parent is already named, and the rule that
 * uses it is a file.
 *
 * `ov-kicker-row` survives because two of its five uses sit under a wrapper
 * with no class of its own, so there is no parent to name. It is one
 * declaration now rather than five, the other four being what `.ov-row`
 * already says.
 */
.ov-head > .ov-row,
.ov-news-lead > .ov-row,
.ov-news-item__body > .ov-row,
.ov-stat-card > .ov-row,
.ov-release-card > .ov-row--s {
	margin-block-end: var(--space-xs);
}

.ov-card > .ov-row--baseline {
	margin-block-end: var(--space-m);
}

/* ==========================================================================
 * THE ACCOUNT DASHBOARD, which is the platform's now (D176).
 *
 * `oview-bricks/assets/account.css` lays it out for every site, from the vote
 * cards, and nothing here builds or arranges it. What is here is what makes it
 * this site's: the serif section heads, the gold every score on this site is
 * drawn in, and the season tints on the tags.
 *
 * ⚠️ IT REPLACED TWENTY RULES the old page alone used (`ov-tabs`,
 * `ov-card__chips`, `ov-card-row__stars` and the rest), which nothing rendered
 * once it was rebuilt, and the meter layout, which is the platform's to draw.
 * ========================================================================== */

.oview-dashboard__title {
	font-family: var(--font-serif);
	font-weight: 400;
	letter-spacing: -0.04rem;
}

/*
 * ⚠️ THE SAME COLOUR AS A FILLED STAR, because it is the same fact. The stars
 * are one person's score and the bar is everyone's, so a reader should be able
 * to compare them without deciding whether two colours mean two things.
 */
.oview-dashboard__bar-fill {
	background: var(--color-tier-top);
}

/*
 * ⚠️ A TAG HERE IS A FACT, NOT A FILTER. `.ov-tag` is drawn to be clicked, with
 * a border and a pointer; on the dashboard it only says when you would wear it,
 * so the border goes and the pointer with it.
 */
.oview-dashboard__chips .ov-tag {
	border-color: transparent;
	font-weight: 600;
	cursor: default;
}

/*
 * ⚠️ THE SEASONS ARE THE COLOURS THE FRAGRANCE PAGE ALREADY USES. `votes.css`
 * gives every occasion bar a hue - winter the accent, spring green, summer the
 * top tier, autumn warning, day teal, night the mid tier - so a tag saying
 * Winter here is the same blue as the Winter bar there. Nothing new is chosen;
 * the tint is the same hue at the soft end.
 */
.oview-dashboard__chips .ov-tag[data-metric="winter"] { background: var(--color-accent-soft); color: var(--color-on-accent-soft); }
.oview-dashboard__chips .ov-tag[data-metric="spring"] { background: var(--color-success-soft); color: var(--color-success-ink); }
.oview-dashboard__chips .ov-tag[data-metric="summer"] { background: var(--color-tier-top-tint); color: var(--color-warning-ink); }
.oview-dashboard__chips .ov-tag[data-metric="autumn"] { background: var(--color-tier-base-tint); color: var(--color-warning-ink); }
.oview-dashboard__chips .ov-tag[data-metric="day"] { background: var(--color-teal-soft); color: var(--color-teal-ink); }
.oview-dashboard__chips .ov-tag[data-metric="night"] { background: var(--color-tier-mid-tint); color: var(--color-tier-mid); }

/*
 * ⚠️ MONO IS WIDER THAN SANS AT THE SAME SIZE, and `ov-meta--mono` adds
 * 0.11rem of tracking on top. A timestamp set that way next to a brand name at
 * the identical `--text-xs` reads as the LARGER of the two, and "45 minutes
 * ago" was wide enough to wrap onto its own line and become the widest thing
 * on the card.
 *
 * ⚠️ It cannot simply be made smaller: 12px is this site's legibility floor and
 * `check-mobile.js` fails anything under it. So the tracking goes instead,
 * which is what made it wide, and it stops wrapping.
 */
.oview-card__body .ov-meta--mono {
	letter-spacing: 0;
	white-space: nowrap;
}

/*
 * ⚠️ THE ASSEMBLED PAGES WERE NEVER GIVEN THIS SITE'S HEADING.
 *
 * A brand, a note and an accord page are built by the platform's scripts rather
 * than drawn, so their section heading carries only Bricks' own class and falls
 * back to the theme's h2. Measured on /brands/dolce-gabbana/: Plus Jakarta Sans
 * at 700, 36.23px - the SAME size as a drawn page's `.ov-sec-title`, and it
 * reads far heavier because that one is Instrument Serif at 400. Nothing was
 * wrong with the size, and chasing the size would have been the wrong fix.
 *
 * ⚠️ `h2.brxe-heading`, NOT A LOOSER SELECTOR. A facet label in the same frame
 * is an h2 too but carries `oview-facet__label` WITHOUT Bricks' class, and a
 * result card's title is an h3; widening this to `h2` would put a serif on both.
 */
.ov-page-frame h2.brxe-heading {
	font-family: var(--font-serif);
	font-weight: 400;
	letter-spacing: -0.04rem;
}

/*
 * And the result card's title beside this site's own: 15.9px against 14px, same
 * family and weight. Two pixels, and they are why a grid of results read
 * heavier than the rails on a fragrance page.
 *
 * ⚠️ THE TOKEN `.ov-card-title` USES, not the number it computes to, so the
 * two stay in step when the scale moves.
 */
.oview-card__title {
	font-size: var(--text-l);
}

/**
 * A poll option's colour, by its position.
 *
 * ⚠️ A FIXED PALETTE, NOT A RANDOM ONE. Random colours change between renders
 * unless they are stored, and a bar that is green on one visit and purple on
 * the next reads as a different answer. Five is enough for the ten options a
 * poll can have; after that it cycles, which is honest and stable.
 *
 * ⚠️ THE AVATAR TOKENS, because they are this site's decorative palette and are
 * already chosen to sit beside each other. A poll picking six new colours would
 * be six values nobody can restyle from one place.
 */
.ov-poll-option[data-ov-index] {
	--ov-poll-colour: var( --color-avatar-green );
}

.ov-poll-option[data-ov-index="2"] { --ov-poll-colour: var( --color-avatar-purple ); }
.ov-poll-option[data-ov-index="3"] { --ov-poll-colour: var( --color-avatar-yellow ); }
.ov-poll-option[data-ov-index="4"] { --ov-poll-colour: var( --color-avatar-blue ); }
.ov-poll-option[data-ov-index="5"] { --ov-poll-colour: var( --color-avatar-pink ); }
.ov-poll-option[data-ov-index="6"] { --ov-poll-colour: var( --color-avatar-green ); }
.ov-poll-option[data-ov-index="7"] { --ov-poll-colour: var( --color-avatar-purple ); }
.ov-poll-option[data-ov-index="8"] { --ov-poll-colour: var( --color-avatar-yellow ); }
.ov-poll-option[data-ov-index="9"] { --ov-poll-colour: var( --color-avatar-blue ); }
.ov-poll-option[data-ov-index="10"] { --ov-poll-colour: var( --color-avatar-pink ); }

.ov-poll-option .ov-bar__fill--poll {
	background: var( --ov-poll-colour );
}

/*
 * ⚠️ `is-chosen` IS NOT `is-mine`. Chosen is selected and not yet submitted;
 * mine is recorded. Both take the option's own colour, so the border answers
 * "which one is this" rather than only "something is selected".
 */
.ov-poll-option.is-chosen,
.ov-poll-option.is-mine {
	border-color: var( --ov-poll-colour );
	box-shadow: 0 0 0 1px var( --ov-poll-colour );
}

.ov-poll-option.is-chosen .ov-poll-option__radio,
.ov-poll-option.is-mine .ov-poll-option__radio {
	border-width: 0.32em;
	border-color: var( --ov-poll-colour );
}

/* ==========================================================================
 * COMMUNITY REVIEWS
 *
 * The platform renders the markup and its own `ov-reviews*` class names; this
 * says what they look like here. Every value below is a token this site
 * already declares - the section is the same card as "Leave a review", the
 * icon is the same `ov-card-icon` chip, the stars fill with the same
 * `--color-tier-top` as a vote card's, and the avatars are the five colours
 * the poll options already cycle through.
 *
 * ⚠️ NOTHING NEW WAS INVENTED FOR THIS BLOCK, and the first pass did invent
 * three things: `--color-star`, `--color-danger` and an `hsl()` built from the
 * account id. None of the three existed here, so all three fell through to a
 * hard-coded colour that no palette change could ever reach.
 * ========================================================================== */

.ov-reviews {
	display: flex;
	flex-direction: column;
	gap: var(--space-l);
	margin-block-start: var(--space-xl);
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-xl);
	padding: var(--space-xl);
	background: var(--color-surface);
	box-sizing: border-box;
	container-type: inline-size;
}

.ov-reviews__head {
	display: flex;
	flex-direction: column;
	gap: var(--space-2xs);
}

.ov-reviews__headrow {
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: var(--space-m);
}

/*
 * ⚠️ NO RULE HERE: THE SITE POINTS THE SLOT AT `.ov-card-icon` INSTEAD.
 *
 * This restated `.ov-card-icon` on the reasoning that a section should not
 * inherit a card component's styling. `check-class-drift.js` disagreed and was
 * right: two names for one appearance is two places to change a chip, and the
 * argument for keeping them apart was a guess about a change nobody has made.
 * `oview/reviews/classes` in oview-site-one.php maps it, and maps the title to
 * `.ov-heading` for the same reason (D187).
 */

.ov-reviews__count {
	font-size: var(--text-m);
	color: var(--color-text-muted);
	margin: 0;
}

.ov-reviews__list {
	display: flex;
	flex-direction: column;
	gap: var(--space-m);
	margin: 0;
	padding: 0;
	list-style: none;
}

.ov-review {
	border: var(--border-hair) solid var(--color-border);
	border-radius: var(--radius-l);
	padding: var(--space-l);
	box-sizing: border-box;
}

/* Your own, so it is findable in a long list. */
.ov-review--yours {
	border-color: var(--color-accent);
}

.ov-review__row {
	display: flex;
	flex-direction: row;
	align-items: flex-start;
	gap: var(--space-m);
	margin-block-end: var(--space-s);
}

/*
 * ⚠️ THE FIVE COLOURS THE POLL OPTIONS ALREADY USE, picked by an index the
 * platform derives from the account id. Same pattern as
 * `.ov-poll-option[data-ov-index]`, so a site changing its avatar palette
 * changes both in one place.
 */
.ov-review__avatar {
	width: var(--control-h);
	height: var(--control-h);
	border-radius: var(--radius-full);
	/* The colour comes from the index below, including index 1. Restating the
	   default here would be a rule that only repeats what it inherits (D158). */
	color: var(--color-text);
	font-family: var(--font-sans);
	font-size: var(--text-m);
	font-weight: 700;
}

/*
 * ⚠️ KEYED ON THE ATTRIBUTE, NOT ON THE CLASS, SO A SECOND AVATAR INHERITS IT.
 *
 * These were `.ov-review__avatar[data-ov-avatar="N"]`, which was right while the
 * review list was the only thing drawing a circle. The header draws one now, and
 * restating five colours under a second class is five more places for the
 * palette to drift - the exact shape `check-class-drift.js` exists to catch.
 *
 * The platform emits the index; this is the site saying what each one means. A
 * class still owns its own size and shape, which is the part that differs.
 */
[data-ov-avatar] { background: var(--color-avatar-green); }
[data-ov-avatar="2"] { background: var(--color-avatar-purple); }
[data-ov-avatar="3"] { background: var(--color-avatar-yellow); }
[data-ov-avatar="4"] { background: var(--color-avatar-blue); }
[data-ov-avatar="5"] { background: var(--color-avatar-pink); }

.ov-review__who {
	display: flex;
	flex-direction: column;
	gap: var(--space-3xs);
	min-width: 0;
}

.ov-review__author {
	font-family: var(--font-sans);
	font-size: var(--text-l);
	font-weight: 700;
	color: var(--color-text);
}

/* ⚠️ The timestamp slot is mapped to `.ov-note`, for the same reason as the
 * icon above. This is only the row it sits in. */
.ov-review__meta {
	display: flex;
	align-items: center;
	gap: var(--space-2xs);
}

/*
 * ⚠️ THE SAME FILL AS A VOTE CARD'S STARS, because it is the same fact drawn
 * smaller: one person's score. `--color-tier-top` is what
 * `.oview-vote__star[data-fill="on"]` carries, and `--color-star-glyph-off` is
 * what the site already calls an unfilled one.
 */
.ov-review__stars {
	color: var(--color-text-muted);
	letter-spacing: 0.05em;
}

.ov-review__star[data-ov-on] {
	color: var(--color-tier-top);
}

.ov-review__value {
	font-family: var(--font-mono);
	font-size: var(--text-m);
	font-weight: 700;
	color: var(--color-text);
}

.ov-review__body {
	font-family: var(--font-sans);
	font-size: var(--text-l);
	line-height: 1.55;
	color: var(--color-text-body);
}

/* ⚠️ The same pill as `.ov-tag`, because Edit and Remove are the same kind of
 * small quiet control the tag chips are. */
.ov-review__action {
	font-family: var(--font-sans);
	font-size: var(--text-xs);
	font-weight: 600;
	min-height: var(--tap-min);
	padding: var(--space-xs) var(--space-m);
	border-radius: var(--radius-xl);
	border: var(--border-hair) solid var(--color-border);
	background: var(--color-surface);
	color: var(--color-text-muted);
	cursor: pointer;
}

.ov-review__action:hover {
	border-color: var(--color-border-strong);
	color: var(--color-text);
}

.ov-reviews__more {
	font-family: var(--font-sans);
	font-size: var(--text-m);
	color: var(--color-text-muted);
	margin: 0;
}

/*
 * ⚠️ THE HEAD STACKS BEFORE THE PAGE DOES. A name, a date and five stars on
 * one line is fine in the main column and not on a phone, and this is a
 * container query because the section's width is the main column's, not the
 * viewport's.
 */
@container (max-width: 30rem) {
	.ov-review__score {
		margin-left: 0;
		flex-basis: 100%;
	}

	.ov-review__row {
		flex-wrap: wrap;
	}
}

/*
 * ⚠️ THE NAME NEEDED A FACE AS WELL AS A SIZE. Sizing it stopped an account
 * called "admin" arriving at hero scale; it still inherited the display serif
 * an `h1` wears here, which at 1.6rem renders a login as a squashed masthead.
 * A member's name is interface, not a headline.
 */
.oview-profile__name {
	font-family: var(--font-sans);
	font-weight: 700;
	letter-spacing: 0;
}

.oview-profile__name span {
	font-family: var(--font-mono);
}

/*
 * A mark beside a heading that names a section: the notes pyramid, when to
 * wear, performance.
 *
 * ⚠️ SIZED OFF THE TITLE IT SITS WITH, not off a fixed step. `--quiet` puts
 * that title on the interface scale, so a hard 2.4rem here would tower over it
 * at one breakpoint and vanish at another. `1em` is whatever the heading is.
 * ⚠️ AND IT INHERITS `color`, because every path in every icon file is
 * currentColor, so it follows the heading into dark mode with no second rule.
 * An icon added to `design/icons/` with a hard `#000000` is black in both.
 */
.ov-heading__mark {
	display: block;
	flex-shrink: 0;
	width: 1em;
	height: 1em;
}

/*
 * A row that has GAINED a mark needs the two corrections `.ov-pyramid__head`
 * already records: a picture has no baseline, so a baseline row hangs it off
 * the bottom of the words, and `space-between` is right for two children and
 * wrong for three, spreading them instead of keeping the mark against its
 * heading.
 *
 * ⚠️ KEYED ON THE MARK, so the correction arrives with it and no other row
 * using this utility is touched. `.ov-group-baseline` is site-wide.
 */
.ov-group-baseline:has(> .ov-heading__mark) {
	align-items: center;
	justify-content: flex-start;
}

/*
 * ⚠️ AND ONLY FROM THE THIRD CHILD. With a mark and a heading alone the last
 * child IS the heading, so an unconditional `auto` pushes the words to the far
 * end of the card and leaves the icon stranded.
 */
.ov-group-baseline:has(> .ov-heading__mark) > :last-child:nth-child(n + 3) {
	margin-left: auto;
}
