/*
 * Supplementary styles.
 *
 * assets/css/app.css is the source build's compiled stylesheet and already covers
 * every design component, because the templates reuse its class names. This file
 * only covers what exists because the site is now WordPress rather than React:
 * the dialog backdrops that used to be React portals, a few transparent layout
 * wrappers, and the admin bar. It uses the same custom properties so nothing drifts.
 *
 * If you find yourself adding a rule for something that looks like part of the
 * design, check the class name against the source component first — it is almost
 * always already styled in app.css under a different name.
 */

/* --------------------------------------------------------------------------
 * Reveal on scroll — the CSS half of the IntersectionObserver in app.js.
 * (The source used framer-motion, which is not shipped here.)
 * -------------------------------------------------------------------------- */

[data-reveal] {
	opacity: 0;
	transform: translateY(32px);
	transition: opacity 0.75s cubic-bezier(0.22, 1, 0.36, 1), transform 0.75s cubic-bezier(0.22, 1, 0.36, 1);
	will-change: opacity, transform;
}

[data-reveal].is-revealed {
	opacity: 1;
	transform: none;
}

@media (prefers-reduced-motion: reduce) {
	[data-reveal] {
		opacity: 1;
		transform: none;
		transition: none;
	}
}

/* --------------------------------------------------------------------------
 * Accessible colour
 *
 * The design's gold (#c69c6d) reads at 2.2:1 on paper, so gold *text* on a light
 * background failed WCAG AA. As a *background* it is fine — dark green on it is
 * 4.54:1 — so the token itself is left alone and only the text uses are corrected.
 * Changing the token broke every gold button, which is why this is scoped.
 *
 * Measured values, each the smallest shift that reaches the ratio while keeping hue:
 *
 *   gold display text on paper   #b58147  3.00:1  (>=24px needs 3:1)
 *   small gold text on paper     #8f6638  4.51:1
 *   bronze eyebrow on paper      #7c6b52  4.55:1
 *   bronze eyebrow on dark       #c9b28d  6.06:1
 *   muted body on paper          #6e6f66  4.50:1
 *   dimmed principle on dark     #a8b2a4  5.19:1
 * -------------------------------------------------------------------------- */

:root {
	/*
	 * The design applies gold emphasis with
	 * "color: var(--color-brand-gold) !important", so the token itself has to move.
	 * Backgrounds are restored to the original gold just below.
	 */
	--color-brand-gold: #b58147;
	--color-text-muted: #66675f;

	/*
	 * app.css declares "--gold: var(--color-brand-gold)" on :root, and a var() inside
	 * a custom property is substituted where that property is declared. Redefining
	 * --color-brand-gold further down therefore does nothing to --gold; both have to
	 * be set together at every scope. Same for --muted.
	 */
	--gold: #b58147;
	--muted: #66675f;
	--gold-background: #c69c6d;
	--gold-small: #8f6638;
	--bronze-on-light: #74644d;
	--bronze-on-dark: #c9b28d;
	--dim-on-dark: #a8b2a4;
}

/* Dark sections: the original gold already passes there, muted must lighten. */
.hero,
.why-chapter,
.contact-chapter,
.detail-hero,
.project-stage,
.about-cta-box,
/*
 * The fixed bars sit on a translucent dark green, so the effective background is
 * lighter than the solid colour and needs a lighter gold to clear 4.5:1.
 */
.mobile-actions,
.desktop-contact-actions {
	--color-brand-gold: #cda87e;
	--gold: #cda87e;
	--color-text-muted: #a8b2a4;
	--muted: #a8b2a4;
}

/*
 * .project-empty is a LIGHT surface (#eae8df), not a dark one — it was grouped with
 * the dark components above by mistake, which put #a8b2a4 text on it at 1.79:1. It
 * went unnoticed because the projects page keeps this block hidden until a filter
 * yields nothing, so axe never saw it; the search page's empty state exposed it.
 *
 * Measured against #eae8df, not against --paper, because it is a shade darker:
 * the page's muted #66675f is 4.66:1 there, and the gold needs a touch more depth
 * than the paper value to clear 3:1 on the 65px heading.
 */
.project-empty {
	--color-brand-gold: #ad7b44;   /* 3.00:1 on #eae8df */
	--gold: #ad7b44;
	--color-text-muted: #66675f;   /* 4.66:1 on #eae8df */
	--muted: #66675f;
}

/*
 * Gold as a surface, not as text: dark green on it is 4.54:1, so it stays put.
 * The design sets these with !important, so the token is redefined locally rather
 * than fighting the declaration.
 */
.nav-visit,
.hero-btn-primary,
.light-button,
.featured-btn-visit,
.solid-button:hover {
	--color-brand-gold: #c69c6d;
	--gold: #c69c6d;
	background-color: var(--gold-background);
	border-color: var(--gold-background);
}

/* The numeral before an eyebrow label is hardcoded in the design. */
.eyebrow > span {
	color: var(--bronze-on-light);
}

.hero .eyebrow > span,
.why-chapter .eyebrow > span,
.contact-chapter .eyebrow > span,
.detail-hero .eyebrow > span,
.project-empty .eyebrow > span,
.about-cta-box .eyebrow > span {
	color: var(--bronze-on-dark);
}

/* Small gold text on light backgrounds needs the stricter 4.5:1. */
.award-year-tag,
.award-category,
.leader-designation,
.related-loc,
.featured-location,
.benchmark-dist,
.pillar-num,
.cat-count,
.price-label,
.quote-attribution span {
	color: var(--gold-small);
}

/* The year pill sits on a slightly darker card, so it needs a touch more. */
.milestone-year-pill {
	color: #8b6337;
}

/* The quote counter used a flat grey at 3.13:1. */
.quote-counter,
.quote-navigation .micro {
	color: var(--color-text-muted);
}

/* Inactive timeline years sat at 2.59:1; 29px is large text, so 3:1 applies. */
.timeline-tabs button {
	color: #8e8b80;
}

.timeline-tabs button.active {
	color: var(--ink);
}

/* Secondary meta that sat just under the threshold. */
.page-breadcrumb a,
.insights-count-meta {
	color: var(--color-text-muted);
}

/* The footer micro labels sat at 4.48:1 on the dark green. */
.footer-details .micro,
.contact-chapter .micro {
	color: #9ea78f;
}

/* Inactive filter buttons sat at 3.01:1; #6e6f65 is 4.51:1. */
.project-filters button,
.insights-pill {
	color: #6e6f65;
}

/* --------------------------------------------------------------------------
 * Principles
 *
 * The design dims inactive items to 42% opacity. Text at that opacity cannot reach
 * 4.5:1 against the dark green at any colour, so the dimming becomes a colour
 * instead: inactive copy sits at 5.19:1 and the active item at 10.08:1, which keeps
 * the hierarchy visible and readable at the same time.
 * -------------------------------------------------------------------------- */

.principle {
	opacity: 1;
}

.principle:not(.active) h3,
.principle:not(.active) p,
.principle:not(.active) .micro,
.principle:not(.active) .principle-plus {
	color: var(--dim-on-dark);
	transition: color 0.8s;
}

.principle.active h3,
.principle.active .principle-plus {
	color: var(--paper);
}

/* The literal gold, not the light-background token: 4.54:1 on the dark green. */
.principle.active .micro {
	color: var(--gold-background);
}

/* --------------------------------------------------------------------------
 * Principles
 *
 * The design swapped one <img> and let its animation library crossfade. Every
 * image is rendered here instead, so the swap has no decode delay; these rules
 * are the crossfade the library used to provide (0.6s, from scale 1.05).
 * -------------------------------------------------------------------------- */

.principle-image img {
	opacity: 0;
	transform: scale(1.05);
	transition: opacity 0.6s ease, transform 0.6s ease;
}

.principle-image img.is-active {
	opacity: 1;
	transform: scale(1);
}

@media (prefers-reduced-motion: reduce) {
	.principle-image img {
		transition: none;
	}
}

/* --------------------------------------------------------------------------
 * Hero on short screens
 *
 * .hero-copy is anchored 29% from the top and .hero-base sits 98px from the bottom.
 * On a phone-height viewport the copy grows down into the base, so "SCROLL TO
 * DISCOVER" lands on top of the "Book a site visit" button. Measured on a 375x667
 * screen the two boxes had 2px between them. The source build has the same overlap.
 *
 * Anchoring the copy to the bottom instead lets it grow upward, so the clearance
 * holds whatever the copy length. Taller phones keep the original placement.
 * -------------------------------------------------------------------------- */

@media (max-width: 600px) and (max-height: 920px) {
	.hero-copy {
		/*
		 * 130px, down from 168.
		 *
		 * The 168 was clearance for the base row — the scroll cue and the project
		 * link. Both are hidden on a phone at the client's request, so `.hero-base`
		 * measures 0px tall there and the gap it was protecting no longer exists:
		 * measured at 390x844 the copy ended 168px up with the action bar starting at
		 * 65px, leaving 103px of nothing between them while the top of the hero was
		 * crowded. 130 puts the copy about 65px clear of the action bar — the
		 * "60 to 80px" the client asked for — and hands the difference back to the
		 * space above, which is what they wanted more of.
		 */
		bottom: 130px;
		top: auto;
	}

	/*
	 * The headline, sized so it stops breaking in the wrong place.
	 *
	 * app.css sets 15.2vw here, and at 375px that is 57px. Measured against the
	 * actual glyph widths, the longest line in the set — "A World Beyond" — needs
	 * 45px or less to stay on one line, so it wrapped to three lines where the design
	 * has two. 11.8vw clears every line of all five slides at 375, 390 and 430.
	 *
	 * The leading comes in to match: `.88` is what app.css uses on a wide screen, and
	 * the phone had been left looser at `.97`, which is what put the daylight between
	 * the two lines. Letter-spacing moves from a fixed -1.8px to -0.035em so it keeps
	 * the same proportion at the smaller size instead of growing tighter.
	 */
	.hero-heading {
		font-size: 11.8vw;
		letter-spacing: -0.035em;
		line-height: 0.88;
	}

	/*
	 * `.line-mask` adds 12px of padding and takes 5 back, so every heading line
	 * carried 7px of extra space that the line-height above cannot see. Net zero
	 * here, so the two lines sit as closely as the leading says they should.
	 */
	.hero-heading .line-mask {
		margin-bottom: -12px;
	}
	/*
	 * At 375x667 the hero has 412px between the header and the base row, and the
	 * readable type needs 429. The size stays at 16px — the space is bought back from
	 * leading and rhythm instead, which costs far less legibility than shrinking the
	 * text: the description's 1.8 line-height is luxurious for an eight-line block on
	 * a small phone.
	 */

	.hero-description {
		line-height: 1.5;
		margin-top: 12px;
	}

	.hero-eyebrow {
		margin-bottom: 10px;
	}

}

/* --------------------------------------------------------------------------
 * Masked headings
 *
 * .line-mask exists so a line can be clipped while it slides up on reveal. It
 * reserves a fixed 19px below the baseline for descenders, but the headings scale
 * with the viewport (clamp(80px, 8.9vw, 178px)), so past roughly 1700px the "g" in
 * "To belong." is cut off. The source build has the same flaw.
 *
 * This build fades sections in rather than sliding each line, so nothing needs
 * clipping here and the mask can simply let descenders through. Padding and margin
 * are untouched, so line spacing is exactly as designed.
 * -------------------------------------------------------------------------- */

.line-mask {
	overflow: visible;
}

/* --------------------------------------------------------------------------
 * Dialog backdrops. Radix rendered these into a portal with its own overlay;
 * here the dialog markup is inline, so it needs its own centring layer.
 * -------------------------------------------------------------------------- */

html.has-overlay-open,
html.has-overlay-open body {
	overflow: hidden;
}

.lead-dialog-backdrop,
.cookie-dialog-backdrop,
.gallery-lightbox {
	align-items: center;
	backdrop-filter: blur(3px);
	background: rgb(8 20 17 / 62%);
	display: flex;
	inset: 0;
	justify-content: center;
	padding: 18px;
	position: fixed;
	z-index: 9000;
}

.navigation-overlay {
	inset: 0;
	position: fixed;
	z-index: 9500;
}

[hidden] {
	display: none !important;
}

/* Close buttons: the source used Radix's built-in dismiss control. */
.lead-dialog-close,
.menu-close,
.gallery-close {
	background: none;
	border: 0;
	color: currentColor;
	cursor: pointer;
	padding: 6px;
	position: absolute;
	right: 20px;
	top: 20px;
	z-index: 2;
}

.gallery-close {
	color: var(--paper);
	position: fixed;
	right: 26px;
	top: 26px;
}

.lead-dialog {
	position: relative;
}

/* Transparent wrappers the server-rendered markup needs but the design does not style. */
.lead-questions,
.lead-visit-only {
	display: contents;
}

/* --------------------------------------------------------------------------
 * Testimonials
 *
 * The source mounted one quote at a time through an animation wrapper, so the
 * stylesheet never needed to hide the others. Here every quote is rendered for
 * crawlers and no-JS visitors, so only the active one is shown.
 * -------------------------------------------------------------------------- */

[data-testimonial] {
	display: none;
}

[data-testimonial].is-active {
	display: block;
}

/* --------------------------------------------------------------------------
 * Gallery
 *
 * The source used Embla for the horizontal track. Native scroll snapping gives
 * the same behaviour without the dependency, so only the scroll mechanics and the
 * lightbox backdrop live here; .gallery-slide and friends are styled by app.css.
 * -------------------------------------------------------------------------- */

.gallery-viewport {
	overflow-x: auto;
	scroll-snap-type: x mandatory;
	scrollbar-width: none;
	-webkit-overflow-scrolling: touch;
}

.gallery-viewport::-webkit-scrollbar {
	display: none;
}

.gallery-viewport.is-dragging {
	cursor: grabbing;
	scroll-snap-type: none;
}

.gallery-slide {
	scroll-snap-align: start;
}

/*
 * Upright galleries.
 *
 * The galleries preserved from the live site are portrait phone photographs, and
 * several are only 450–780px wide. The design's 16/9 slide crops them to a sliver
 * and scales them past their own pixels to fill it, which is what made them look
 * soft. These are the values app.css already uses for floor plans — the one other
 * place the design shows an upright image whole — applied to the same frame.
 */

.gallery-upright .gallery-slide {
	aspect-ratio: 0.8;
	background: #e8e7de;
	flex-basis: 45%;
}

.gallery-upright .gallery-slide img {
	object-fit: contain;
}

@media (max-width: 600px) {

	.gallery-upright .gallery-slide {
		aspect-ratio: 0.75;
		flex-basis: 88%;
	}
}

.gallery-dialog-backdrop {
	align-items: center;
	backdrop-filter: blur(3px);
	background: rgb(8 20 17 / 82%);
	display: flex;
	inset: 0;
	justify-content: center;
	position: fixed;
	z-index: 9200;
}

.lightbox-image img {
	max-height: 76vh;
	max-width: 92vw;
	object-fit: contain;
}

/* --------------------------------------------------------------------------
 * The About closing quote
 *
 * It now uses the same carousel as the home page's stories section, at the client's
 * request. .about-quote-container carried its own two-track grid in the design, so
 * it is reduced to spacing and the carousel provides the layout.
 * -------------------------------------------------------------------------- */

.about-quote-container {
	border-top: 1px solid var(--line);
	display: block;
	margin-top: 72px;
	padding-top: 56px;
}

/* --------------------------------------------------------------------------
 * Article layout
 *
 * The design has no working article page to copy — the reference build returns its
 * not-found view for /insights/<slug> — so these are the client's instructions:
 * a title that stays readable rather than display-sized, and body copy aligned to
 * the same gutter as the title and cover instead of floating in the middle.
 * -------------------------------------------------------------------------- */

.article-header h1 {
	font-size: clamp(32px, 3.4vw, 56px);
	letter-spacing: -1px;
	line-height: 1.1;
	margin: 18px 0;
	max-width: 20ch;
}

.article-body {
	margin: 0;
	max-width: none;
	padding: 70px 6.25% 40px;
}

/*
 * A readable measure, aligned left rather than centred. 880px is about 100
 * characters at the body size — wide enough to use the container properly without
 * the line length becoming tiring.
 */
.article-body > section,
.article-body > p,
.article-body > h2,
.article-body > h3,
.article-sections > section,
.article-sections > p,
.article-sections > h2,
.article-sections > h3 {
	margin-left: 0;
	margin-right: 0;
	max-width: 880px;
}

.article-body h2 {
	font-size: clamp(24px, 2.2vw, 34px);
	line-height: 1.2;
	margin: 46px 0 14px;
}

.article-body h3 {
	font-size: clamp(19px, 1.5vw, 23px);
	line-height: 1.3;
	margin: 30px 0 10px;
}

.article-body p {
	font-size: 16px;
	line-height: 1.85;
	max-width: 880px;
}

.article-summary {
	max-width: 62ch;
}

/*
 * The article ran ~300px of empty space between the last paragraph and the closing
 * actions, and another ~110px before the related band. Both are tightened here.
 */
.article-footer {
	padding-bottom: 40px;
	padding-top: 40px;
}

.article-page .journal-section {
	padding-top: 40px;
}

.article-body section:last-of-type {
	margin-bottom: 0;
}

@media (max-width: 600px) {
	.article-body {
		padding: 45px 7% 30px;
	}

	.article-footer {
		padding-bottom: 30px;
		padding-top: 30px;
	}
}

/* --------------------------------------------------------------------------
 * Cookie dialog
 *
 * The design renders this through a dialog component that supplies the width and
 * the dismiss control. Rendering it inline means both have to be declared here:
 * a reading-width panel, a close button, and the two choices stacked full width
 * rather than sitting side by side.
 * -------------------------------------------------------------------------- */

.cookie-dialog {
	max-height: calc(100vh - 48px);
	max-width: 512px;
	overflow-y: auto;
	position: relative;
	width: 100%;
}

.cookie-dialog h2 {
	font-family: var(--serif);
	font-size: 30px;
	letter-spacing: -0.6px;
	line-height: 1.1;
	margin: 0 0 14px;
	padding-right: 32px;
}

.cookie-dialog > p {
	color: var(--muted);
	font-family: var(--sans);
	font-size: 16px;
	line-height: 1.7;
	margin: 0 0 20px;
	max-width: 44ch;
}

.cookie-dialog-close {
	background: none;
	border: 0;
	color: var(--ink);
	cursor: pointer;
	padding: 6px;
	position: absolute;
	right: 16px;
	top: 16px;
}

.cookie-dialog .solid-button {
	justify-content: space-between;
	margin-top: 26px;
	width: 100%;
}

.cookie-dialog .arrow-link {
	border-bottom: 1px solid var(--line);
	display: flex;
	justify-content: space-between;
	margin-top: 4px;
	padding: 16px 0;
	width: 100%;
}

/* --------------------------------------------------------------------------
 * Contact page
 *
 * The source never shipped a contact page, so this is the one place with layout
 * of its own. It borrows the design's spacing, type and rules so it sits with the
 * rest of the site rather than looking bolted on.
 * -------------------------------------------------------------------------- */

.contact-layout {
	align-items: start;
	display: grid;
	gap: clamp(32px, 5vw, 90px);
	grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
	margin-top: clamp(40px, 5vw, 76px);
}

.contact-details-col {
	display: flex;
	flex-direction: column;
	gap: 34px;
}

.contact-detail-item {
	border-top: 1px solid var(--line);
	display: flex;
	flex-direction: column;
	gap: 9px;
	padding-top: 18px;
}

.contact-detail-item .micro {
	color: var(--muted);
	margin-bottom: 4px;
}

.contact-detail-item a,
.contact-detail-item span:not(.micro) {
	color: var(--ink);
	font-family: var(--sans);
	font-size: 16px;
	line-height: 1.8;
	text-decoration: none;
}

.contact-detail-item a:hover {
	opacity: 0.6;
}

.contact-detail-item .contact-actions {
	gap: 18px;
	margin-top: 4px;
}

.contact-form-col {
	border: 1px solid var(--line);
	padding: clamp(26px, 3vw, 46px);
}

.inline-form-title {
	font-family: var(--serif);
	font-size: clamp(26px, 2.8vw, 38px);
	letter-spacing: -0.8px;
	line-height: 1.08;
	margin: 0 0 12px;
}

.inline-form-title em {
	color: var(--gold);
	font-style: italic;
}

.inline-form .form-grid {
	display: grid;
	gap: 22px 20px;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	margin-top: 26px;
}

.inline-form .form-field.full-width {
	grid-column: 1 / -1;
}

.inline-form .form-bottom {
	display: flex;
	justify-content: flex-start;
	margin-top: 26px;
}

@media (max-width: 900px) {
	.contact-layout {
		grid-template-columns: minmax(0, 1fr);
	}

	.inline-form .form-grid {
		grid-template-columns: minmax(0, 1fr);
	}
}

/* --------------------------------------------------------------------------
 * WordPress-only bits
 * -------------------------------------------------------------------------- */

/*
 * Reading columns start at the page gutter, not centred: the heading above them sits
 * at the gutter, and a centred column left the two starting 400px apart. The measure
 * is what keeps the line length readable, so that stays.
 */
.reading-column {
	margin: 0;

	/*
	 * 84ch, not the 68ch this started at. A legal page is a heading 1400px wide above
	 * a 664px column, and the imbalance read as a narrow strip of text under a banner
	 * rather than as a document. 84ch is about 820px at the 16px reading size — still
	 * inside a comfortable measure for this face, and wide enough to look deliberate
	 * beside the heading it belongs to.
	 */
	max-width: 84ch;
}

.wide-column {
	margin: 0;
	max-width: 1100px;
}

.rich-text-section .reading-column p,
.rich-text-section .wide-column p {
	color: var(--muted);
	font-family: var(--sans);
	font-size: 16px;
	line-height: 1.8;
	margin-bottom: 18px;
}

.empty-state {
	color: var(--muted);
	font-family: var(--sans);
	font-size: 16px;
	padding: 48px 0;
	text-align: center;
}

.screen-reader-text {
	clip: rect(1px, 1px, 1px, 1px);
	height: 1px;
	overflow: hidden;
	position: absolute !important;
	width: 1px;
	word-wrap: normal !important;
}

.pagination {
	display: flex;
	gap: 14px;
	justify-content: center;
	margin-top: 56px;
}

.pagination .page-numbers {
	color: var(--ink);
	font-family: var(--sans);
	font-size: 16px;
	padding: 8px 12px;
	text-decoration: none;
}

.pagination .page-numbers.current {
	border-bottom: 1px solid var(--dark);
}

/* Admin bar clearance for logged-in editors. */
body.admin-bar .site-header {
	top: 32px;
}

@media screen and (max-width: 782px) {
	body.admin-bar .site-header {
		top: 46px;
	}
}

/* --------------------------------------------------------------------------
 * Insight card headings at any level
 *
 * The card's heading level is contextual: h3 under a section that owns the h2,
 * but h2 on the archive, index and search pages, where it follows the page's h1
 * directly and anything lower skips a level. app.css styles `.journal-card h3`
 * alone, so on those pages the title fell back to 14px Manrope and its trailing
 * arrow dropped to a line of its own.
 *
 * These are app.css's own values, at its own breakpoints, applied whatever the
 * level is — the h3 rules still come from app.css.
 * -------------------------------------------------------------------------- */

.journal-card h2,
.journal-card h4,
.journal-card h5,
.journal-card h6 {
	display: flex;
	font-family: var(--serif);
	font-size: 29px;
	gap: 20px;
	justify-content: space-between;
	line-height: 1.1;
}

.journal-card h2 svg,
.journal-card h4 svg,
.journal-card h5 svg,
.journal-card h6 svg {
	flex: none;
	margin-top: 6px;
	transition: transform 0.3s;
	width: 18px;
}

.journal-card:hover h2 svg,
.journal-card:hover h4 svg,
.journal-card:hover h5 svg,
.journal-card:hover h6 svg {
	transform: translate(3px, -3px);
}

@media (max-width: 1200px) {

	.journal-card h2,
	.journal-card h4,
	.journal-card h5,
	.journal-card h6 {
		font-size: 26px;
	}
}

@media (max-width: 900px) {

	.journal-card h2,
	.journal-card h4,
	.journal-card h5,
	.journal-card h6 {
		font-size: 22px;
	}
}

@media (max-width: 600px) {

	.journal-card h2,
	.journal-card h4,
	.journal-card h5,
	.journal-card h6 {
		font-size: 29px;
	}

	.journal-card h2 svg,
	.journal-card h4 svg,
	.journal-card h5 svg,
	.journal-card h6 svg {
		margin-top: 4px;
		width: 21px;
	}
}

/* --------------------------------------------------------------------------
 * The project stage name at any heading level
 *
 * Same story as the insight card above: the project explorer promotes the active
 * project's name to an h2 when it owns the page's h1 (the /projects/ archive) and
 * leaves it an h3 under a section heading. app.css styles `.project-stage-info h3`
 * alone, so on /projects/ the name rendered at 14px Manrope over the photograph.
 *
 * app.css's own values, at its own breakpoints.
 * -------------------------------------------------------------------------- */

.project-stage-info h2 {
	font-family: var(--serif);
	font-size: clamp(42px, 5.8vw, 105px);
	line-height: 1.03;
	margin-top: 14px;
	max-width: 900px;
}

@media (max-width: 1200px) {

	.project-stage-info h2 {
		font-size: 6vw;
	}
}

@media (max-width: 600px) {

	.project-stage-info h2 {
		font-size: 49px;
		letter-spacing: -1px;
		line-height: 0.98;
		margin-top: 15px;
		max-width: 300px;
	}
}


/* --------------------------------------------------------------------------
 * Legal documents
 *
 * The design's privacy page is all paragraphs, so app.css styles `.privacy-copy p`
 * and nothing else — and its preflight resets `ul` to `list-style: none; padding: 0`.
 * The live policies this site carries over are mostly bulleted, so the items lost
 * their markers and ran together as plain lines.
 *
 * The values follow the design's own list treatment (`.article-part-list li`) and
 * its `.privacy-copy p` colour, so a list reads as part of the same column.
 * -------------------------------------------------------------------------- */

.privacy-copy ul,
.privacy-copy ol {
	color: var(--muted);
	margin: 0 0 18px;
	padding-left: 20px;
}

.privacy-copy ul {
	list-style: disc;
}

.privacy-copy ol {
	list-style: decimal;
}

.privacy-copy li {
	font-size: 16px;
	line-height: 1.9;
	margin-bottom: 10px;
	padding-left: 4px;
}

.privacy-copy li::marker {
	color: var(--color-brand-gold);
}

.privacy-copy li strong {
	color: var(--ink);
	font-weight: 600;
}

/* The lead sits above the first heading, so it carries the gap instead of it. */
.privacy-copy .privacy-lead {
	margin-bottom: 10px;
}

.privacy-copy p {
	margin-bottom: 18px;
}

.privacy-copy > :last-child {
	margin-bottom: 0;
}

.privacy-footer {
	border-top: 1px solid var(--line);
	margin-top: 40px;
	padding-top: 28px;
}

/* --------------------------------------------------------------------------
 * Search results
 *
 * The design has no site-search page — search on the insights page filters cards in
 * place. These are the few rules that exist because WordPress has a real results
 * page: the spacing between one group of results and the next, and the heading that
 * names each group. Everything inside a group is the design's own component.
 * -------------------------------------------------------------------------- */

.search-group {
	margin-top: 56px;
}

.search-group > .eyebrow {
	margin-bottom: 24px;
}

.search-rows .directory-row:first-child {
	border-top: 1px solid var(--line);
}

/* The empty state borrows .project-empty, whose heading the design draws as an h3. */
.project-empty h2 {
	font-family: var(--serif);
	font-size: 65px;
	line-height: 1.05;
	margin: 22px 0;
}

@media (max-width: 600px) {

	.search-group {
		margin-top: 40px;
	}

	.project-empty h2 {
		font-size: 51px;
	}
}

/* --------------------------------------------------------------------------
 * 404
 *
 * The design's not-found page is an eyebrow, a heading, a line of copy and one
 * link, which is all a prototype needs. A real site's 404 also has to help someone
 * who mistyped a URL get somewhere, so it carries the search box and the primary
 * menu's destinations — both existing components; these are only their spacing.
 * -------------------------------------------------------------------------- */

.not-found .insights-search-box {
	margin-bottom: 36px;
	max-width: 480px;
}

.not-found-links {
	border-top: 1px solid var(--line);
	margin-bottom: 40px;
	max-width: 620px;
}

.not-found-links .directory-row {
	padding: 18px 0;
}

.not-found-links .directory-row svg {
	margin-left: auto;
}

@media (max-width: 600px) {

	.not-found-links .directory-row svg {
		margin-left: 0;
	}
}

/*
 * The shortest phones still in use (360x640) have 40px less height again, which the
 * rhythm above does not quite recover. The heading gives it up rather than the copy:
 * it is display type at 57px, so a tighter leading costs nothing legible, whereas
 * the body text is the thing this whole layer exists to keep at 16px.
 */
@media (max-width: 600px) and (max-height: 660px) {

	.hero-heading {
		line-height: 0.9;
	}
}

/* --------------------------------------------------------------------------
 * Award photographs
 *
 * The design's award card is text only — the ceremony photographs live on the live
 * site's /awards-achievements/ page, and the client asked for them here. There is no
 * class for them in app.css, so this is the frame: the card's own paper and border
 * are already set by the design, and the image sits inside it at a fixed ratio so a
 * portrait and a landscape frame line up across the grid.
 * -------------------------------------------------------------------------- */

.award-media {
	aspect-ratio: 16 / 10;
	background: #e2e2d8;
	margin-bottom: 26px;
	overflow: hidden;
}

.award-media img {
	display: block;
	height: 100%;
	object-fit: cover;
	object-position: center;
	transition: transform 0.9s;
	width: 100%;
}

.award-card:hover .award-media img {
	transform: scale(1.04);
}

/*
 * A card with no photograph (a registration rather than a ceremony) keeps its top
 * edge level with the others rather than floating its header upward.
 */
.awards-grid .award-card:not(.has-photo) .award-header {
	margin-top: 4px;
}

@media (max-width: 600px) {

	.award-media {
		margin-bottom: 20px;
	}
}

/* --------------------------------------------------------------------------
 * About — "who we are" sub copy
 *
 * The design sets the lead paragraph to --ink and the paragraphs under it to #555,
 * so the block read as two different greys. At the client's request all of it is
 * --ink (#292b26), which is also the stronger contrast: 12.67:1 on paper against
 * the 6.60:1 #555 gave.
 *
 * Line height and rhythm stay as the design set them (1.85 / 16px bottom margin) —
 * only the colour changes, so nothing reflows.
 * -------------------------------------------------------------------------- */

.about-who-copy p:not(.eyebrow) {
	color: #292b26;
}

/*
 * The eyebrow in this block is a <p>, so `.about-who-copy p` outranks `.eyebrow` on
 * specificity — it was taking the paragraph colour, and the readable type layer was
 * promoting it to 16px while every other eyebrow on the site sits at 12px. It is a
 * label, so it keeps the label treatment.
 */
.about-who-copy p.eyebrow {
	color: var(--muted);
	font-size: 12px;
}

/* --------------------------------------------------------------------------
 * Legal pages start at the gutter
 *
 * app.css centres the reading column (`.privacy-copy { margin: 60px auto 0 }`), so
 * the h1 sat at the page gutter while the text it introduces began 335px further in.
 * At the client's request the column is aligned left with the heading. The 730px
 * measure stays — that is what keeps the line length readable.
 * -------------------------------------------------------------------------- */

.privacy-copy {
	margin-left: 0;
	margin-right: 0;
}

/* --------------------------------------------------------------------------
 * Hero edition note vs the signage in the photograph
 *
 * `.hero-edition` is the bottom-right item of the hero base row, and its second line
 * ("ARCHITECTURAL INSPIRATION · REPRESENTATIVE IMAGERY") runs 348px leftward — far
 * enough to cross the Divine Group sign in the hero photograph and sit on top of the
 * brand name. The first line clears it; only the note does not.
 *
 * Constraining the block to the clear strip right of the sign makes the note wrap
 * within it instead of reaching across, and lifting it clears the taller wrapped
 * block off the bottom edge. Right-aligned, so it stays anchored to the gutter.
 * -------------------------------------------------------------------------- */

@media (min-width: 901px) {

	/*
	 * The row aligns on its first lines, not its middles.
	 *
	 * app.css centres the three items, which is right when they are all one line — and
	 * the design's are: its note is 8px with a 5.5px second line and comes to 197px
	 * wide. Ours is 344px, because the readable-type layer floors that label tier at
	 * 11px, so it wraps to four lines inside the strip and centring pushed it 150px
	 * above the other two. The client saw that as a misalignment, and it was one.
	 *
	 * `flex-start` puts "PLACES MADE FOR LIFE" level with the scroll cue and the
	 * project link — the same arrangement the design has — and lets the wrapped note
	 * hang below them rather than lifting the whole block.
	 */
	.hero-base {
		align-items: flex-start;
	}

	.hero-edition {
		/*
		 * A percentage, not a pixel width: the photograph is object-fit:cover, so the
		 * sign scales with the viewport and the clear strip beside it stays roughly a
		 * constant fraction of the width (its right edge sits at ~85% at every desktop
		 * size). A fixed width cleared at 1600 and collided again at 1280.
		 */
		max-width: 11%;
	}
}

/* --------------------------------------------------------------------------
 * The regional map
 *
 * The design drew an abstract "editorial guide" — five grey bars on a graph-paper
 * background — which the client called a placeholder, correctly. It is a real Google
 * Maps embed now, at the client's request, so that anyone reading this section can
 * get directions; it replaced a Leaflet map over OpenStreetMap tiles.
 *
 * `.location-visual` keeps the design's frame (its aspect ratio and paper). These
 * rules fill it with the map and put the place list beside it. There is no map chrome
 * left to restyle — the embed renders inside an iframe, so Google's own controls
 * cannot be reached from here and are not ours to restyle anyway.
 * -------------------------------------------------------------------------- */

.location-visual {
	background-image: none;
	display: grid;
	gap: 0;
	grid-template-columns: 1fr 300px;
	overflow: hidden;
	position: relative;
}

.location-map {
	background: #e5e5da;
	height: 100%;
	min-height: 320px;
	width: 100%;
	z-index: 0;
}

/*
 * The place list: the keyboard and no-JavaScript route to the same addresses.
 *
 * Typography comes from the design's own location-card classes, which the markup
 * uses — .location-region, .location-title, .badge-count, .location-explore-link —
 * so there is nothing to restate here. These rules are only the panel: its frame,
 * and sharing the column height evenly between the places so the last one does not
 * leave a bordered gap under it.
 */
.location-map-list {
	background: var(--paper);
	border-left: 1px solid var(--line);
	display: flex;
	flex-direction: column;
	list-style: none;
	margin: 0;
	overflow-y: auto;
	padding: 0;
}

.location-map-item {
	border-bottom: 1px solid var(--line);
	display: flex;
	flex: 1 1 0;
	flex-direction: column;
	justify-content: center;
	min-height: 0;
	padding: 20px 26px;
}

.location-map-item:last-child {
	border-bottom: 0;
}

.location-map-choice {
	background: none;
	border: 0;
	cursor: pointer;
	display: block;
	padding: 0;
	text-align: left;
	width: 100%;
}

.location-map-choice .location-title {
	display: block;
	transition: color 0.3s ease;
}

.location-map-choice .badge-count {
	display: block;
}

.location-map-choice:hover .location-title,
.location-map-choice:focus-visible .location-title {
	color: var(--color-brand-gold);
}

.location-map-item .location-explore-link {
	margin-top: 12px;
}

/* The embed fills the frame. It is the only child, so there is nothing to stack. */
.location-map-frame {
	border: 0;
	display: block;
	height: 100%;
	width: 100%;
}

/*
 * The chosen place. A keyless Google embed shows one place at a time, so the list is
 * the control and has to say which one the map is on — the design has no "selected"
 * treatment for a location because its map had none, so this borrows the gold it uses
 * for an active filter, plus a rule in the gutter to mark the row itself.
 */
.location-map-choice.is-active .location-title {
	color: var(--color-brand-gold);
}

.location-map-item:has( .location-map-choice.is-active ) {
	background: rgb(198 156 109 / 8%);
	box-shadow: inset 3px 0 0 var(--color-brand-gold);
}

@media (max-width: 900px) {

	/*
	 * Stacked, and no longer a fixed-height frame: sharing the height evenly between
	 * the places is right in the desktop column, but below the map there is no height
	 * to share — it squeezed each entry to 41px and they overlapped. Here the list
	 * sizes to its content and the section grows to fit.
	 */
	.location-visual {
		aspect-ratio: auto;
		grid-template-columns: 1fr;
		height: auto;
		min-height: 0;
	}

	.location-map {
		min-height: 340px;
	}

	.location-map-list {
		border-left: 0;
		border-top: 1px solid var(--line);
		overflow-y: visible;
	}

	.location-map-item {
		flex: 0 0 auto;
		padding: 22px 26px;
	}
}

/*
 * The map caption used to sit on a flat drawing; over real tiles it needs its own
 * ground to stay readable. Bottom-left keeps it clear of the embed's own controls
 * (the zoom cluster bottom-right, the Google mark bottom-left of the tiles).
 *
 * `z-index: 1`, not 500. The 500 was for Leaflet, whose own panes climb to 400–700;
 * the map is an iframe now, which an absolutely-positioned element already stacks
 * above. 500 also put the caption above the **site header**, which is z-index 35 and
 * fixed at 68px tall on a phone — so scrolling the section up painted the caption
 * straight over the brand and the menu button.
 */
.location-visual .location-visual-note {
	background: rgb(243 241 235 / 88%);
	bottom: 14px;
	color: var(--muted);
	font-size: 12px;
	left: 14px;
	letter-spacing: 1.5px;
	padding: 6px 12px;
	z-index: 1;
}

/*
 * Stacked layout: the caption stops floating and becomes a caption. Over the map it
 * covered tiles on a panel only ~270px tall, and pinned to the top it met the sticky
 * header. In flow it is the last child, so it reads as a line under the whole thing.
 */
@media (max-width: 900px) {

	.location-visual .location-visual-note {
		background: none;
		display: block;
		padding: 14px 0 0;
		position: static;
	}
}

/* --------------------------------------------------------------------------
 * Rich text inside an article
 *
 * The article body used to be escaped plain text, so it could only ever be
 * paragraphs. It is a rich-text field now, which means lists, links and quotes can
 * appear — and app.css's preflight strips list markers, exactly as it did on the
 * legal pages. These give them the design's own treatment.
 * -------------------------------------------------------------------------- */

.article-part-paragraphs ul,
.article-part-paragraphs ol {
	color: var(--muted);
	margin: 0 0 18px;
	padding-left: 22px;
}

.article-part-paragraphs ul {
	list-style: disc;
}

.article-part-paragraphs ol {
	list-style: decimal;
}

.article-part-paragraphs li {
	font-size: 16px;
	line-height: 1.8;
	margin-bottom: 8px;
}

.article-part-paragraphs li::marker {
	color: var(--color-brand-gold);
}

.article-part-paragraphs a {
	color: var(--ink);
	text-decoration: underline;
	text-underline-offset: 4px;
}

.article-part-paragraphs a:hover {
	color: var(--color-brand-gold);
}

.article-part-paragraphs strong {
	color: var(--ink);
	font-weight: 600;
}

.article-part-paragraphs blockquote {
	border-left: 2px solid var(--color-brand-gold);
	color: var(--ink);
	font-family: var(--serif);
	font-size: 24px;
	line-height: 1.4;
	margin: 26px 0;
	padding: 4px 0 4px 22px;
}

/* --------------------------------------------------------------------------
 * Chapter dots: the hit area is the dot, not the hidden label
 *
 * Each dot's link contains a 3px dot and a hover label. The label is only faded
 * out — it stays in normal flow, so the link sized to it: a 169px-wide invisible
 * strip, fixed down the right edge of every page at z-index 30, swallowing clicks
 * on whatever sat under it. The footer's back-to-top arrow was one casualty — a
 * click on it hit `#insights` instead and jumped to the wrong section.
 *
 * Taking the label out of flow leaves the link the size of its dot. The label still
 * slides in on hover exactly as the design intends, and the link keeps a 24px
 * square so the dot stays easy to hit.
 * -------------------------------------------------------------------------- */

.chapter-dots > a {
	min-height: 24px;
	min-width: 24px;
	position: relative;
}

.chapter-dots span {
	position: absolute;
	right: calc(100% + 10px);
	top: 50%;
	transform: translateY(-50%) translateX(8px);
	white-space: nowrap;
}

.chapter-dots a:focus-visible span,
.chapter-dots a:hover span {
	transform: translateY(-50%) translateX(0);
}

/* --------------------------------------------------------------------------
 * Header legibility over the hero photograph
 *
 * The header is #faf9f4 over whatever the hero image happens to be. Measured
 * against the actual rendered pixels, the place name came out at 3.86:1 and the
 * coordinates — which the design fades to opacity .6 — at 2.42:1. Both are below
 * the 4.5:1 that 11px text needs, and axe cannot see it because the background is
 * an image rather than a colour.
 *
 * A soft scrim under the header fixes it for any hero image rather than for this
 * one: dark at the very top where the text sits, gone by the time it reaches the
 * heading. Only on pages whose header sits over the hero — the article, privacy and
 * not-found pages put ink text on paper and need no scrim.
 * -------------------------------------------------------------------------- */

body:has(.hero) .site-header::before,
body:has(.detail-hero) .site-header::before {
	background: linear-gradient(to bottom, rgb(5 27 24 / 72%) 0%, rgb(5 27 24 / 40%) 45%, rgb(5 27 24 / 0%) 100%);
	content: "";
	inset: 0;
	pointer-events: none;
	position: absolute;
	z-index: -1;
}

/*
 * ...but not once the header has left the hero behind.
 *
 * Below 600px app.css pins the header and gives it the paper treatment —
 * `background: #f3f1ebf2`, ink text, 68px tall. The scrim above is scoped to the
 * *page* rather than to that state, so it carried on painting a dark green gradient
 * under a paper header: the brand stayed legible on the left where the gradient is
 * weakest, and the right-hand side went visibly green behind the menu button. That is
 * the "colour isn't displaying correctly" on a phone.
 *
 * There is nothing to protect here — the header is ink on its own opaque paper, which
 * is the contrast the measured colour layer is built for.
 */
.site-header.is-scrolled::before {
	display: none;
}

/*
 * The coordinates run at full strength everywhere, and take the header's own colour:
 * near-white over a hero photograph, ink over paper on every other page.
 *
 * The design fades them to opacity .6, which fails at 11px on both grounds — 2.42:1
 * against the bright sky in the hero, and about 3.2:1 as ink on paper elsewhere. Only
 * the hero case was obvious from a screenshot; the paper case was caught by axe on
 * /about-us/ after an earlier version of this rule scoped the fix to hero pages.
 *
 * Their smaller size and wider tracking already mark them as the secondary line.
 */
.header-place > span {
	opacity: 1;
}

/* --------------------------------------------------------------------------
 * Gallery — tile slider
 *
 * The same track, arrows and drag as the carousel; only the slide sizing differs.
 * A wide project photograph wants one slide in view, but a set of 9:16 phone
 * photographs does not: full-width they read as a wall of images, and laid out as a
 * grid the section runs to thousands of pixels. Several tiles in view, sliding
 * sideways, keeps the section one row high however many photographs there are.
 *
 * The crop is only the tile — the lightbox opens each photograph whole, which is why
 * a tile can take a calmer 3/4 shape than the pictures themselves.
 * -------------------------------------------------------------------------- */

.gallery-tiles .gallery-slide {
	aspect-ratio: var(--gallery-tile-aspect, 3 / 4);
	/* One row of --gallery-per-view, with the track's 16px gaps accounted for. */
	flex: 0 0 calc((100% - (var(--gallery-per-view, 4) - 1) * 16px) / var(--gallery-per-view, 4));
}

.gallery-tiles .gallery-slide img {
	height: 100%;
	object-fit: cover;
	object-position: center;
	transition: transform 0.9s;
	width: 100%;
}

.gallery-tiles .gallery-slide:hover img {
	transform: scale(1.04);
}

@media (max-width: 1100px) {

	.gallery-tiles .gallery-viewport {
		--gallery-per-view: 3;
	}
}

@media (max-width: 760px) {

	.gallery-tiles .gallery-viewport {
		--gallery-per-view: 2;
	}
}

@media (max-width: 480px) {

	/* A hair over one, so the next tile peeks in and the track reads as swipeable. */
	.gallery-tiles .gallery-viewport {
		--gallery-per-view: 1.35;
	}
}

/* --------------------------------------------------------------------------
 * Mock-house set switch
 *
 * `.project-filters` is a space-between row, which on the projects page spreads
 * several categories across the width. With two buttons on a phone it threw them to
 * opposite edges of the screen; a set switch reads as one group, so it stays one.
 * -------------------------------------------------------------------------- */

.mockhouse-filters {
	gap: 26px;
	justify-content: flex-start;
	margin-bottom: 8px;
}

/*
 * A drawing is shown whole, not cropped: a site layout loses its meaning the moment
 * an edge is cut off. The tile keeps its own paper behind the letterboxing.
 */
.gallery-tiles-contain .gallery-slide {
	background: #e8e7de;
}

.gallery-tiles-contain .gallery-slide img {
	object-fit: contain;
}

/*
 * A set that fits has nothing to scroll, so it sits centred and stops behaving like a
 * track: no snapping, no drag cursor, and the tiles keep their width rather than
 * stretching to fill the row.
 */
.gallery-tiles-static .gallery-viewport {
	overflow-x: visible;
	scroll-snap-type: none;
}

.gallery-tiles-static .gallery-track {
	justify-content: center;
}

@media (max-width: 760px) {

	.gallery-tiles-static .gallery-track {
		flex-wrap: wrap;
		justify-content: flex-start;
	}
}

/* --------------------------------------------------------------------------
 * Headline figures
 *
 * At the client's request the credibility numbers carry more weight: larger, and
 * with the tabular figures the serif offers, so "24+", "4M+" and "2,000+" line up as
 * a set rather than drifting. They are the first thing the section is there to say.
 * -------------------------------------------------------------------------- */

.trust-stat-card .stat-number,
.about-stat-card .stat-number {
	font-feature-settings: "tnum" 1, "lnum" 1;
	font-weight: 500;
	letter-spacing: -1px;
}

.trust-stat-card .stat-number {
	font-size: clamp(48px, 4.6vw, 76px);
}

.about-stat-card .stat-number {
	font-size: clamp(42px, 3.6vw, 58px);
}

@media (max-width: 900px) {

	.trust-stat-card .stat-number {
		font-size: clamp(38px, 8vw, 52px);
	}

	.about-stat-card .stat-number {
		font-size: clamp(36px, 7.5vw, 48px);
	}
}

/* --------------------------------------------------------------------------
 * The outline button on a photograph
 *
 * `.hero-btn-secondary` is a ghost button — transparent, with a hairline border —
 * sitting on a hero photograph, and it only takes colour on hover. It reads as
 * decoration until you happen to point at it, which is why it was reported as
 * "only visible on hover". It keeps its outline character but now has enough ground
 * to be legible at rest.
 *
 * The opacity is a contrast figure, not a taste one. Giving the button a background
 * at all is what makes its contrast *measurable*: while it was transparent axe could
 * not resolve a background and skipped it, and the first pass here (42%) composited
 * to rgb(151 156 155) over a light ancestor — 2.46:1 against --paper text, which axe
 * duly failed on /about-us/ and /contact-us/. 78% composites to rgb(62 72 69) in the
 * same worst case, 8.34:1, so the button stays legible whatever photograph is behind
 * it. Hover goes darker still because gold is the lighter of the two text colours.
 * -------------------------------------------------------------------------- */

/*
 * On a photograph. `.hero` is the home hero and `.about-hero.has-image` is a page
 * hero that was given one; `page-hero.php` already puts `has-image` on the section,
 * so the distinction is read from the markup rather than guessed at.
 */
/*
 * No `backdrop-filter` here, deliberately. It sat directly over the hero photograph,
 * which the parallax moves, so the blur was re-sampled every scrolled frame for a
 * purely decorative effect. The contrast guarantee is the 78% ground below, never the
 * blur — and a rule this harness cannot measure must not be in the critical path.
 */
.hero .hero-btn-secondary,
.about-hero.has-image .hero-btn-secondary {
	background: rgb(8 20 17 / 78%);
	border-color: rgb(243 241 235 / 85%);
}

.hero .hero-btn-secondary:hover,
.hero .hero-btn-secondary:focus-visible,
.about-hero.has-image .hero-btn-secondary:hover,
.about-hero.has-image .hero-btn-secondary:focus-visible {
	background: rgb(8 20 17 / 92%);
	border-color: var(--color-brand-gold);
	color: var(--color-brand-gold);
}

/*
 * On paper, where there is no photograph at all — the About and Contact page heroes.
 * The dark ground was a contrast device for an unknown background, and applied here
 * it painted a near-black slab on a pale page, which is how it was reported. With a
 * surface this stylesheet knows, the design's own ghost button is legible as it
 * stands; it only has to swap the near-white it inherits for ink.
 *
 * #292b26 on #f3f1eb is 12.4:1. The border is the ink at 30%, which is the weight of
 * `--line` (#d6d4cb) the design already rules this page with.
 */
.about-hero:not(.has-image) .hero-btn-secondary {
	background: transparent;
	border-color: rgb(41 43 38 / 30%);
	color: var(--ink);
}

.about-hero:not(.has-image) .hero-btn-secondary:hover,
.about-hero:not(.has-image) .hero-btn-secondary:focus-visible {
	background: var(--ink);
	border-color: var(--ink);
	color: var(--paper);
}

/*
 * On the closing panel, which is solid brand green — so the ghost button app.css
 * draws is both legible (#f3f1eb on #054239 is 9.99:1) and measurable, and needs
 * nothing from here but its hover.
 */
.about-cta-box .hero-btn-secondary:hover,
.about-cta-box .hero-btn-secondary:focus-visible {
	background: var(--color-soft-gold, #c69c6d);
	border-color: var(--color-soft-gold, #c69c6d);
	color: var(--dark);
}

/* --------------------------------------------------------------------------
 * The footer's company links, and the social row above the columns
 *
 * The overlay menu is deliberately four destinations, so the footer is where the
 * rest of the company is reachable. It went too far the other way for a while: six
 * columns, one of them every project by name, which wrapped onto a second row and
 * read as clutter rather than as a way in.
 *
 * At the client's request the projects column is gone and the social links have
 * moved out of the grid to a row of their own above it. What is left — brand,
 * contact, address, company — is **exactly the four columns app.css already sizes**
 * (`1.2fr 1.3fr 1fr .7fr`), so there is no grid override here at all any more. The
 * company column simply takes the slot the social links used to hold.
 * -------------------------------------------------------------------------- */

.footer-links a {
	display: block;
	margin-top: 8px;
	transition: color 0.3s ease;
}

.footer-links a:hover {
	color: var(--color-brand-gold);
}

/*
 * The social row. `.footer-details > div` is what app.css styles the footer's small
 * type with, and this sits outside that grid, so it restates those two values and
 * nothing else; everything after is layout.
 */
.footer-social-row {
	align-items: baseline;
	border-bottom: 1px solid rgb(217 220 210 / 14%);
	column-gap: 26px;
	color: #d9dcd2;
	display: flex;
	flex-wrap: wrap;
	font-size: 11px;
	line-height: 1.9;

	/*
	 * This row is a band between two rules, not a section sitting under one, so its
	 * space is symmetric — the client asked for the gap above and below to match.
	 * The two numbers differ by 2px because they are padding and the gap is measured
	 * to the text: the label's line box adds about 1px of half-leading at the top and
	 * 3px at the bottom, so equal padding would read as unequal space.
	 *
	 * The rows below it are sections rather than bands, and keep the rhythm app.css
	 * sets on `.footer-details` — 28px below a rule, 48px above the next.
	 */
	padding-bottom: 33px;
	padding-top: 35px;
}

.footer-social-row .micro {
	margin-right: 4px;
}

/*
 * inline-flex, not the inherited inline: the label and its arrow are separate nodes
 * and wrapped onto two lines in the row's narrower line box, so "Instagram" sat above
 * its own arrow.
 */
.footer-social-row a {
	align-items: center;
	display: inline-flex;
	gap: 5px;
	transition: color 0.3s ease;
	white-space: nowrap;
}

.footer-social-row a:hover {
	color: var(--color-brand-gold);
}

/* The row above takes the padding, so the grid does not add its own on top. */
.footer-social-row + .footer-details {
	padding-top: 28px;
}

@media (max-width: 600px) {

	.footer-social-row {
		column-gap: 18px;
		font-size: 9px;
	}
}

/* --------------------------------------------------------------------------
 * The hero slider
 *
 * app.css positions `.hero-image` and sizes `.hero-image img` to fill it, but the
 * design only ever had one photograph there, so the images are in normal flow. In
 * slider mode they are stacked instead and crossfaded — the class is only on the
 * wrapper when there is more than one image, so a single-still hero is untouched.
 *
 * The transition is on the image rather than a wrapper so `object-fit` and
 * `object-position` keep coming from app.css's own `.hero-image img` rule; the
 * crop must not change between slides.
 * -------------------------------------------------------------------------- */

/*
 * A hero photograph is wrapped in `<picture>` when a mobile version is set.
 * `display: contents` takes the wrapper out of the box tree, so the img keeps sizing
 * and positioning against `.hero-image` exactly as it did as its direct child —
 * otherwise `height: 100%` resolves against an auto-height inline wrapper and the
 * photograph collapses, and `inset: 0` below would position against the wrapper.
 */
.hero-image picture {
	display: contents;
}

.hero-image-slider img {
	inset: 0;
	opacity: 0;
	position: absolute;
	transition: opacity 1400ms ease-in-out;
}

.hero-image-slider img.is-active {
	opacity: 1;
}

/*
 * No JavaScript, or reduced motion: the first photograph carries `.is-active` in the
 * markup, so it stands on its own without a second rule here — an
 * `img[data-hero-slide="0"]` selector would tie with `.is-active` on specificity and,
 * coming later, pin slide one visible and break every crossfade.
 */

@media ( prefers-reduced-motion: reduce ) {
	.hero-image-slider img {
		transition: none;
	}
}

/* --------------------------------------------------------------------------
 * The principles on a phone
 *
 * The design is two columns: a sticky image on the left that swaps as you scroll
 * through the principles on the right. Below 600px app.css collapses that to one
 * column, which leaves the sticky frame as a single picture sitting above all four
 * principles and changing under you — so the photograph is never next to the words it
 * illustrates, which is what was reported.
 *
 * Each principle carries its own copy of the same photograph, and exactly one of the
 * two sets is ever shown: the stacked frame on a wide screen, the inline ones on a
 * narrow one. The hidden set is `display: none`, so it is out of the accessibility
 * tree too and a screen reader is never offered the image twice.
 *
 * Desktop is untouched — that half already works.
 * -------------------------------------------------------------------------- */

.principle-inline-image {
	display: none;
}

@media (max-width: 600px) {

	/*
	 * The sticky frame and its big numeral go with it: the numeral labels whichever
	 * image is showing, and there is no single image to label any more.
	 */
	.why-chapter .principle-image {
		display: none;
	}

	.principle-inline-image {
		aspect-ratio: 1.5;
		display: block;
		margin-top: 22px;
		object-fit: cover;
		width: 100%;
	}

	/*
	 * app.css dims an inactive principle to .75 on a phone. That was right while the
	 * dimming pointed at the one image above; with the photograph inside the article
	 * it reads as a half-loaded picture instead. Nothing is lost by dropping it: each
	 * principle now shows its own image, so there is no longer an "active" one for the
	 * dimming to point at.
	 */
	.why-chapter .principle {
		opacity: 1;
		padding-bottom: 38px;
	}
}

/* --------------------------------------------------------------------------
 * Smooth scrolling: the one place Lenis' own CSS is wrong for this site
 *
 * `lenis.css` sets `.lenis.lenis-smooth iframe { pointer-events: none }`, which is
 * right in general — it stops the wheel being swallowed by an embed the visitor is
 * only scrolling past — and wrong for the regional map, which is an iframe the
 * visitor is meant to pan, zoom and click. Its container carries
 * `data-lenis-prevent`, so Lenis already leaves the wheel alone over it; this gives
 * the embed its pointer back so it can actually be used.
 * -------------------------------------------------------------------------- */

/*
 * The embed takes no pointer events until the map has been clicked.
 *
 * With them on, the wheel over the map went to Google and the page stopped scrolling
 * — the stutter that was reported. Off, the wheel reaches Lenis and the page moves
 * normally across the section; `map.js` adds `.is-interactive` on click and removes
 * it when the pointer leaves, so panning and zooming still work once asked for.
 *
 * The `.lenis.lenis-smooth` prefix is gone with it: that existed to undo a rule in
 * lenis.css, and the default is now the same either way.
 */
.location-map iframe {
	pointer-events: none;
}

.location-map.is-interactive iframe {
	pointer-events: auto;
}

/* A quiet hint, so a dormant map does not look broken. */
.location-map:not(.is-interactive)::after {
	background: rgb(8 22 18 / 72%);
	border-radius: 2px;
	bottom: 14px;
	color: var(--paper);
	content: attr(data-dg-activate-label);
	font-family: var(--sans);
	font-size: 10px;
	left: 50%;
	letter-spacing: 0.1em;
	padding: 7px 12px;
	pointer-events: none;
	position: absolute;
	text-transform: uppercase;
	transform: translateX(-50%);
	white-space: nowrap;
	z-index: 2;
}

/* --------------------------------------------------------------------------
 * The principles: holding the sticky column through the fourth image
 *
 * A sticky element can only travel inside its containing block, which for this grid
 * item is its grid area — so the left column unsticks the moment the *right* column
 * runs out. Measured at 1600x913: it released at scrollY 5013 while the fourth
 * principle only became the active one at 5140. The fourth image therefore appeared
 * on a column that had already started scrolling away, which is what was reported.
 *
 * The room has to come from the right column, because that is what bounds the travel.
 * The band that activates a principle is the middle 40% of the viewport (the source's
 * `-30% 0px -30% 0px`), so the space needed to scroll a principle through it is a
 * fraction of the viewport, not a fixed number of pixels — hence vh. 30vh holds the
 * fourth image for about 150px of scroll after it appears, then the column releases.
 *
 * Only in the two-column layout. Below 600px app.css collapses the grid, nothing is
 * sticky, and this would be dead space under the last principle.
 * -------------------------------------------------------------------------- */

@media (min-width: 601px) {

	.principles-list {
		padding-bottom: 30vh;
	}
}

/* --------------------------------------------------------------------------
 * The hero slider's pager
 *
 * Auto-advance on its own gives a visitor no way back to a photograph they liked, so
 * the slider carries a manual control. It rides on the hero's bottom row, beside the
 * scroll cue, sharing one `space-between` slot with it — see the note in app.js for
 * the two placements that were measured and rejected before this one, and why a
 * fourth child of `.hero-base` was not an option.
 *
 * Each control is a rule rather than a circle because that is the hero's own
 * vocabulary: `.small-rule` before the description, the `i` between the two halves of
 * `.hero-index`, the three strokes of `.architecture-lines`. The active one is gold
 * and longer, so the state reads at a glance and not only by colour.
 *
 * The hit area is 26px tall via padding while the mark stays 2px, because a 2px
 * target is not a target.
 * -------------------------------------------------------------------------- */

/*
 * The wrapper keeps `.hero-base` at three slots, and the pager is taken out of its
 * flow so the wrapper measures exactly what the scroll cue measures. Both halves
 * matter: a fourth child would have `space-between` redistribute every item, and a
 * wrapper merely *wider* than the cue does the same thing more quietly — laid out in
 * flow it pushed "Discover our places" 85px to the right. Out of flow, the row is
 * byte-identical to the design and the pager hangs 30px off the cue's right edge.
 */
.hero-base-lead {
	align-items: center;
	display: flex;
	position: relative;
}

.hero-slider-nav {
	align-items: center;
	display: flex;
	gap: 10px;
	left: 100%;
	margin-left: 30px;
	position: absolute;
	top: 50%;
	transform: translateY(-50%);
}

.hero-slider-dot {
	background: none;
	border: 0;
	cursor: pointer;
	display: block;
	padding: 12px 0;
}

/*
 * These sit on a photograph, which no colour alone can be relied on to clear — the
 * same trap as the header's coordinates. The rules are near-white at 70% and carry a
 * hairline of the hero's own dark under them, so they read against a bright sky or a
 * pale facade without being drawn heavier than the design would allow.
 */
.hero-slider-dot::before {
	background: rgb(243 241 235 / 70%);
	box-shadow: 0 1px 0 rgb(8 20 17 / 45%);
	content: "";
	display: block;
	height: 2px;
	transition: background-color 0.4s ease, width 0.4s ease;
	width: 26px;
}

.hero-slider-dot:hover::before,
.hero-slider-dot:focus-visible::before {
	background: rgb(243 241 235 / 100%);
}

.hero-slider-dot.is-active::before {
	background: var(--color-brand-gold);
	width: 46px;
}

/*
 * Narrow screens: the base row runs out of width before the pager does. Measured, the
 * pager's right edge against "Discover our places" — it overlaps by 21px at 600 and
 * by more on every phone, is 2px clear at 620, and gets 39px at 660. So it comes off
 * below 680, with margin. The photographs still advance on their own; nothing is lost
 * but the control.
 *
 * There is no height rule any more. The pager was under the hero copy at one point,
 * where it ate the clearance that keeps the copy off the base row on short screens;
 * out of flow inside `.hero-base` it adds no height anywhere, so short screens are
 * fine — verified at 1024x700 and 1366x680.
 */
@media (max-width: 679px) {

	.hero-slider-nav {
		display: none;
	}
}

/* --------------------------------------------------------------------------
 * The footer's columns on a phone
 *
 * app.css drops the footer to `1fr 1fr` below 900px, and a bare `1fr` is really
 * `minmax(auto, 1fr)` — it will not shrink below its content's minimum. The email
 * address is one unbreakable word whose minimum is 196px at the 16px reading size the
 * type layer sets, so at 375px the two columns wanted 346px of a 335px grid and the
 * address was simply cut off at the edge. Nothing caught it because the overflow is
 * clipped rather than scrolled, so the page reports no horizontal overflow at all.
 *
 * `overflow-wrap: anywhere` on the address is what actually fixes it — verified by
 * turning each rule off in turn — and the `<wbr>` that `divine_group_email_html()`
 * puts after the "@" is what makes the break land there rather than mid-word
 * ("sales@omdivinewo / rld.com"). `minmax(0, 1fr)` is the structural half: a column
 * that cannot shrink below its content is the root cause, and without it the next
 * long thing dropped into these columns blows the grid out again. Only the address
 * needs `anywhere`; ordinary prose here keeps normal wrapping.
 * -------------------------------------------------------------------------- */

@media (max-width: 900px) {

	.footer-details {
		grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
	}

	.footer-details a[href^="mailto:"] {
		overflow-wrap: anywhere;
	}
}

/* --------------------------------------------------------------------------
 * The hero copy changing with the photograph
 *
 * Each slide can carry its own words. The fade is on the copy's children, never on
 * `.hero-copy` itself: the parallax writes that element's `opacity` every frame as
 * the hero scrolls away, and a transition on the same property would fight it.
 *
 * `visibility` is deliberately left alone — the text stays in the accessibility tree
 * and in the tab order through the swap, so a keyboard user mid-focus is not dropped.
 * -------------------------------------------------------------------------- */

.hero-copy .hero-eyebrow,
.hero-copy .hero-heading,
.hero-copy .hero-description p {
	transition: opacity 360ms ease;
}

.hero-copy.is-swapping .hero-eyebrow,
.hero-copy.is-swapping .hero-heading,
.hero-copy.is-swapping .hero-description p {
	opacity: 0;
}

@media (prefers-reduced-motion: reduce) {

	/* The slider does not run at all here, but the rule should not be left armed. */
	.hero-copy .hero-eyebrow,
	.hero-copy .hero-heading,
	.hero-copy .hero-description p {
		transition: none;
	}
}

/* --------------------------------------------------------------------------
 * The map, now that it shows one place
 *
 * The frame was a two-column grid — map on the left, the three places on the right —
 * because a keyless embed shows one place at a time and that list was the control
 * for it. With the tabs gone there is nothing to choose between, so the map takes
 * the whole frame and the address sits under it as a single row.
 *
 * The rules the list needed are left where they are rather than deleted: they are
 * scoped to `.location-map-list`, which no longer renders, and the project-explorer
 * and search pages reuse some of the same `.location-*` type classes.
 * -------------------------------------------------------------------------- */

/*
 * The map is the whole frame, and a taller one than the design's.
 *
 * app.css gives `.location-visual` `aspect-ratio: 2.75`, drawn for an abstract
 * regional diagram. A real map at that ratio is a letterbox — about 420px of it once
 * the row under it had taken its share — and the client asked for more height after
 * that row came out, then for 50px of it back.
 *
 * The ratios are worked from the frame's measured width so each breakpoint loses the
 * same amount rather than a different one. It came down twice after that, 50px then
 * 30: 2.39 is 580px at the 1387px desktop frame (from 660), 1.44 is 541px at 779
 * (from 623), and the phone is a height rather than a ratio, 340px (from 420).
 */
.location-visual-single {
	aspect-ratio: 2.39;
	grid-template-columns: minmax(0, 1fr);
	grid-template-rows: minmax(0, 1fr);
}

.location-visual-single .location-map {
	min-height: 420px;
}



@media (max-width: 900px) {

	/* Taller again where the frame is narrow, or the map is a slot. */
	.location-visual-single {
		aspect-ratio: 1.44;
	}
}

@media (max-width: 600px) {

	/*
	 * Height, not a ratio. A ratio plus the map's own `min-height` disagree on a narrow
	 * screen — 0.85 of 323px is 379 while the map asked for 400 — and `.location-visual`
	 * clips its overflow, so 21px of the map was simply cut off. Here the frame is told
	 * the height and the map fills it.
	 */
	.location-visual-single {
		aspect-ratio: auto;
		min-height: 340px;
	}

	.location-visual-single .location-map {
		min-height: 0;
	}
}

/* --------------------------------------------------------------------------
 * Every project by name, under the footer columns
 *
 * Back at the client's request on SEO grounds — each project is its own product page
 * and wants an internal link from every page — but **not** as a fifth column. That is
 * what it was the first time, and six columns wrapped onto a second row and read as
 * clutter. Outside the grid, in a row of its own, the design's four columns above are
 * untouched and a list of nine-and-growing names flows as text columns, which is what
 * it wants anyway.
 *
 * `.footer-details > div` is what app.css gives the footer's small type, and this
 * sits outside that grid, so the two values are restated here and nothing else.
 * -------------------------------------------------------------------------- */

.footer-projects-row {
	border-top: 1px solid rgb(217 220 210 / 14%);
	color: #d9dcd2;
	line-height: 1.9;

	/* The same 28/48 rhythm as the social row above. */
	padding: 28px 0 48px;
}

/*
 * The same size as every other link in the footer.
 *
 * app.css sets `.footer-details > div` to 11px and the readable-type layer raises it
 * to 16px; this row sits outside that grid, so it kept the 11px and the project names
 * read as fine print beside "About / Leadership / Awards" directly above them. The
 * label keeps the design's 11px micro size, as it does in the columns.
 */
.footer-projects-row .footer-projects-list {
	font-size: 16px;
}

.footer-projects-row .micro {
	font-size: 11px;
	margin-bottom: 12px;
}

/*
 * The same four columns as `.footer-details` above it, not CSS text columns.
 *
 * As `columns: 4` the names started at 100, 460 and 820 while the grid above began
 * its columns at 100, 507, 944 and 1292 — only the first lined up, so the block read
 * as a separate thing bolted under the footer rather than part of it. Grid with
 * app.css's own `1.2fr 1.3fr 1fr .7fr` and its 50px gap puts every name on one of
 * the four gridlines already in use.
 *
 * `minmax(0, …)` rather than a bare `fr`: a bare `1fr` is `minmax(auto, 1fr)` and
 * will not shrink below its content, which is exactly how the footer email blew the
 * grid out at 375px. A long project name may wrap here; it may not widen a column.
 *
 * Items flow across rather than down, which is why `break-inside` is gone with the
 * text columns that needed it.
 */
.footer-projects-list {
	column-gap: 50px;
	display: grid;
	grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.3fr) minmax(0, 1fr) minmax(0, 0.7fr);
	list-style: none;
	margin: 0;
	padding: 0;
}

/*
 * app.css changes the footer grid three times on the way down, and a single
 * hardcoded template only lined up at the widest. Measured off `.footer-details`
 * itself: the gap goes 50 → 25 → 35 → 20, the template goes
 * `1.2 1.3 1 .7` → `1 1.2 1 .7` → two columns, and below 1024 the brand column is
 * floored at 205px, which is what `minmax(205px, 1fr)` reproduces.
 *
 * This is a mirror of values this file does not own, so `09-interaction` compares
 * the two rows' column origins rather than trusting the numbers to stay true.
 */
@media (max-width: 1200px) {

	.footer-projects-list {
		column-gap: 25px;
		grid-template-columns: minmax(205px, 1fr) minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 0.7fr);
	}
}

.footer-projects-list a {
	display: block;
	padding: 3px 0;
	transition: color 0.3s ease;
}

.footer-projects-list a:hover,
.footer-projects-list a:focus-visible {
	color: var(--color-brand-gold);
}

@media (max-width: 900px) {

	/*
	 * Below 900 the names flow as text columns rather than sitting in a grid.
	 *
	 * In a grid every cell in a row is as tall as the tallest, and "Divine World
	 * Phase III Extension" wraps to two lines on a phone — so the cell beside it was
	 * padded out and the list showed a hole under "Divine Residency". Flowing them
	 * fills the column instead. The grid stays above 900, where the four columns line
	 * up with the four above them and nothing wraps.
	 */
	.footer-projects-list {
		column-gap: 35px;
		columns: 2;
		display: block;
		grid-template-columns: none;
	}

	.footer-projects-list li {
		break-inside: avoid;
	}
}

@media (max-width: 600px) {

	/*
	 * Two columns on a phone, at the client's request — one column made a tall stack of
	 * nine names. A couple of the longer names wrap to two lines in a ~150px column,
	 * which is the trade for the shorter list; the size comes down a little so they
	 * wrap at the word rather than mid-name.
	 */
	.footer-projects-list {
		column-gap: 20px;
	}

	.footer-projects-row .footer-projects-list {
		font-size: 14px;
	}
}

/* --------------------------------------------------------------------------
 * The bottom bar on a phone
 *
 * The design sets these utility links at 8px and the readable-type layer raises them
 * to 16px with every other link. That is right on a wide screen and too much on a
 * narrow one: four of them plus "Cookie preferences" do not fit a 375px line, so the
 * row wrapped and the last label broke across two lines.
 *
 * They are navigation furniture rather than reading copy — the one place the 16px
 * floor is eased, and only below 600px.
 *
 * 12px with a 10px gap, measured at 375px rather than picked: 14px and 13px still
 * wrapped the row onto two lines (39px tall), 12px at the old 16px gap still wrapped,
 * and 12px at 10px fits on one 30px line. 11px fits too but is a bigger drop than the
 * problem needs — it is still double the 8px the design itself asked for.
 * -------------------------------------------------------------------------- */

@media (max-width: 600px) {

	.footer-bottom > div > a,
	.footer-bottom > div > button {
		font-size: 12px;
	}

	.footer-bottom > div {
		column-gap: 10px;
		row-gap: 10px;
	}
}

/* --------------------------------------------------------------------------
 * The hero's bottom row on a phone
 *
 * "SCROLL TO DISCOVER" and "Discover our places" come off below 600px at the
 * client's request. On a phone the hero is already full of its own copy and two
 * buttons, and those two sit right above the fixed action bar.
 *
 * 600px is the design's own phone breakpoint — where app.css pins the header and
 * turns `.hero-description` into a column — and `.hero-edition`, the third item in
 * this row, is already hidden by app.css below 900px. So on a phone the row has
 * nothing left in it and collapses; it is `position: absolute`, so that costs the
 * hero no layout either way.
 *
 * Nothing is lost but the shortcut: "Projects" is in the fixed action bar at the
 * bottom of every phone screen and in the overlay menu, and the scroll cue was only
 * ever an invitation to do what a phone user does by reflex.
 * -------------------------------------------------------------------------- */

@media (max-width: 600px) {

	.hero-base .scroll-cue,
	.hero-base .hero-project-link {
		display: none;
	}
}

/* --------------------------------------------------------------------------
 * Locking the page without moving it
 *
 * Opening an overlay locks the page, which removes the scrollbar, which takes
 * `document.documentElement.clientWidth` from 1585 to 1600 — so the hero, its
 * photograph and the heading all grew 15px on open and snapped back on close.
 *
 * `scrollbar-gutter: stable` fixed that and cost something subtler: it reserves the
 * track *permanently*, so on a system with overlay scrollbars — every Mac — the page
 * ran 15px narrower than the design's for ever. The hero photograph is
 * `object-fit: cover`, so 15px of width is a different crop, and side by side the
 * background did not match. Measured on the photograph away from any text, the
 * difference was 0.123 RMSE; at a matched width it is 0.011.
 *
 * So the space is given back only while the page is actually locked, and only as much
 * as the scrollbar really took — `app.js` measures that and sets `--dg-lock-gutter`.
 * Where scrollbars are overlays it measures 0 and nothing changes at all.
 * -------------------------------------------------------------------------- */

html.has-overlay-open {
	padding-right: var(--dg-lock-gutter, 0px);
}

/* --------------------------------------------------------------------------
 * The first-visit dialog
 *
 * It carries a welcome, the imagery disclaimer and the cookie notice now, so it is
 * longer than the choice panel it replaced. These rules only give the new blocks
 * their rhythm — the frame, the heading and the buttons are the dialog's own, and the
 * micro eyebrow and `.arrow-link` are the design's.
 *
 * `.cookie-dialog` already scrolls past the viewport height, which the disclaimer
 * needs on a short screen.
 * -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
 * The first-visit notice
 *
 * A floating card near the bottom of the window — the shape the client pointed at on
 * galaxyweblinks.com: inset from the edges, rounded, dark, with two pill buttons.
 * Not a full-bleed slab, which is what it was first and what prompted the note.
 *
 * It does not dim or lock the page: the site stays visible and usable behind it,
 * which is why the markup carries no `aria-modal`. The backdrop is shared with the
 * gallery lightbox — that one is still a centred modal — so only the cookie one is
 * re-anchored, and it passes pointer events through to the page.
 *
 * Dark green on paper text, gold for the primary: the panel borrows the treatment the
 * design already uses for the mobile action bar, so it reads as part of the site
 * rather than as a plugin.
 * -------------------------------------------------------------------------- */

.cookie-dialog-backdrop {
	align-items: flex-end;
	backdrop-filter: none;
	background: none;
	justify-content: center;
	padding: 0 clamp(12px, 2vw, 28px) clamp(12px, 2vw, 28px);
	pointer-events: none;
}

/*
 * `!important` because app.css already uses it here: it pins `.cookie-dialog` to
 * paper, square corners and 40px of padding with `!important` on each, and app.css is
 * not ours to edit. Equal specificity, so this wins on order — wordpress.css loads
 * after it.
 */
.cookie-dialog-welcome {
	/*
	 * One gutter, used by the card's padding and by the header strip's negative
	 * margin. It cannot be a percentage: padding resolves against the backdrop and
	 * a child's margin against the card's content box, so the two differed by about
	 * 5px and the divider stopped short of the card's edges at every width.
	 */
	--dg-notice-gutter: clamp(20px, 3.2vw, 52px);
	background: var(--dark) !important;
	border-radius: 16px !important;
	box-shadow: 0 -14px 44px rgb(8 20 17 / 30%);
	color: var(--paper);
	max-height: 76vh;
	max-width: none !important;
	padding: 26px var(--dg-notice-gutter) !important;
	pointer-events: auto;
	width: 100%;
}

/* The dark panel is the other palette — the tokens have to move with it. */
.cookie-dialog-welcome {
	--color-brand-gold: #cda87e;
	--gold: #cda87e;
	--color-text-muted: #c9cfc4;
	--muted: #c9cfc4;
}

.cookie-dialog-welcome h2 {
	color: var(--paper);
	font-size: 24px;
	line-height: 1.2;
	margin: 0 0 10px;
	padding-right: 0;
}

.cookie-dialog-welcome .micro {
	color: var(--muted);
	display: block;
	margin-bottom: 10px;
}

.cookie-dialog-welcome .cookie-welcome > p {
	line-height: 1.75;
	max-width: 62ch;
}

.cookie-dialog-welcome > p,
.cookie-dialog-welcome .cookie-ask p {
	color: var(--muted);
}

.cookie-dialog-welcome .cookie-welcome p {
	color: var(--muted);
	margin: 0;
}

.cookie-dialog-welcome .cookie-subhead {
	color: var(--paper);
	font-family: var(--sans);
	font-size: 15px;
	font-weight: 500;
	letter-spacing: 0;
	margin: 0 0 4px;
}

/*
 * The header strip: the mark and the close, with a rule under both.
 *
 * It bleeds to the card's edges rather than sitting inside its padding, which is
 * what makes the rule read as a divider across the notice instead of a line
 * floating in the middle of it. The negative margins mirror the card's own padding,
 * so the two have to be changed together.
 */
.cookie-dialog-welcome .cookie-dialog-head {
	align-items: center;
	border-bottom: 1px solid rgb(217 220 210 / 16%);
	display: flex;
	justify-content: space-between;
	margin: -26px calc(var(--dg-notice-gutter) * -1) 20px;
	padding: 14px var(--dg-notice-gutter);
}

.cookie-dialog-welcome .cookie-dialog-logo {
	height: auto;
	width: 132px;
}

/*
 * The close button, on the bar as well as on a phone — it was hidden on wide screens
 * while this was a panel with its own buttons, and the client asked for it back.
 *
 * In the strip it is a flex item, so the `position: absolute` it inherits from the
 * centred dialog's own close has to be undone, or it leaves the strip and lands over
 * the copy.
 *
 * Dismissing is not consenting: `app.js` deliberately records nothing here, so the
 * notice returns on the next visit rather than being taken as a silent yes.
 */
.cookie-dialog-welcome .cookie-dialog-close {
	border-radius: 50%;
	color: var(--paper);
	display: grid;
	height: 32px;
	place-items: center;
	position: static;
	width: 32px;
}

.cookie-dialog-welcome .cookie-dialog-close:hover,
.cookie-dialog-welcome .cookie-dialog-close:focus-visible {
	background: rgb(243 241 235 / 12%);
	color: var(--gold);
}

/*
 * The disclaimer, folded. Closed it is one line; open it scrolls inside the card
 * rather than growing it past the window.
 */
/*
 * No rule above this. It was a separator across the full width of the bar, and with
 * the actions sitting in the row above it read as a stray line hanging off the
 * decline button rather than as a divider.
 */
.cookie-dialog-welcome .cookie-disclaimer {
	font-size: 13px;
	margin-top: 18px;
}

.cookie-dialog-welcome .cookie-disclaimer > summary {
	color: var(--paper);
	cursor: pointer;
	font-size: 13px;
	letter-spacing: 0.4px;
	list-style: none;
	text-transform: uppercase;
}

.cookie-dialog-welcome .cookie-disclaimer > summary::-webkit-details-marker {
	display: none;
}

.cookie-dialog-welcome .cookie-disclaimer > summary::after {
	content: " +";
	color: var(--gold);
}

.cookie-dialog-welcome .cookie-disclaimer[open] > summary::after {
	content: " \2212";
}

.cookie-dialog-welcome .cookie-disclaimer > p {
	color: var(--muted);
	font-size: 13px;
	line-height: 1.7;
	margin: 10px 0 0;
	max-height: 26vh;
	overflow-y: auto;
}

/* Two pills, the way the design draws its hero buttons. */
.cookie-dialog-welcome .cookie-actions {
	align-items: center;
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
	margin-top: 16px;
}

/*
 * Two pills. Three sets of rules have to be undone to get them: app.css pins
 * `.solid-button` to dark green, square corners and its own padding with `!important`
 * on each; `.cookie-dialog .solid-button` and `.cookie-dialog .arrow-link` above were
 * written for the centred dialog and make both buttons full width with a rule between
 * them; and `.solid-button` spaces its label and icon apart, which a text-only pill
 * does not want.
 */
.cookie-dialog-welcome .cookie-actions .solid-button,
.cookie-dialog-welcome .cookie-actions .arrow-link {
	border-bottom: 0;
	border-radius: 9999px !important;
	gap: 0;
	justify-content: center;
	margin-top: 0;
	min-height: 0;
	min-width: 172px;
	padding: 15px 30px !important;
	text-align: center;
	width: auto;
}

.cookie-dialog-welcome .cookie-actions .solid-button {
	background: var(--gold-background, #c69c6d) !important;
	border: 1px solid var(--gold-background, #c69c6d);
	color: var(--dark) !important;
}

.cookie-dialog-welcome .cookie-actions .arrow-link {
	border: 1px solid rgb(243 241 235 / 55%);
	color: var(--paper);
}

/*
 * The stray line under the decline button.
 *
 * `.arrow-link` draws its own underline as a full-width `:after`, which is right for
 * a text link and wrong inside a pill — it sat across the bottom of the button and
 * out past its corner. `border-bottom: 0` did nothing, because the line was never a
 * border. app.css switches it off the same way for `.form-bottom .arrow-link`.
 */
.cookie-dialog-welcome .cookie-actions .arrow-link::after {
	display: none;
}

/* The ask column sits with its text, not floating in the middle of the card. */
.cookie-dialog-welcome .cookie-ask {
	align-self: start;
}

.cookie-dialog-welcome .cookie-actions .arrow-link:hover,
.cookie-dialog-welcome .cookie-actions .arrow-link:focus-visible {
	border-color: var(--gold);
	color: var(--gold);
}

@media (min-width: 901px) {

	/*
	 * Two columns: what the site is and what it is asking, then the buttons. The
	 * disclaimer spans the full width underneath, so opening it pushes nothing
	 * sideways.
	 */
	.cookie-dialog-welcome {
		column-gap: 40px;
		display: grid;
		grid-template-columns: minmax(0, 1fr) auto;
	}

	/*
	 * Rows named as well as columns. The disclaimer sits between the other two in the
	 * markup, and auto-placement does not backtrack — so with only a column given, the
	 * ask was pushed to a third row below the disclaimer instead of sitting beside the
	 * welcome, and opening the disclaimer shoved it further down.
	 */
	.cookie-dialog-welcome .cookie-dialog-head {
		grid-column: 1 / -1;
		grid-row: 1;
	}

	.cookie-dialog-welcome .cookie-welcome {
		grid-column: 1;
		grid-row: 2;
	}

	.cookie-dialog-welcome .cookie-ask {
		grid-column: 2;
		grid-row: 2;
		max-width: 430px;
	}

	.cookie-dialog-welcome .cookie-disclaimer {
		grid-column: 1 / -1;
		grid-row: 3;
	}
}

@media (max-width: 900px) {

	.cookie-dialog-welcome {
		--dg-notice-gutter: 20px;
		overflow-y: auto;
		padding: 22px var(--dg-notice-gutter) !important;
		width: 100% !important;
	}

	.cookie-dialog-welcome .cookie-dialog-head {
		margin: -22px calc(var(--dg-notice-gutter) * -1) 16px;
		padding: 12px var(--dg-notice-gutter);
	}

	.cookie-dialog-welcome .cookie-dialog-logo {
		width: 112px;
	}

	.cookie-dialog-welcome .cookie-ask {
		margin-top: 14px;
	}
}

@media (max-width: 600px) {

	.cookie-dialog-welcome .cookie-actions .solid-button,
	.cookie-dialog-welcome .cookie-actions .arrow-link {
		width: 100% !important;
	}
}

/* --------------------------------------------------------------------------
 * The brand film
 *
 * Nothing like this exists in the source build, so every rule here is new — but the
 * vocabulary is the design's: the section uses `.section-padding` and `.eyebrow` like
 * every other band, and the dialog is drawn the way the cookie and lead dialogs are.
 * -------------------------------------------------------------------------- */

.brand-video-section {
	background: var(--paper);
}

.brand-video-header {
	margin-bottom: 36px;
	max-width: 720px;
}

.brand-video-heading {
	color: var(--ink);
	font-family: var(--serif);
	font-size: clamp(32px, 4vw, 54px);
	font-weight: 400;
	letter-spacing: -0.02em;
	line-height: 1.08;
	margin: 12px 0 0;
}

/* The italic gold second half, exactly as the other section headings set it. */
.brand-video-heading em {
	color: var(--color-brand-gold, #c69c6d);
	font-style: italic;
}

.brand-video-intro {
	color: var(--muted);
	line-height: 1.7;
	margin: 16px 0 0;
	max-width: 56ch;
}

/*
 * The cover is the button, so the whole picture is the target rather than the disc
 * drawn on it. `display: block` and the reset undo the UA button styling that would
 * otherwise centre and inset the image.
 */
/*
 * Full width, and the whole artwork — nothing cropped.
 *
 * It was briefly cropped to 2.1/1 to bring the height down, and the client asked for
 * the picture back. The artwork is 16/9 and its content runs from 3.7% (the top of
 * the hair) to 96% (the forearms), with the logo at 72-84%, so there is no band that
 * can come off without cutting something. Showing it whole at the full width means
 * the box is as tall as 16/9 makes it; the only way to have both would be to
 * letterbox it inside a shorter frame, which is a different look and the client's
 * call, not a silent one.
 */
.brand-video-cover {
	aspect-ratio: 16 / 9;
	background: var(--dark);
	border: 0;
	border-radius: 2px;
	cursor: pointer;
	display: block;
	margin: 0;
	overflow: hidden;
	padding: 0;
	position: relative;
	width: 100%;
}

.brand-video-image {
	display: block;
	height: 100%;
	object-fit: cover;
	transition: transform 0.9s cubic-bezier(0.22, 1, 0.36, 1);
	width: 100%;
}

.brand-video-cover:hover .brand-video-image,
.brand-video-cover:focus-visible .brand-video-image {
	transform: scale(1.03);
}

/*
 * A scrim under the play control. The cover is the client's own artwork and its
 * right-hand side is light, so the disc needs its own ground rather than relying on
 * whatever happens to be behind it — the same reasoning as the header's hero scrim.
 */
.brand-video-cover::after {
	background: radial-gradient(circle at 50% 50%, rgb(5 36 31 / 45%) 0%, rgb(5 36 31 / 0%) 55%);
	content: "";
	inset: 0;
	pointer-events: none;
	position: absolute;
}

.brand-video-play {
	align-items: center;
	background: var(--color-brand-gold, #c69c6d);
	border-radius: 50%;
	color: var(--dark);
	display: flex;
	height: 84px;
	justify-content: center;
	left: 50%;
	position: absolute;
	top: 50%;
	transform: translate(-50%, -50%);
	transition: transform 0.4s ease, background-color 0.4s ease;
	width: 84px;
	z-index: 1;
}

/*
 * Lucide draws its icons as outlines — `fill="none"` with a stroke — and this one is
 * asked for with no stroke at all, so without a fill the triangle is simply not
 * there. A CSS declaration beats the presentation attribute on the element, which is
 * why this works without touching the icon markup.
 *
 * And the triangle reads as centred only when it is nudged off centre.
 */
.brand-video-play svg {
	fill: currentColor;
	margin-left: 3px;
}

.brand-video-cover:hover .brand-video-play,
.brand-video-cover:focus-visible .brand-video-play {
	transform: translate(-50%, -50%) scale(1.08);
}

/*
 * There is no label drawn on the cover.
 *
 * It was there, in paper white at the foot of the picture, and the cover is the
 * client's own artwork whose left half is nearly white — so the words sat at about
 * 1.3:1 on their own background. axe reports nothing, because it cannot judge text
 * over an image, which is the same trap the header's coordinates line fell into. The
 * disc carries the meaning and `play_label` names the button for a screen reader.
 */

.brand-video-caption {
	color: var(--muted);
	font-size: 12px;
	letter-spacing: 0.4px;
	margin: 14px 0 0;
}

/* The dialog, drawn the way the site's other dialogs are. */
.video-dialog-backdrop {
	align-items: center;
	background: rgb(5 36 31 / 88%);
	display: flex;
	inset: 0;
	justify-content: center;
	padding: clamp(16px, 4vw, 56px);
	position: fixed;
	z-index: 1000;
}

.video-dialog-backdrop[hidden] {
	display: none;
}

.video-dialog {
	max-width: 1180px;
	position: relative;
	width: 100%;
}

/*
 * The frame keeps 16/9 whatever the window is, so the film is never letterboxed by
 * the dialog as well as by itself.
 */
.video-dialog-frame {
	aspect-ratio: 16 / 9;
	background: #000;
	overflow: hidden;
	width: 100%;
}

.video-dialog-frame iframe {
	border: 0;
	display: block;
	height: 100%;
	width: 100%;
}

/*
 * Above the frame on a wide screen and inside it on a narrow one, where there is no
 * margin left to sit in.
 */
.video-dialog-close {
	background: none;
	border: 0;
	color: var(--paper);
	cursor: pointer;
	display: grid;
	height: 40px;
	place-items: center;
	position: absolute;
	right: 0;
	top: -48px;
	transition: color 0.3s ease;
	width: 40px;
}

.video-dialog-close:hover,
.video-dialog-close:focus-visible {
	color: var(--color-brand-gold, #c69c6d);
}

@media (max-width: 900px) {

	.brand-video-play {
		height: 64px;
		width: 64px;
	}

	.brand-video-header {
		margin-bottom: 26px;
	}
}

@media (max-width: 600px) {

	.brand-video-play {
		height: 54px;
		width: 54px;
	}

	/*
	 * No room above the frame at this size, so the close moves inside it and takes a
	 * disc of its own to stay legible over the film's first frame.
	 */
	.video-dialog-close {
		background: rgb(5 36 31 / 70%);
		border-radius: 50%;
		right: 8px;
		top: 8px;
	}
}

/*
 * The footer credit on a phone.
 *
 * app.css pushes the middle span right with `margin-left: auto`, which suited the
 * design's own two-word tagline ("Places made for life.") at 5px. The tagline now
 * carries the agency credit, and the readable-type layer sets it at 11px, so it wraps
 * to a line of its own and then sits hard against the right gutter — reading as
 * though it has been cut off at the edge, which is how it was reported.
 *
 * On its own line it should start where everything else in the footer starts.
 */
@media (max-width: 600px) {

	.footer-bottom > span:nth-child(2) {
		margin-left: 0;
		width: 100%;
	}
}

/* --------------------------------------------------------------------------
 * The footer, made calmer
 *
 * Reported as crowded, unevenly aligned and inconsistent between groups. Measured,
 * the last part was literally true: the same 16px text ran at four different
 * leadings — the brand paragraph at 1.6, the address at 1.9, the contact lines and
 * the company links at 2.3 — so no two columns sat on the same rhythm.
 * -------------------------------------------------------------------------- */

/*
 * One leading for every line in the columns. 1.9 is the design's own value, already
 * on the address and the project names, so this brings the other two to it rather
 * than inventing a number.
 *
 * The brand paragraph keeps 1.6: it is running copy rather than a list, and a
 * sentence set at 1.9 reads as though the lines have come apart.
 */
.footer-details [data-testid="contact-email"],
.footer-details [data-testid="contact-phone"],
.footer-details > div > span,
.footer-details > div > a,
.footer-details > div > ul > li > a,
.footer-brand-col p {
	line-height: 1.5;
}

/*
 * 1.5, not the 1.9 this was first set to.
 *
 * 1.9 made the four columns agree with each other, which was the measured fault, but
 * it left each list airy: measured between "About" and "Leadership" the real gap was
 * 22px — 8px of margin app.css puts on every footer link, plus 14.4px of leading that
 * the line-height alone does not show. The leading comes down and the margin with it,
 * so the lists read as lists. The brand paragraph joins them rather than keeping its
 * own value, which is what "the elements at the top as well" was about.
 */
.footer-details > div > a,
.footer-details > div > ul > li > a {
	margin-bottom: 4px;
	margin-top: 4px;
}

/* More air between the groups than within them, which is what makes a list readable. */
.footer-details {
	row-gap: 40px;
}

@media (max-width: 900px) {

	.footer-details {
		row-gap: 36px;
	}
}

@media (max-width: 479px) {

	/*
	 * One column on the narrowest phones.
	 *
	 * Two columns leave 157px each at 390px wide, and "sales@omdivineworld.com" needs
	 * 196px at the readable size — so it broke at the `<wbr>` after the "@" and the
	 * address read as two separate lines. The break is correct behaviour for a column
	 * that narrow; the column was the problem. Full width, it sits on one line and the
	 * whole footer reads straight down.
	 */
	.footer-details {
		grid-template-columns: minmax(0, 1fr);
		row-gap: 32px;
	}
}

/* --------------------------------------------------------------------------
 * Project cards as decision cards
 *
 * The handoff's point: a visitor should see what a project is, what they can choose
 * and what it costs without opening it. The card carried a category tag, a list of
 * configuration names and one price, so comparing two projects meant opening both.
 *
 * Nothing here fights app.css — the card, its media panel, its price box and its
 * actions are the design's own. This adds the status badge, the comparison table and
 * the disclaimer, which the source build had no equivalent of.
 * -------------------------------------------------------------------------- */

.project-status-badge {
	background: var(--paper);
	border-radius: 2px;
	color: var(--dark);
	font-family: var(--sans);
	font-size: 10px;
	font-weight: 500;
	left: 14px;
	letter-spacing: 0.12em;
	padding: 6px 10px;
	position: absolute;
	text-transform: uppercase;
	top: 14px;
	z-index: 2;
}

/*
 * The badge carries its meaning in a word, and colour only reinforces it — a sold-out
 * card is not told apart by hue alone.
 */
.project-status-badge.project-status-ongoing {
	background: var(--color-brand-gold, #c69c6d);
	color: var(--dark);
}

.project-status-badge.project-status-pre-launch,
.project-status-badge.project-status-coming-soon {
	background: var(--dark);
	color: var(--paper);
}

.project-status-badge.project-status-sold-out {
	background: #6b7169;
	color: #fff;
}

/* The category tag sits top-left in the design, so the status takes the other corner. */
.featured-card-media .featured-badge ~ .project-status-badge,
.featured-card-media .project-status-badge {
	left: auto;
	right: 14px;
}

.project-config-table {
	border-collapse: collapse;
	font-family: var(--sans);
	margin: 14px 0 0;
	width: 100%;
}

.project-config-table caption {
	text-align: left;
}

.project-config-table th,
.project-config-table td {
	padding: 9px 0;
	text-align: left;
	vertical-align: top;
}

.project-config-table thead th {
	border-bottom: 1px solid var(--line, #e2dfd5);
	color: var(--muted);
	font-size: 11px;
	font-weight: 500;
	letter-spacing: 0.1em;
	padding-bottom: 7px;
	text-transform: uppercase;
}

.project-config-table tbody th {
	color: var(--ink);
	font-size: 14px;
	font-weight: 500;

	/*
	 * No `white-space: nowrap` here.
	 *
	 * It suited "3 BHK", and then Midtown Square arrived with "Corner showroom" —
	 * which in a fixed 20% column simply ran out across the size beside it, so the row
	 * read "Corner showroom1,452 sq ft". A label that is too long should wrap inside
	 * its own column, not overprint the next one.
	 *
	 * `break-word`, not `anywhere`: the latter broke "Showroom" into "Showro / om"
	 * mid-word. The column is 26% so the longest single word fits, and "Corner
	 * showroom" wraps at its space like ordinary text.
	 */
	overflow-wrap: break-word;
}

.project-config-table tbody td {
	color: var(--muted);
	font-size: 14px;
}

.project-config-table tbody tr + tr th,
.project-config-table tbody tr + tr td {
	border-top: 1px solid rgb(41 43 38 / 8%);
}

/* The price column is the one being compared, so it reads as the answer. */
.project-config-table tbody td:last-child {
	color: var(--ink);
	font-weight: 500;
	text-align: right;

	/*
	 * No `nowrap`. It was there to stop a figure breaking across lines, but a figure
	 * has no space to break at — "₹60.90L" stays whole on its own. What it actually
	 * did was force "On request" to run past its column at 1280, where the cell is
	 * only 66px wide.
	 */
	white-space: normal;
}

.config-availability {
	color: var(--muted);
	display: block;
	font-size: 11px;
	font-weight: 400;
	letter-spacing: 0.04em;
	text-transform: uppercase;

	/*
	 * The price column is `nowrap` so a figure never breaks across lines, and this
	 * note sits in the same cell and inherited it — "FIRST 20 BOOKINGS" then ran
	 * 41px wider than its own box at 1280. A figure must not wrap; a sentence under
	 * it should.
	 */
	white-space: normal;
}

.project-card-disclaimer {
	border-top: 1px solid rgb(41 43 38 / 8%);
	color: var(--muted);
	font-family: var(--sans);
	font-size: 11px;
	line-height: 1.6;
	margin: 16px 0 0;
	padding-top: 12px;
}

@media (max-width: 600px) {

	/*
	 * The handoff asks that a comparison table scroll sideways rather than squeeze.
	 * These three columns fit a phone, so there is nothing to scroll — but the size
	 * column is the first thing worth giving up if a longer label arrives.
	 */
	.project-config-table th,
	.project-config-table td {
		padding: 8px 0;
	}

	.project-config-table tbody th,
	.project-config-table tbody td {
		font-size: 13px;
	}
}

/* --------------------------------------------------------------------------
 * Hero legibility over a photograph
 *
 * Measured, not judged by eye: axe cannot assess text over an image, so the
 * rendered pixels behind the copy were sampled on every slide. The *mean* was a
 * comfortable 8.9–10:1 — which is why this looked fine — but the brightest patch in
 * that band was 0.70 luminance on all five, putting white text at 2.1:1 against a
 * sunlit sky or a pale wall. That is the "content is hard to read" report, and it
 * was never only the later slides; the first one simply has its bright patch
 * somewhere the words do not fall.
 *
 * White text needs the background under 0.46 luminance to clear 4.5:1, so the band
 * the copy occupies gets roughly 40% more coverage. It fades out above the copy so
 * the photograph is still a photograph — the top two thirds are untouched.
 * -------------------------------------------------------------------------- */

.detail-hero-shade::after {
	background: linear-gradient(
		0deg,
		/*
		 * Lighter at the foot than in the middle, which is the opposite of the usual
		 * shape and is deliberate. The buttons sit in the bottom fifth and are mostly
		 * transparent — "Enquire Now" has no fill at all — so they show whatever the
		 * scrim is doing, and at the gradient's old peak they read as black boxes. The
		 * cover is concentrated on the title band instead, which is the only part that
		 * actually needs it.
		 */
		rgb(8 22 18 / 24%) 0%,
		rgb(8 22 18 / 30%) 20%,
		rgb(8 22 18 / 56%) 29%,
		rgb(8 22 18 / 72%) 36%,
		rgb(8 22 18 / 54%) 48%,
		rgb(8 22 18 / 12%) 60%,
		rgb(8 22 18 / 0%) 68%
	);
	content: "";
	inset: 0;
	pointer-events: none;
	position: absolute;
}

/*
 * The home hero gets an adaptive scrim instead of a fixed one.
 *
 * A flat gradient strong enough for the four bright slides was too much for the
 * first, which the design had already shot dark — the client's point exactly.
 * `brightness()` multiplies what is behind it, so a dark frame loses little and a
 * sunlit one is pulled down hard. One rule, right for all five.
 *
 * It covers the copy only and is masked at the top edge so it does not end in a
 * visible line across the photograph.
 */
.hero-copy {
	isolation: isolate;
}

.hero-copy::before {
	background: linear-gradient(to top, rgb(8 22 18 / 62%) 0, rgb(8 22 18 / 46%) 55%, rgb(8 22 18 / 0%) 100%);
	content: "";

	/*
	 * Full bleed sideways and faded at both ends.
	 *
	 * This first masked only its top edge, which left the other three drawn across
	 * the photograph as a visible rectangle — the box reported on the hero. Running
	 * it to the viewport edges removes the two vertical seams outright, because there
	 * is no edge left inside the picture, and the mask then only has the top and
	 * bottom to soften.
	 */
	inset: -40px -50vw -80px;
	mask-image: linear-gradient(
		0deg,
		rgb(0 0 0 / 0%) 0%,
		#000 18%,
		#000 68%,
		rgb(0 0 0 / 0%) 100%
	);
	pointer-events: none;
	position: absolute;
	z-index: -1;
}

/*
 * No second gradient on the project banner.
 *
 * One was added here, reaching 93% at the base, and it stacked on both the gradient
 * above *and* the adaptive filter below — three darkening layers, which is why the
 * photograph stopped being visible at all. The adaptive filter is the one that
 * belongs: it measures nothing but scales what is actually behind the text, so it
 * covers a bright sky without flattening a dark one. The gradients stay only as the
 * fallback for browsers without `backdrop-filter`.
 */

/*
 * A card with no configuration data should not show a hole.
 *
 * app.css gives `.featured-price-box` `margin-top: auto`, which pins it to the foot
 * of the card — right when every card holds about the same amount. The comparison
 * table made them very uneven, so a project with no sizes or prices recorded got all
 * the slack in the middle: its configuration names at the top, then an empty band,
 * then the price box. That is the "looks odd" report.
 *
 * The slack moves to above the actions, where a short card normally carries it, so
 * the content reads as one block from the top and the buttons still line up across
 * the row.
 */
.featured-card.has-no-config-table .featured-price-box {
	margin-top: 0;
}

.featured-card.has-no-config-table .featured-actions {
	margin-top: auto;
}

/*
 * The price on a project banner, and the scroll cue that sat on a button.
 *
 * `.hero-meta-price` is the design's gold on a photograph. Measured against the
 * brightest patch behind it that is 1.65:1 — gold simply cannot reach 4.5 over a
 * mid-tone, whatever the scrim, because gold's own luminance is only 0.31. The
 * starting price is the single most useful line on the banner, so it takes the same
 * near-white as the rest of the hero copy, which clears with the scrim above. Gold
 * stays everywhere it sits on paper or on dark green.
 *
 * The scroll cue is hidden on a phone: measured at 390px it occupied y726-742 inside
 * a WhatsApp button spanning y699-755, so it was drawn on top of a control. The home
 * hero's cue is already hidden there at the client's request; this matches it.
 */
.detail-hero .hero-meta-price {
	color: var(--paper);
}

@media (max-width: 900px) {

	.detail-hero .detail-scroll {
		display: none;
	}
}

/*
 * A gradient, not `backdrop-filter`.
 *
 * The filter was the better idea — it scales what is actually behind the text, so a
 * dark photograph is barely touched — but it cannot be verified here: headless
 * Chrome runs with `--disable-gpu` and `backdrop-filter` needs compositing, so it is
 * a no-op in every capture. brightness(0.02) measured identically to brightness(0.58).
 *
 * That is worse than useless. On a real screen the filter *did* apply, stacking on
 * top of gradients that had been thickened to make the numbers work without it —
 * which is exactly why the banner came back as too dark to see the photograph. A
 * plain gradient is weaker in principle and honest in practice: what the tools
 * measure is what a visitor gets.
 */

/*
 * (Formerly an adaptive scrim.)
 *
 * A fixed gradient cannot: tuned until the darker banners passed, the brightest two
 * were still at 3.5:1 and 4.5:1, and tuning further for them turned the dark ones
 * into near-black slabs. `backdrop-filter: brightness()` darkens whatever is actually
 * there by a proportion, so a sunlit sky is pulled down hard and an already-dark
 * frame is barely touched — which is the property a fixed overlay cannot have.
 *
 * It sits behind the copy only, masked so the top edge fades out rather than ending
 * in a visible line. Where `backdrop-filter` is unsupported the gradients above are
 * still in place, which is why those were tuned first rather than replaced.
 */
/*
 * No `isolation: isolate` here.
 *
 * It was added so the scrim's `z-index: -1` would stay inside this element — but
 * isolating creates a stacking context, and `backdrop-filter` can only sample what
 * is *behind* its own stacking context. Isolated, the filter had an empty backdrop
 * and did nothing at all: darkening it from 0.58 to 0.30 moved the measured patch
 * by one thousandth. `.detail-hero` already isolates, which is what keeps the scrim
 * from falling behind the photograph.
 */

/*
 * No scrim panel behind the project banner's copy.
 *
 * One was here, and raising its opacity from 74% to 90% moved the measured patch by
 * 0.026 — it was never covering the thing that needed covering. The cover belongs on
 * `.detail-hero-shade`, which app.css already provides for this: full size, behind
 * the copy, no mask and no stacking tricks to get wrong.
 */

/*
 * Enough leading that a descender clears the line below it.
 *
 * app.css sets the hero heading at `line-height: .88`. Cormorant's descender runs
 * about .28em below the baseline and its cap height is about .66em, so at .88 the
 * next line's capitals start .06em *above* where the previous line's "g" ends — which
 * is why "Living," and "Made Smarter." had their g and S touching. .95 is the first
 * value that clears it, and it is still tighter than the 1.09 the phone was
 * effectively running before any of this.
 */
.hero-heading {
	line-height: 0.95;
}

/*
 * The hero sub-copy wraps to two lines, as the design has it — by running out of
 * room rather than by a newline typed into the field.
 *
 * It was two lines because the copy carried hard breaks, which is why a longer
 * sentence broke in an odd place; taking them out left it as one long line on a
 * desktop. A measured max-width gives the design's shape back and still lets any
 * length the client types wrap sensibly.
 */
.hero-description p {
	max-width: 46ch;

	/*
	 * Balanced wrapping, so the last line is not a stranded word or two. On a phone
	 * the two longest descriptions still need three lines at the 16px reading size —
	 * shrinking the type to force two would undo the accessible size the client asked
	 * for — but they break evenly rather than leaving a gap.
	 */
	text-wrap: pretty;
}

/* --------------------------------------------------------------------------
 * Project cards, aligned row by row
 *
 * The cards are equal height, but their insides were not: a project with a short
 * name put its price box where the next card had its heading, and the comparison
 * tables started at four different heights. Reading across the row — which is the
 * whole point of a comparison — meant the eye had to find each value again.
 *
 * Subgrid is what fixes this properly: the card and its body both take their rows
 * from the grid, so every card's media, heading, price, table, actions and
 * disclaimer sit on the same six lines. Spacer rules cannot do it, because the
 * heights depend on content that differs per card.
 *
 * Wrapped in @supports: without subgrid the cards keep app.css's flex layout, which
 * is what they had before and still works — it simply does not align.
 * -------------------------------------------------------------------------- */

@supports (grid-template-rows: subgrid) {

	.featured-cards-grid {
		/* media · heading · price · configurations · actions · disclaimer */
		grid-template-rows: repeat(6, auto);
	}

	.featured-cards-grid .featured-card {
		display: grid;
		grid-row: span 6;
		grid-template-rows: subgrid;
	}

	.featured-cards-grid .featured-card-body {
		display: grid;
		grid-row: span 5;
		grid-template-rows: subgrid;
	}

	/*
	 * The price box no longer needs pushing to the foot: the rows hold it in place,
	 * and `margin-top: auto` inside a subgrid row would drop it to the bottom of its
	 * own row instead of aligning with its neighbours.
	 */
	.featured-cards-grid .featured-price-box,
	.featured-cards-grid .has-no-config-table .featured-price-box {
		align-self: start;
		margin-top: 0;
	}

	.featured-cards-grid .has-no-config-table .featured-actions {
		margin-top: 0;
	}

	/* Each section sits at the top of its row so the first lines line up. */
	.featured-cards-grid .project-config-table,
	.featured-cards-grid .featured-actions,
	.featured-cards-grid .project-card-disclaimer {
		align-self: start;
	}
}

/*
 * The project banner's cover is sized to where its own title actually sits, and that
 * differs sharply between a desktop and a phone.
 *
 * Measured against the hero's height, the title occupies 29-44% up from the bottom on
 * a desktop and 46-53% on a phone, because the copy block is far taller there. One
 * gradient for both was tuned on the phone and then applied everywhere, so a desktop
 * banner was covered to 78% opacity across its whole frame — which is why the
 * photograph went black. Each now stops just above its own title.
 */
@media (max-width: 900px) {

	.detail-hero-shade::after {
		background: linear-gradient(
			0deg,
			/* Same shape as above: the buttons occupy the bottom third here. */
			rgb(8 22 18 / 26%) 0%,
			rgb(8 22 18 / 34%) 26%,
			rgb(8 22 18 / 62%) 38%,
			rgb(8 22 18 / 78%) 46%,
			rgb(8 22 18 / 66%) 58%,
			rgb(8 22 18 / 18%) 74%,
			rgb(8 22 18 / 0%) 84%
		);
	}
}

/* --------------------------------------------------------------------------
 * The closing contact block, and the footer columns, on a phone
 * -------------------------------------------------------------------------- */

@media (max-width: 600px) {

	/*
	 * The heading sat 40px above the line it introduces, which on a phone reads as a
	 * break rather than as a pair.
	 */
	.contact-main > p:first-of-type {
		margin-top: -18px;
	}

	/*
	 * The four ways to get in touch, two up instead of four down.
	 *
	 * As a single column they were four full-width rows at 24px leading with 14px
	 * between them — roughly 230px of a phone screen for what is a short menu. Two
	 * columns halve that, and the leading comes in to match.
	 */
	.contact-actions {
		column-gap: 20px;
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		row-gap: 10px;
	}

	.contact-actions > * {
		font-size: 14px;
		line-height: 1.35;
	}
}

@media (max-width: 479px) {

	/*
	 * Two columns again, but only for the two short ones.
	 *
	 * The grid was dropped to a single column so "sales@omdivineworld.com" would stop
	 * breaking mid-address — it needs 196px and two columns leave 157px. That is true
	 * of the contact block alone, so it spans the full width and the other two sit
	 * side by side, which fills the empty half that was left beside "Rooted here".
	 */
	.footer-details {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}

	.footer-details > .footer-brand-col,
	.footer-details > div:nth-child(2) {
		grid-column: 1 / -1;
	}
}

/* --------------------------------------------------------------------------
 * Project card tables: same columns in every card, and less air around them
 * -------------------------------------------------------------------------- */

/*
 * `table-layout: fixed` so the columns are the same in all four cards.
 *
 * Left to size themselves, each table measured its own content: a project with no
 * sizes recorded shows "—", so its SIZE column collapsed and FROM landed 93px left
 * of where the next card put it. Reading a row across the grid — the entire point of
 * a comparison — meant finding the price column again in each card.
 */
.project-config-table {
	table-layout: fixed;
}

/*
 * The widths are set by what the columns actually hold, not by round numbers: "3 BHK"
 * is short, "65 Sq. Yards · 585 Sq. Ft." is the longest thing in the table, and the
 * price is "On request" at its widest. A first pass at 22/46/32 aligned the columns
 * and made every size wrap onto two lines, which is worse than the misalignment.
 */
.project-config-table th:nth-child(1),
.project-config-table td:nth-child(1) {
	width: 26%;
}

.project-config-table th:nth-child(2),
.project-config-table td:nth-child(2) {
	width: 44%;
}

.project-config-table th:nth-child(3),
.project-config-table td:nth-child(3) {
	width: 30%;
}

/*
 * Columns need their own gutter once the table is `fixed`. Sized to content, the
 * browser leaves space between columns; fixed, it does not, and at 1280 the type ran
 * straight into the size — "1 BHK65 Sq. Yards".
 */
.project-config-table th:nth-child(1),
.project-config-table td:nth-child(1),
.project-config-table th:nth-child(2),
.project-config-table td:nth-child(2) {
	padding-right: 10px;
}

/* The size is the longest value and the least load-bearing; it takes the smaller size. */
.project-config-table td:nth-child(2) {
	font-size: 13px;
}

/* The card was loose at three points; each comes in without crowding the type. */
.featured-cards-grid .featured-card-body {
	padding-top: 16px;
}

.featured-cards-grid .project-config-table {
	margin-top: 6px;
}

.featured-cards-grid .project-config-table th,
.featured-cards-grid .project-config-table td {
	padding: 7px 0;
}

.featured-cards-grid .featured-actions {
	padding-top: 10px;
}

.featured-cards-grid .featured-price-box {
	margin-bottom: 12px;
}

/*
 * The price column's header sits over its own values.
 *
 * The values are right-aligned — a column of figures is read down its last digit —
 * but the header was left-aligned with the rest, so "FROM" sat at the left edge of a
 * column whose numbers were at the right, about 85px away. The columns themselves
 * matched across all four cards; the label simply was not above the thing it labels.
 */
.project-config-table thead th:nth-child(3) {
	text-align: right;
}

/* --------------------------------------------------------------------------
 * The project banner on a phone
 *
 * Desktop is deliberately untouched — every rule here is inside the phone query.
 *
 * The banner read as full: the title ran at 16vw (62px) over two lines, the three
 * buttons were 54-56px each at full width, and the whole block sat 55px off the foot
 * where the home hero leaves 130. So it filled the frame top to bottom with no room
 * to breathe and no family resemblance to the home page.
 * -------------------------------------------------------------------------- */

@media (max-width: 900px) {

	/*
	 * The same standard spacing the home hero uses, so the two pages open the same
	 * way. The space this frees goes to the top of the frame, which is what was
	 * missing.
	 */
	/*
	 * The banner fills the screen on a phone, as the home hero does.
	 *
	 * app.css gives it 90svh, which stops 84px short of the bottom of a 844px screen.
	 * `bottom: 130px` is measured from the hero's own foot, so the copy ended up 214px
	 * above the viewport bottom here against the home page's 130 — the same CSS value
	 * and a visibly different result. Matching the height is what makes the two pages
	 * open alike, rather than tuning a second number to compensate for the first.
	 */
	.detail-hero {
		height: 100svh;
		max-height: none;
	}

	.detail-hero-copy {
		bottom: 130px;
	}

	/*
	 * 12.5vw rather than 16, and the leading in to match the home hero's 0.88. At
	 * 62px a two-word project name filled the width; at 48px it sits as a heading
	 * rather than a wall.
	 */
	.detail-hero-copy h1 {
		font-size: 12.5vw;
		letter-spacing: -0.03em;
		line-height: 0.88;
	}

	/*
	 * The three calls to action, lighter. 54px tall at 16px each is a lot of furniture
	 * for a phone; they keep their full width and their order, and lose the bulk.
	 */
	.detail-hero-copy .light-button,
	.detail-hero-copy .detail-btn-secondary,
	.detail-hero-copy .detail-btn-whatsapp {
		font-size: 14px;
		letter-spacing: 0.08em;
		padding-bottom: 11px;
		padding-top: 11px;
	}

	/*
	 * `.light-button` needs its own line: app.css pins it with
	 * `padding: 14px 18px !important` and a 50px min-height, so without matching both
	 * it stayed 51px tall while the two beside it came down to 45 — worse than leaving
	 * all three alone.
	 */
	.detail-hero-copy .light-button {
		min-height: 0;
		padding-bottom: 11px !important;
		padding-top: 11px !important;
	}
}

/* --------------------------------------------------------------------------
 * Project status, on the listing row and the project's own banner
 *
 * The card has carried it since the handoff; the list and the detail page did not,
 * so a delivered project and one taking pre-launch bookings read identically until
 * you opened them. Both now say so, in the same words, from one helper.
 * -------------------------------------------------------------------------- */

/*
 * A filled pill, not a hairline outline.
 *
 * It was 9px in the same grey as the category beside it, so it read as more meta
 * rather than as the answer to "can I still buy this" — which is the one question the
 * row exists to answer at a glance. Filled, a size up, and each status its own
 * ground: gold for pre-launch, the brand green for ongoing, a quiet stone for
 * delivered.
 *
 * Every pair is measured against its own background, because this sits on paper
 * where the design's gold is only 2.2:1 as text:
 *   pre-launch   #054239 on #c69c6d   4.90:1
 *   ongoing      #f3f1eb on #054239   9.99:1
 *   completed    #3f423c on #dcd9cf   7.01:1
 *
 * The word is always there, so colour never carries the meaning alone.
 */
.directory-status {
	background: #dcd9cf;
	border-radius: 2px;
	color: #3f423c;
	font-size: 10px;
	font-style: normal;
	font-weight: 600;
	letter-spacing: 0.14em;
	margin-left: 14px;
	padding: 4px 10px;
	text-transform: uppercase;
	white-space: nowrap;
}

.directory-status-pre_launch {
	background: #c69c6d;
	color: #054239;
}

.directory-status-ongoing {
	background: #054239;
	color: #f3f1eb;
}

/*
 * Colour reinforces the word; it never carries the meaning on its own.
 *
 * #8f6638, not the brand gold. This was `var(--color-brand-gold)` and axe caught it
 * on `/projects/` at both widths — the design's gold is 2.2:1 on paper, which is the
 * trap this file already carries a measured colour layer for, and I walked into it
 * anyway. #8f6638 is that layer's value for small gold text on paper: 4.51:1.
 *
 * The banner's status takes no colour at all. It sits on a photograph, where axe
 * cannot judge contrast and would have reported nothing either way, so it inherits
 * the near-white the rest of the hero copy uses and is measured with it.
 */
/*
 * (The pre-launch colour now comes from the pill rules above. This was gold text on
 * paper, which axe failed at 2.2:1 — the trap the measured colour layer exists for.)
 */

/*
 * The banner's status, as solid as the one on the listing.
 *
 * It was a translucent panel behind a hairline border, which on a photograph is
 * barely a shape at all — and it had to survive whatever the hero image happens to be
 * doing behind it. A solid fill puts the contrast between the pill's own two colours,
 * so it no longer depends on the picture, and it matches the listing so the same
 * project says the same thing in the same way in both places.
 *
 * Same measured pairs as the listing pills:
 *   pre-launch   #054239 on #c69c6d   4.90:1
 *   ongoing      #f3f1eb on #054239   9.99:1
 *   completed    #3f423c on #dcd9cf   7.01:1
 */
.hero-meta-status {
	background: #dcd9cf;
	border-radius: 2px;
	color: #3f423c;
	font-family: var(--sans);
	font-size: 11px;
	font-weight: 600;
	letter-spacing: 0.16em;
	margin-right: 10px;
	padding: 6px 12px;
	text-transform: uppercase;
	white-space: nowrap;
}

.hero-meta-status-pre_launch {
	background: #c69c6d;
	color: #054239;
}

.hero-meta-status-ongoing {
	background: #054239;
	color: #f3f1eb;

	/*
	 * The one case that needs an edge: brand green on a dark hero can sit on near
	 * identical pixels, so the pill would lose its shape. A thin paper rule keeps it
	 * a pill without lightening the fill.
	 */
	box-shadow: inset 0 0 0 1px rgb(243 241 235 / 45%);
}

@media (max-width: 600px) {

	.hero-meta-status {
		font-size: 10px;
		padding: 5px 10px;
	}
}

@media (max-width: 600px) {

	.directory-status {
		display: inline-block;
		margin-left: 0;
		margin-top: 6px;
	}
}

/*
 * The closing contact block's own rule, removed.
 *
 * `.contact-main` carries a `border-bottom`, and `.footer-social-row` directly below
 * carries one too — so the page drew a line, then "STAY CONNECTED", then another
 * line, at every width. The client marked the first as the odd one.
 *
 * `border-bottom-color: transparent` rather than `border-bottom: 0`: the border still
 * occupies its 1px, so the spacing below the links is exactly what it was. The
 * padding that sets that gap is 50px on a desktop and 32px on a phone, declared in
 * app.css, so removing the border outright would mean re-stating both just to buy
 * back a single pixel.
 */
.contact-main {
	border-bottom-color: transparent;

	/*
	 * Its padding-bottom goes with the rule. It was the space *above* the line; the
	 * social row's own padding-top is the space below it. With no line between them
	 * the two stacked into one gap of 68px on a phone and 87px on a desktop, against
	 * 34px below the label — which is the asymmetry the client marked. The row owns
	 * the whole gap now, so it stays symmetric at both widths without a breakpoint.
	 */
	padding-bottom: 0;
}

/* --------------------------------------------------------------------------
 * Section headings on a phone
 *
 * `page-hero.php` writes a `<br>` between the two heading lines, which is the
 * design's own two-line shape and right on a desktop. On a phone each of those
 * lines then wraps again inside itself, and "A developer with a" stranded its
 * last word: "A developer with" / "a" / "long-term view.".
 *
 * `text-wrap: balance` is the fix rather than a smaller size or a non-breaking
 * space, because it evens the lines the browser already chose and leaves the
 * forced break alone — the heading becomes "A developer" / "with a" /
 * "long-term view." with no change to the type. A browser without it wraps as
 * before, so nothing depends on it.
 *
 * Phone only. The desktop headings are the design's own and already measured;
 * `.hero-heading` is excluded by selector rather than by width, since the hero's
 * line count is asserted in `09-interaction` and is not the browser's to balance.
 * -------------------------------------------------------------------------- */

@media (max-width: 600px) {

	.display-heading {
		text-wrap: balance;
	}
}

/* --------------------------------------------------------------------------
 * Phone type: the section heading, and the copy under it
 *
 * app.css sets `.display-heading` to a flat **13vw** below 600px with no ceiling,
 * so the heading grows without limit across the range: 41.6px at 320 and 77.9px at
 * 599, where the same heading one pixel wider is 42.07px. That cliff is the design's,
 * and the wide end of it is what pushed "A developer with a" onto three lines with a
 * word to itself.
 *
 * `min(10.5vw, 44px)` keeps the design's fluid behaviour at the narrow end, where the
 * heading has to shrink to fit, and stops it at 44px — which also lands next to the
 * 42.07px the design itself uses at 601px, so the two sides of the breakpoint finally
 * meet instead of halving.
 *
 * Site-wide rather than scoped to the About page, because the 13vw is site-wide: the
 * same rule sizes every section heading on every page at this width, and a fix
 * conditioned on one page would only move the problem to the next one.
 * -------------------------------------------------------------------------- */

@media (max-width: 600px) {

	.display-heading {
		font-size: min(10.5vw, 44px);
	}

	/*
	 * The eyebrow's 32px below it was set against a heading half this size. Against
	 * the heading's own 44px it reads as a gap rather than as a label introducing
	 * something, so it comes back to 16px — the heading's own leading is what
	 * separates them now.
	 */
	.about-hero .eyebrow {
		margin-bottom: 16px;
	}

	/*
	 * The hero lead is 27px of serif on a phone, which put six lines of it between
	 * the heading and the first button. 20px keeps it clearly the lead paragraph
	 * — the reading copy below it is 16px — and brings it to four.
	 */
	.about-hero .story-lead {
		font-size: 20px;
		line-height: 1.45;
	}

	/*
	 * The paragraphs in the closing panel and under a section heading stay at the
	 * 16px reading size; it is the leading that reads as loose on a narrow column,
	 * at 1.8 and 1.9. 1.55 is the same rhythm the 20px lead above uses.
	 */
	.about-cta-box .cta-description,
	.section-side-copy {
		line-height: 1.55;
	}
}

/* --------------------------------------------------------------------------
 * The closing panel's four actions on a phone
 *
 * Two of them are `.arrow-link`s rather than buttons — the design's inline text
 * links, which sit in a row beside the buttons on a desktop and are right there.
 * Stacked on a phone they are four full-width rows that disagree with each other:
 * 12px uppercase with 1.2px of tracking against 16px sentence case, a border
 * against a 1px underline rule, and — the part that reads as broken — the two
 * buttons centring their label while "Explore projects" keeps `justify-content:
 * normal` and pushes its arrow to the far edge.
 *
 * So on a phone the two links take the ghost button's shape. The WhatsApp one keeps
 * the design's own #25d366 for its border and label so the affordance is still
 * recognisable (5.74:1 on the panel's #054239); the other takes the paper outline
 * the button above it uses.
 *
 * Phone only — the desktop row is the design's and is left alone.
 * -------------------------------------------------------------------------- */

@media (max-width: 600px) {

	.about-cta-buttons .arrow-link {
		align-items: center;
		border: 1px solid rgb(243 241 235 / 85%);
		font-size: 12px;
		gap: 8px;
		justify-content: center;
		letter-spacing: 1.2px;
		padding: 14px 18px;
		text-transform: uppercase;
	}

	/* The underline rule is the inline link's; a bordered button does not need it. */
	.about-cta-buttons .arrow-link::after {
		display: none;
	}

	.about-cta-buttons .about-whatsapp-link {
		border-color: #25d366;
		margin-left: 0;
	}

	.about-cta-buttons .arrow-link:hover,
	.about-cta-buttons .arrow-link:focus-visible {
		background: var(--paper);
		border-color: var(--paper);
		color: var(--dark);
	}

	.about-cta-buttons .about-whatsapp-link:hover,
	.about-cta-buttons .about-whatsapp-link:focus-visible {
		background: #25d366;
		border-color: #25d366;
		color: var(--dark);
	}
}

/* --------------------------------------------------------------------------
 * The hero's depth, driven by the scroll itself
 *
 * `app.js` wrote these transforms from a rAF callback, which is the main thread.
 * A phone scrolls on the compositor, so the layers arrived a frame late and the
 * hero drifted against the finger instead of tracking it. Frame timing showed
 * nothing — 16.9ms mean, 17.0 at p95 — because nothing was slow; what was wrong
 * was the relationship between the scroll and the transform, and only one of the
 * two was on the compositor.
 *
 * A scroll-driven animation puts both there. The numbers are the source's, the
 * same ones the JavaScript branch still uses where this is not supported:
 * `Hero.jsx` travels the photograph 24% of its own height and the copy 40%, and
 * fades the copy out over the first 80% of the range.
 *
 * The range is the hero's own box, handed over as two custom properties by
 * `app.js` on load and on resize — never per frame. `100vh` would have been close
 * enough on a desktop and wrong on a phone, where it means the URL-bar-hidden
 * height while the hero is laid out against the visible one.
 *
 * `animation-duration: auto` is what maps an animation across a timeline's range;
 * the `animation` shorthand is not used because it resets `animation-timeline`.
 * -------------------------------------------------------------------------- */

@keyframes dg-hero-image-parallax {

	from {
		transform: translateY(0);
	}

	to {
		transform: translateY(24%);
	}
}

@keyframes dg-hero-copy-parallax {

	from {
		opacity: 1;
		transform: translateY(0);
	}

	80% {
		opacity: 0;
	}

	to {
		opacity: 0;
		transform: translateY(40%);
	}
}

@keyframes dg-scroll-progress {

	from {
		transform: scaleX(0);
	}

	to {
		transform: scaleX(1);
	}
}

/*
 * The bar is outside the reduced-motion guard on purpose: it reports where you are
 * on the page rather than animating anything you did not ask for, which is the same
 * reason the JavaScript branch kept it when it dropped the parallax.
 */
.has-scroll-parallax [data-scroll-progress] {
	animation-duration: auto;
	animation-fill-mode: both;
	animation-name: dg-scroll-progress;
	animation-timing-function: linear;
	animation-timeline: scroll(root block);
}

@media (prefers-reduced-motion: no-preference) {

	.has-scroll-parallax .hero-image {
		animation-duration: auto;
		animation-fill-mode: both;
		animation-name: dg-hero-image-parallax;
		animation-timing-function: linear;
		animation-timeline: scroll(root block);
		animation-range: var(--hero-parallax-start, 0) var(--hero-parallax-end, 100vh);
	}

	.has-scroll-parallax .hero-copy {
		animation-duration: auto;
		animation-fill-mode: both;
		animation-name: dg-hero-copy-parallax;
		animation-timing-function: linear;
		animation-timeline: scroll(root block);
		animation-range: var(--hero-parallax-start, 0) var(--hero-parallax-end, 100vh);
	}
}

/* --------------------------------------------------------------------------
 * The contact discs move out of the chat widget's corner
 *
 * `.desktop-contact-actions` is `fixed; bottom: 30px; right: 3%` — the design put
 * the call and WhatsApp discs there because nothing else was. The chat widget claims
 * the same corner.
 *
 * Lifting them above it was tried first and is not enough, because the widget is not
 * one size. Collapsed it is a ~50px disc; with its greeting bubble open it is roughly
 * 380x200; with the conversation open it is a panel several hundred pixels tall. A
 * clearance that clears the disc sits under the bubble, and nothing clears the panel.
 * Any value in that corner is a guess against a box that changes by itself.
 *
 * So they move to the opposite corner, which is the only position that is right in
 * all three of those states. Bottom-left is free: `.floating-nav` is centred, and the
 * chapter dots run down the right edge at mid-height.
 *
 * `right: auto` is not optional — a fixed element with `width: auto` and both offsets
 * set stretches between them instead of moving, so the row would span the viewport.
 *
 * Desktop only: this row is not rendered on a phone, where `.mobile-actions` owns the
 * bottom of the screen. The widget overlaps *that* bar on a phone, which is a
 * separate problem and needs the widget's own offset changing.
 * -------------------------------------------------------------------------- */

.desktop-contact-actions {
	left: 3%;
	right: auto;
}

/* --------------------------------------------------------------------------
 * A third "what we build" card
 *
 * app.css sizes this grid `repeat(2, 1fr)`, which was right while the portfolio had
 * Residential and Land in it. Commercial makes three, and the third sat alone on a
 * second row at half width — the same stranded tile the mock-house grid was built to
 * avoid.
 *
 * Scoped with `:has()` to the three-card case, so a page still carrying two is the
 * design's own layout untouched, and a browser without `:has()` simply keeps it.
 *
 * 1000px is measured, not chosen: at 960 the three columns are 263px and "1 Delivered
 * Enclave" wraps to a second line in one card and not the others, so the three card
 * headers stop lining up. Between there and app.css's own 900px break they stack,
 * which is what the design does 60px further down anyway.
 * -------------------------------------------------------------------------- */

@media (min-width: 1001px) {

	.portfolio-categories-grid:has(> :nth-child(3)) {
		grid-template-columns: repeat(3, minmax(0, 1fr));
	}
}

@media (min-width: 901px) and (max-width: 1000px) {

	.portfolio-categories-grid:has(> :nth-child(3)) {
		grid-template-columns: minmax(0, 1fr);
	}
}

/* --------------------------------------------------------------------------
 * The chat widget clears the phone action bar
 *
 * `.mobile-actions` is the full width of the bottom of the screen on a phone, so
 * there is no corner for the widget to move to — it has to come up. Measured at
 * 390x844: the bar is 65px tall starting at y779, and the widget's launcher sat at
 * y768..820, straight across `Book Visit`.
 *
 * `.engt-wrapper` is the widget's own anchor — `fixed; bottom: 0; right: 0`, zero by
 * zero, with the launcher, the greeting callout and the unread badge absolutely
 * placed inside it. Shifting that one element carries all three and keeps the offsets
 * the vendor chose between them; overriding each `bottom` separately would re-space
 * the widget's own parts and break again whenever the vendor retunes them.
 *
 * 53px, not a round number: the launcher sits 24px up inside the wrapper, the bar is
 * 65px, and 12px of air reads as deliberate — 65 + 12 - 24. `!important` because the
 * widget injects its stylesheet at runtime, after this file.
 *
 * Phone only. On a desktop the widget keeps its corner, and the site's own discs moved
 * to the opposite one.
 * -------------------------------------------------------------------------- */

@media (max-width: 900px) {

	.engt-wrapper {
		bottom: 53px !important;
	}
}

/* --------------------------------------------------------------------------
 * Pictures and tables inside an article
 *
 * The live articles carry both, and the `body` repeater had nowhere to put them, so
 * `tools/import-article-media.php` puts them back into the row they followed. app.css
 * has no rules for either, and its preflight resets `border` to 0 on everything — the
 * same trap the legal pages' list markers hit — so a migrated table arrived as a grid
 * of sentences with no rules and 1px of padding.
 *
 * The values are the design's own: `--line` for the rules, the paper one shade darker
 * for the header row (the shade `.project-empty` uses), and the body's reading size.
 * -------------------------------------------------------------------------- */

.article-part-paragraphs img {
	display: block;
	height: auto;
	margin: 28px 0;
	max-width: 100%;
}

.article-table {
	margin: 28px 0;

	/*
	 * A long table scrolls rather than pushing the article sideways — the failure that
	 * reports no page overflow because an ancestor clips it, so it has to be designed
	 * for rather than detected.
	 */
	overflow-x: auto;
}

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

.article-table th,
.article-table td {
	border: 1px solid var(--line);
	font-size: 15px;
	line-height: 1.5;
	padding: 11px 14px;
	text-align: left;
	vertical-align: top;
}

.article-table thead th {
	background: #eae8df;
	font-family: var(--sans);
	font-weight: 600;
	letter-spacing: 0.01em;
}

@media (max-width: 600px) {

	.article-table th,
	.article-table td {
		font-size: 14px;
		padding: 9px 10px;
	}
}

/* --------------------------------------------------------------------------
 * The EMI calculator
 *
 * `os-emi-calculator` ships its own stylesheet from 2014 — a black header bar, Times
 * New Roman, and a repayment table in green, orange and lilac. It works, and none of
 * it belongs on this site. The plugin's markup cannot be changed, so it is restyled
 * by its own ids and classes.
 *
 * The calculator is the plugin's; the clothes are the design's.
 * -------------------------------------------------------------------------- */

.emi-container,
#emipaymenttable {
	font-family: var(--sans);
	margin: 0 auto;
	max-width: 900px;
}

/*
 * The plugin lays its two halves out with floats and clears them with a `.clear-style`
 * div whose only rule lived in the stylesheet now blocked. Without it the container
 * collapses to the height of its header and the calculator renders on top of whatever
 * follows.
 */
.emi-container::after,
.emi-container .clear-style {
	clear: both;
	content: "";
	display: block;
}

.emi-container #container {
	max-width: 100%;
}

#osemi-header,
#osemi-mthbrekup {
	background: var(--dark);
	color: var(--paper);
	font-family: var(--sans);
	font-size: 12px;
	font-weight: 500;
	letter-spacing: 1.4px;
	margin: 32px 0 0;
	padding: 14px 18px;
	text-align: left;
	text-transform: uppercase;
}

/* The inputs and the answer, side by side while there is room for both. */
.emi-container .emi-form,
.emi-container #osemi-summary {
	box-sizing: border-box;
	float: none;
	width: 100%;
}

@media (min-width: 801px) {

	.emi-container .emi-form {
		float: left;
		width: 54%;
	}

	.emi-container #osemi-summary {
		float: right;
		width: 42%;
	}
}

.emi-container .emi-form table {
	border-collapse: collapse;
	width: 100%;
}

.emi-container .emi-form td {
	border: 0;
	font-size: 15px;
	padding: 12px 0;
	vertical-align: middle;
}

.emi-container .emi-form input[type="text"],
.emi-container .emi-form input[type="number"] {
	background: var(--paper);
	border: 1px solid var(--line);
	border-radius: 0;
	box-sizing: border-box;
	color: var(--ink);
	font-family: var(--sans);
	font-size: 16px;
	padding: 10px 12px;
	width: 100%;
}

.emi-container .emi-form input:focus-visible {
	border-color: var(--dark);
	outline: 2px solid var(--dark);
	outline-offset: 1px;
}

/* The three figures: the monthly payment is the answer, the other two support it. */
#osemi-summary > div {
	background: #eae8df;
	border: 1px solid var(--line);
	margin-bottom: 10px;
	padding: 16px 18px;
	text-align: left;
}

#osemi-summary h4 {
	color: #66675f;          /* 4.66:1 on #eae8df */
	font-family: var(--sans);
	font-size: 11px;
	font-weight: 500;
	letter-spacing: 1.2px;
	margin: 0 0 4px;
	text-transform: uppercase;
}

#osemi-summary p {
	color: var(--ink);
	font-family: var(--serif);
	font-size: 26px;
	line-height: 1.2;
	margin: 0;
}

/*
 * `#osemi-summary > div` above is one id *and* one element, so it outranks a bare id
 * and the answer rendered near-white on paper — invisible. Matched through the parent
 * so the two rules compare like for like.
 */
#osemi-summary > #osemi-monthlypayment {
	background: var(--dark);
	border-color: var(--dark);
}

#osemi-monthlypayment h4 {
	color: #c7cec3;          /* 6.2:1 on #054239 */
}

#osemi-monthlypayment p {
	color: var(--paper);
	font-size: 34px;
}

/* The repayment schedule, in the same clothes as a migrated article's table. */
#emipaymenttable table {
	border-collapse: collapse;
	min-width: 100%;
}

#emipaymenttable th,
#emipaymenttable td {
	border: 1px solid var(--line);
	color: var(--ink);
	font-family: var(--sans);
	font-size: 14px;
	padding: 10px 12px;
	text-align: left;
}

#emipaymenttable thead th {
	background: #eae8df;
	font-weight: 600;
}

#emipaymenttable {
	overflow-x: auto;
}

/* Google's chart canvas, which the plugin leaves unsized. */
.emi-container #container:not(:empty) {
	margin-top: 24px;
}

@media (max-width: 600px) {

	#emipaymenttable th,
	#emipaymenttable td {
		font-size: 13px;
		padding: 8px 9px;
	}

	#osemi-summary p {
		font-size: 22px;
	}

	#osemi-monthlypayment p {
		font-size: 28px;
	}
}

/* --------------------------------------------------------------------------
 * Where an article is filed, and what it is tagged
 *
 * Both lead to archives that render in the journal's own card grid, so they are real
 * destinations rather than decoration — an internal link from every article into the
 * page that collects the rest of its subject, which is why the client asked for them.
 *
 * A category reads as the primary fact and takes the filled treatment; a tag is a
 * cross-reference and takes the outline. Both clear AA on paper: #054239 on #e4e1d6
 * is 8.6:1, and the ink outline is the page's own text colour.
 * -------------------------------------------------------------------------- */

.article-taxonomy {
	display: flex;
	flex-direction: column;
	gap: 12px;
	padding-bottom: 0;
}

.article-taxonomy-row {
	align-items: center;
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
}

.article-taxonomy-row .micro {
	color: #66675f;
	margin-right: 4px;
}

.article-term {
	background: #e4e1d6;
	border: 1px solid transparent;
	color: var(--dark);
	display: inline-block;
	font-family: var(--sans);
	font-size: 12px;
	letter-spacing: 0.02em;
	padding: 6px 12px;
	transition: background 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}

.article-term.is-tag {
	background: transparent;
	border-color: var(--line);
	color: var(--ink);
}

.article-term:hover,
.article-term:focus-visible {
	background: var(--dark);
	border-color: var(--dark);
	color: var(--paper);
}

/* --------------------------------------------------------------------------
 * A contents list for a long article
 *
 * These articles run to twenty-odd numbered sections, and the reading column left
 * most of a wide screen empty beside it. The honest options were to widen the measure
 * or to put something in the space; a wider measure makes long-form *harder* to read,
 * so the space goes to a contents list instead. It is the one thing a reader of a
 * 3,000-word article actually wants and the page did not offer.
 *
 * Only on articles with more than three numbered sections — below that a list is just
 * a second copy of the headings a reader can already see.
 *
 * Below 1200px it stacks above the article, where it still earns its place: on a phone
 * it is the only way to see the shape of the piece without scrolling the whole of it.
 * -------------------------------------------------------------------------- */

.article-contents {
	border-top: 1px solid var(--line);
	margin-bottom: 40px;
	padding-top: 18px;
}

.article-contents .micro {
	color: #66675f;
	display: block;
	margin-bottom: 14px;
}

.article-contents ol {
	counter-reset: none;
	display: flex;
	flex-direction: column;
	gap: 2px;
	list-style: none;
	margin: 0;
	padding: 0;
}

.article-contents a {
	align-items: baseline;
	color: var(--ink);
	display: flex;
	font-family: var(--sans);
	font-size: 14px;
	gap: 10px;
	line-height: 1.45;
	padding: 6px 0;
	transition: color 0.3s ease;
}

.article-contents a:hover,
.article-contents a:focus-visible {
	color: var(--dark);
}

.article-contents-number {
	color: #8f6638;          /* the measured gold, 4.5:1 on paper */
	flex: none;
	font-size: 11px;
	letter-spacing: 1px;
}

@media (min-width: 1200px) {

	.article-body.has-contents {
		align-items: start;
		column-gap: 72px;
		display: grid;
		grid-template-columns: minmax(0, 1fr) 260px;
	}

	/*
	 * Placed explicitly rather than by source order: the list comes first in the
	 * markup so that a reader without this layout — a phone, a screen reader — meets
	 * the contents before the article it describes, which is the point of it.
	 */
	.article-body.has-contents .article-sections {
		grid-column: 1;
		grid-row: 1;
	}

	.article-body.has-contents .article-contents {
		grid-column: 2;
		grid-row: 1;
		margin-bottom: 0;
		position: sticky;
		top: 110px;
	}
}

/* --------------------------------------------------------------------------
 * A page hero that ends on its lead paragraph
 *
 * `.about-hero` and the section under it both carry a full `section-padding`, so the
 * two stack: 110px of hero padding plus 26px of content margin plus 142px above the
 * next eyebrow — **278px of empty page** on `/residential/` and `/commercial/`, where
 * the hero has neither an image nor buttons and simply stops after a sentence.
 *
 * Scoped to heroes with no action row, which is exactly that case. Where the hero ends
 * on a pair of buttons — `/about-us/`, `/contact-us/` — the same space reads as
 * separating them from what follows, and those pages are signed off.
 *
 * `:has()` rather than a page-level class, because the condition is what the hero
 * actually contains. A browser without it keeps the design's own spacing, which is
 * only ever too generous, never broken.
 * -------------------------------------------------------------------------- */

.about-hero:not(:has(.about-hero-actions)) {
	padding-bottom: 0;
}

/* --------------------------------------------------------------------------
 * Two more places a full section padding lands against another one
 *
 * Same fault as the hero above, in two further spots that only show up once a block
 * is used without the part that normally fills the space.
 *
 * 1. A legal page is `page_hero` + `rich_text`, and the rich text carries 110px of its
 *    own top padding under a hero that has just stopped — 138px between the heading
 *    and the first line it introduces.
 *
 * 2. `.project-intro` is deliberately `padding-bottom: 0`, because the filter row
 *    below it provides the separation from the carousel. Switch the filters off — which
 *    `/residential/` and `/commercial/` do, since a filter with one choice is noise —
 *    and the heading sits directly on the photograph with nothing between them.
 *
 * Both are written as "when the thing that filled the space is absent", so a page that
 * still has it is untouched.
 * -------------------------------------------------------------------------- */

.about-hero:not(:has(.about-hero-actions)) + .rich-text-section {
	padding-top: 56px;
}

.project-intro:not(:has(.project-filters)) {
	padding-bottom: 72px;
}

@media (max-width: 600px) {

	.about-hero:not(:has(.about-hero-actions)) + .rich-text-section {
		padding-top: 34px;
	}

	.project-intro:not(:has(.project-filters)) {
		padding-bottom: 44px;
	}
}

/* --------------------------------------------------------------------------
 * The project names on an About portfolio card
 *
 * They were one italic sentence of names joined by middots — readable, but the names
 * are the most useful thing on the card and led nowhere. Each is a link to its project
 * now, stacked so the eye can run down them.
 *
 * `font-style: normal` is not optional: app.css italicises the whole preview block, and
 * a list of italic links reads as a quotation rather than as navigation.
 *
 * The ink colour, not the #777 the preview inherits. That grey is fine for a passive
 * line of text but these are targets, and ink on this card is 11.9:1 against #777's
 * 4.2:1 — the difference between "legible" and "obviously clickable".
 * -------------------------------------------------------------------------- */

.cat-projects-list {
	display: flex;
	flex-direction: column;
	gap: 0;
	list-style: none;
	margin: 0;
	padding: 0;
}

.cat-projects-list a {
	color: var(--ink);
	display: inline-block;
	font-family: var(--sans);
	font-size: 14px;
	font-style: normal;
	line-height: 1.45;
	padding: 5px 0;
	text-decoration: none;
	transition: color 0.3s ease;
}

.cat-projects-list a:hover,
.cat-projects-list a:focus-visible {
	color: var(--dark);
	text-decoration: underline;
	text-underline-offset: 3px;
}

/* --------------------------------------------------------------------------
 * Who it suits, and the questions before enquiring
 *
 * Two sections the client asked for from their other site. Neither exists in the React
 * design, so neither has classes in app.css — they are built here in the design's own
 * vocabulary instead of carrying the other site's Elementor styling across.
 * -------------------------------------------------------------------------- */

.personas-grid {
	display: grid;
	gap: 28px;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	margin-top: 40px;
}

.persona-card-media {
	aspect-ratio: 4 / 3;
	margin-bottom: 20px;
	overflow: hidden;
}

.persona-card-media img {
	display: block;
	height: 100%;
	object-fit: cover;
	transition: transform 1.4s ease;
	width: 100%;
}

.persona-card:hover .persona-card-media img {
	transform: scale(1.04);
}

.persona-card h3 {
	font-family: var(--serif);
	font-size: clamp(21px, 1.7vw, 26px);
	line-height: 1.2;
	margin: 0 0 10px;
}

.persona-card p {
	color: #66675f;          /* 4.9:1 on --paper */
	font-family: var(--sans);
	font-size: 16px;
	line-height: 1.7;
	margin: 0;
}

.personas-action,
.faq-action {
	display: flex;
	margin-top: 44px;
}

/*
 * `<details>` with no scripting: the browser supplies the keyboard operation, the
 * expanded state and the roles, and the answers are in the document for a crawler
 * rather than behind a click. The only work here is hiding the default marker and
 * drawing our own.
 */
.faq-list {
	border-top: 1px solid var(--line);
	margin-top: 40px;
	max-width: 900px;
}

.faq-item {
	border-bottom: 1px solid var(--line);
}

.faq-item summary {
	/*
	 * Top, not centre. A question that wraps to two lines — which most of them do on a
	 * phone — put the marker in the gap between them, pointing at nothing. The offset
	 * below centres it on the first line instead.
	 */
	align-items: flex-start;
	cursor: pointer;
	display: flex;
	gap: 24px;
	justify-content: space-between;
	list-style: none;
	padding: 22px 0;
}

/* Safari draws its own triangle through `list-style`; this is the only way to remove it. */
.faq-item summary::-webkit-details-marker {
	display: none;
}

.faq-item summary:focus-visible {
	outline: 2px solid var(--dark);
	outline-offset: 3px;
}

.faq-question {
	color: var(--ink);
	font-family: var(--serif);
	font-size: clamp(18px, 1.5vw, 22px);
	line-height: 1.35;
}

/*
 * The marker is drawn from two rules rather than a glyph, so it matches the hairlines
 * the rest of the page is built from and rotates to a minus when the answer is open.
 */
.faq-marker {
	flex: none;
	height: 13px;

	/* Half the difference between the question's line box and this box. */
	margin-top: 6px;
	position: relative;
	width: 13px;
}

.faq-marker::before,
.faq-marker::after {
	background: var(--ink);
	content: "";
	inset: 50% 0 auto;
	position: absolute;
	transition: transform 0.3s ease;
}

.faq-marker::before {
	height: 1px;
	width: 100%;
}

.faq-marker::after {
	height: 100%;
	inset: 0 50% auto;
	width: 1px;
}

.faq-item[open] .faq-marker::after {
	transform: scaleY(0);
}

.faq-answer {
	color: #66675f;          /* 4.9:1 on --paper */
	font-family: var(--sans);
	font-size: 16px;
	line-height: 1.8;
	max-width: 70ch;
	padding: 0 0 24px;
}

.faq-answer p {
	margin: 0 0 12px;
}

.faq-answer p:last-child {
	margin-bottom: 0;
}

/* app.css's preflight strips list markers; these answers are mostly bulleted. */
.faq-answer ul {
	list-style: disc;
	margin: 0 0 12px;
	padding-left: 20px;
}

.faq-answer li {
	margin-bottom: 4px;
}

/*
 * Three columns or one, never two.
 *
 * Two columns leaves the third card alone on a second row with an empty half beside
 * it — the stranded tile this project has turned down twice before, on the mock-house
 * grid and on the About portfolio cards. A single column below 900px is what the
 * design does with its own three-up grids, and it is what the cards read best as:
 * the photograph is the point, and it gets the full width.
 */
@media (max-width: 900px) {

	.personas-grid {
		gap: 36px;
		grid-template-columns: minmax(0, 1fr);
	}
}

@media (max-width: 600px) {

	.personas-grid {
		gap: 32px;
	}

	.faq-item summary {
		gap: 16px;
		padding: 18px 0;
	}

	.personas-action,
	.faq-action {
		margin-top: 32px;
	}
}

/*
 * The persona cards and the questions are a pair, so the join between them is tighter
 * than the gap either keeps from the rest of the page. Left alone they met at two full
 * section paddings — 110px plus 110px, 252px from the button to the next eyebrow.
 *
 * Written as a relationship, like the hero and the rich text above: each rule only
 * applies while the other section is actually next to it, so moving either one in the
 * builder restores its own spacing.
 */
.personas-section:has(+ .faq-section) {
	padding-bottom: 70px;
}

.personas-section + .faq-section {
	padding-top: 56px;
}

/*
 * The icon sits above the name, at the size the card headings are set in, and takes
 * the measured gold rather than the design's — 2.2:1 on paper is fine for a surface
 * and not for a 1.2px stroke.
 */
.persona-icon {
	color: #8f6638;
	display: block;
	margin-bottom: 14px;
}

.persona-icon svg {
	display: block;
}

@media (max-width: 600px) {

	.personas-section:has(+ .faq-section) {
		padding-bottom: 44px;
	}

	.personas-section + .faq-section {
		padding-top: 36px;
	}
}

/* --------------------------------------------------------------------------
 * A configuration card's two actions
 *
 * The card names a specific unit, so both buttons carry that unit into the form —
 * enquire about this one, or book a visit to see this one. app.css styles
 * `.config-enquire-btn` as a full-width row with a rule above it; the visit button
 * is the same row, so the pair reads as one stack rather than two shapes.
 *
 * `margin-top: auto` moves from the button to the wrapper, or the first of the two
 * absorbs the card's spare height and the rule above it floats mid-card.
 * -------------------------------------------------------------------------- */

/*
 * The two actions on a configuration card
 *
 * app.css's `.config-enquire-btn` is a full-width underlined row with its arrow
 * pushed to the far edge — the design's own vocabulary, and right when it is the
 * card's single action. With a second one added beneath it the card ended in two
 * stacked rules with the words at one end and an icon at the other, which read as
 * a list of links rather than as the two things you can do here.
 *
 * They are a pair of buttons now, side by side and equal: Enquire is the filled
 * one (paper on #054239, 9.99:1), booking a visit is the outlined one. Scoped to
 * `.config-card-actions`, so the class keeps the design's treatment anywhere it is
 * the only action.
 */
.config-card-actions {
	border-top: 1px solid var(--line);
	display: flex;
	flex-wrap: wrap;
	gap: 10px;
	margin-top: auto;
	padding-top: 16px;
}

.config-card-actions .config-enquire-btn,
.config-card-actions .config-visit-btn {
	align-items: center;
	border: 1px solid transparent;
	cursor: pointer;
	display: inline-flex;
	flex: 1 1 130px;
	font-family: var(--sans);
	font-size: 11px;
	font-weight: 500;
	gap: 8px;
	justify-content: center;
	letter-spacing: 1px;
	min-height: 42px;
	padding: 11px 14px;
	text-transform: uppercase;
	transition: background .25s ease, border-color .25s ease, color .25s ease;
	width: auto;
}

.config-card-actions .config-enquire-btn {
	background: var(--dark);
	border-color: var(--dark);
	color: var(--paper);               /* 9.99:1 on #054239 */
}

.config-card-actions .config-enquire-btn:hover,
.config-card-actions .config-enquire-btn:focus-visible {
	background: #073029;               /* the same green, one step down */
	border-color: #073029;
	color: var(--paper);
}

.config-card-actions .config-visit-btn {
	background: none;
	border-color: var(--line);
	color: var(--ink);                 /* 11.2:1 on the card */
}

.config-card-actions .config-visit-btn:hover,
.config-card-actions .config-visit-btn:focus-visible {
	background: rgb(183 161 124 / 16%);
	border-color: var(--gold);
	color: var(--ink);
}

/*
 * One under the other on a phone: side by side they were 138px wide, which wraps
 * "BOOK A SITE VISIT" onto two lines and leaves a thumb a narrow target.
 */
@media (max-width: 600px) {

	.config-card-actions .config-enquire-btn,
	.config-card-actions .config-visit-btn {
		flex: 1 1 100%;
	}
}

/* --------------------------------------------------------------------------
 * A bookable configuration row on a project card
 *
 * Each row of the Type / Size / From table books a visit for that unit. The row is
 * the pointer target; the button inside its header cell is the real control, so the
 * keyboard reaches it and a screen reader announces it as an action.
 *
 * The button is styled to look like the cell it replaced — the row should read as a
 * table, not as a list of buttons — and the affordance is the row's hover state plus
 * the cursor, which is how the rest of the design signals a clickable surface.
 * -------------------------------------------------------------------------- */

.config-row.is-bookable {
	cursor: pointer;
	transition: background 0.25s ease;
}

.config-row.is-bookable:hover,
.config-row.is-bookable:focus-within {
	background: #eae8df;
}

.config-row-trigger {
	background: none;
	border: 0;
	color: inherit;
	cursor: pointer;
	font: inherit;
	letter-spacing: inherit;
	padding: 0;
	text-align: left;
	width: 100%;
}

.config-row-trigger:focus-visible {
	outline: 2px solid var(--dark);
	outline-offset: 2px;
}

/* --------------------------------------------------------------------------
 * The unit an enquiry is about, shown inside the form
 *
 * Opened from a configuration row or card, the form already knows the unit — so it
 * states it with its size and price instead of asking a question it has the answer
 * to, and the visitor can see they are enquiring about the right one before they
 * type anything.
 *
 * **Ink on paper.** The lead dialog is `--paper` (#f3f1eb) with ink text, not one of
 * the dark panels — the first pass took the light palette and put #f3f1eb type on
 * #f3f1eb ground, so the panel rendered as two faint labels with nothing beside
 * them. Measured against #f3f1eb: #66675f labels 5.37:1, ink figures 11.2:1.
 * -------------------------------------------------------------------------- */

.lead-unit-summary {
	align-items: baseline;
	background: rgb(41 43 38 / 4%);
	border-left: 2px solid var(--gold);
	display: flex;
	flex-wrap: wrap;
	gap: 2px 28px;
	margin-bottom: 20px;
	padding: 12px 16px;
}

/*
 * One strip, not a stacked card: the unit, its size and its figure read across a
 * single line. Stacked it was 131px of a dialog the client had already called too
 * tall, and the facts are three short values, not a paragraph.
 */
.lead-unit-summary .micro {
	color: var(--color-text-muted);   /* 5.37:1 on #f3f1eb */
	flex-basis: 100%;
	margin-bottom: 2px;
}

.lead-unit-type {
	color: var(--ink);
	font-family: var(--serif);
	font-size: 22px;
	line-height: 1.25;
	margin: 0;
}

.lead-unit-facts {
	display: flex;
	flex-wrap: wrap;
	gap: 6px 28px;
	margin: 0;
}

/*
 * Label beside value, not above it: stacked, the two facts cost a third line of a
 * panel that has to share a dialog with a calendar.
 */
.lead-unit-facts > div {
	align-items: baseline;
	display: flex;
	flex-direction: row;
	gap: 7px;
}

.lead-unit-facts dt {
	color: var(--color-text-muted);   /* 5.37:1 on #f3f1eb */
	font-family: var(--sans);
	font-size: 11px;
	letter-spacing: 1.2px;
	text-transform: uppercase;
}

.lead-unit-facts dd {
	color: var(--ink);
	font-family: var(--sans);
	font-size: 15px;
	margin: 0;
}

@media (max-width: 600px) {

	.lead-unit-summary {
		margin-bottom: 16px;
		padding: 11px 13px;
	}

	.lead-unit-type {
		font-size: 20px;
	}

	.lead-unit-facts {
		gap: 6px 22px;
	}
}

/* --------------------------------------------------------------------------
 * A dialog has to fit the screen it opens on
 *
 * The lead dialog is laid out for a tall window: a 57px display title, 48px of
 * padding top and bottom and 45px of air around the description. At 1366x768 the
 * visible height is about 640px and the booking ran to 786, so the submit button
 * was below the fold and the visitor had to scroll a dialog to find it.
 *
 * The space comes from the rhythm, not from the words — nothing is hidden and no
 * field moves. Keyed off the window's **height**, since that is what runs out.
 * -------------------------------------------------------------------------- */

@media (max-height: 860px) {

	.lead-dialog {
		padding-bottom: 34px;
		padding-top: 34px;
	}

	.lead-dialog .lead-title {
		font-size: 44px !important;   /* app.css sets this one !important */
	}

	.lead-dialog .lead-description {
		/* app.css sets this margin with !important. */
		margin-bottom: 20px !important;
		margin-top: 12px !important;
	}
}

@media (max-height: 800px) {

	.lead-dialog {
		padding-bottom: 26px;
		padding-top: 26px;
	}

	.lead-dialog .lead-title {
		font-size: 36px !important;   /* app.css sets this one !important */
	}

	.lead-dialog .lead-description {
		/* app.css sets this margin with !important. */
		margin-bottom: 16px !important;
		margin-top: 10px !important;
	}

	.lead-dialog .form-grid {
		gap: 14px 20px;
		margin-top: 18px;
	}

	.lead-dialog .consent-field {
		margin-top: 16px;
	}

	.lead-dialog .form-bottom {
		margin-top: 24px;
	}

	.lead-unit-summary {
		margin-bottom: 14px;
		padding: 10px 14px;
	}
}

/* The last tier: a small laptop, or a desktop window someone has made short. */
@media (max-height: 700px) {

	.lead-dialog .lead-title {
		font-size: 30px !important;   /* app.css sets this one !important */
	}



	.lead-dialog .lead-description {
		/* app.css sets this margin with !important. */
		margin-bottom: 12px !important;
		margin-top: 8px !important;
	}

	.lead-dialog .form-bottom {
		margin-top: 18px;
	}
}

/* --------------------------------------------------------------------------
 * Choosing when to visit
 *
 * The browser's own date control is dd/mm/yyyy in the browser's own language and
 * opens a calendar in the browser's own colours — the one part of this dialog the
 * design could not reach. This is a month grid and four pills in the dialog's own
 * type and palette, writing the same YYYY-MM-DD to a hidden field.
 *
 * Contrast, all measured on this dialog's #f3f1eb paper: a bookable day is --ink
 * at 11.2:1, a day already past is #8a8b82 at 3.2:1 — which is the AA floor for a
 * **disabled** control, and the point of the shade is that it is unavailable. The
 * chosen day is #054239 on #c69c6d, the 4.90:1 pair the project pills already use.
 * -------------------------------------------------------------------------- */

/*
 * The calendar and the times sit side by side.
 *
 * Stacked, a month grid and four pills ran to 300px of a dialog that also carries a
 * title, the unit and a button — and the dialog is 650px wide, which is room for
 * both. It also reads as one booking control rather than as two more form fields,
 * which is what it is.
 */
.visit-when {
	align-items: start;
	display: grid;
	gap: 8px 18px;
	grid-template-areas:
		"date-label time-label"
		"calendar   times";
	grid-template-columns: minmax(0, 1fr) 180px;
}

.visit-when > .micro:first-of-type {
	grid-area: date-label;
}

.visit-when-label {
	grid-area: time-label;
	margin-top: 0;
}

.visit-calendar {
	grid-area: calendar;
}

.visit-time-select {
	grid-area: times;
}

/*
 * The schedule screen carries a calendar where the others carry two inputs, so the
 * air around the form has to come down with it or the Continue button lands below
 * the dialog. Scoped to the screen that has one, by the element that is only on it.
 */
.lead-dialog form:has(.visit-when:not([hidden])) .form-bottom {
	margin-top: 22px;
}

.lead-dialog form:has(.visit-when:not([hidden])) .form-grid {
	margin-top: 16px;
}

.visit-when .micro {
	color: var(--color-text-muted);   /* 5.37:1 on #f3f1eb */
}

.visit-calendar {
	border: 1px solid var(--line);
	padding: 10px 12px 12px;
}

.visit-calendar-head {
	align-items: center;
	display: flex;
	justify-content: space-between;
	margin-bottom: 6px;
}

.visit-calendar-month {
	color: var(--ink);
	font-family: var(--serif);
	font-size: 19px;
	letter-spacing: -0.2px;
}

.visit-calendar-nav {
	align-items: center;
	background: none;
	border: 1px solid var(--line);
	color: var(--ink);
	cursor: pointer;
	display: inline-flex;
	height: 30px;
	justify-content: center;
	transition: border-color .25s ease, background .25s ease;
	width: 30px;
}

.visit-calendar-nav:hover:not(:disabled) {
	background: rgb(183 161 124 / 15%);
	border-color: var(--gold);
}

.visit-calendar-nav:disabled {
	cursor: default;
	opacity: .35;
}

.visit-calendar-weekdays,
.visit-calendar-grid {
	display: grid;
	gap: 2px;
	grid-template-columns: repeat(7, minmax(0, 1fr));
}

.visit-calendar-weekdays {
	color: var(--color-text-muted);
	font-family: var(--sans);
	font-size: 10px;
	letter-spacing: 1px;
	margin-bottom: 4px;
	text-align: center;
	text-transform: uppercase;
}

.visit-day {
	background: none;
	border: 1px solid transparent;
	color: var(--ink);
	cursor: pointer;
	font-family: var(--sans);
	font-size: 14px;
	line-height: 1;
	padding: 6px 0;
	transition: background .2s ease, border-color .2s ease, color .2s ease;
}

.visit-day:hover:not(:disabled) {
	background: rgb(183 161 124 / 18%);
}

.visit-day:disabled {
	color: #8a8b82;                   /* 3.2:1 — a day that has been and gone */
	cursor: default;
}

.visit-day.is-chosen {
	background: var(--gold);
	border-color: var(--gold);
	color: #054239;                   /* 4.90:1 on #c69c6d */
	font-weight: 500;
}

/*
 * `.lead-dialog .visit-when` prefix: app.css styles `.form-field select` and
 * `.lead-dialog .form-field select`, both of which outrank a lone class, so a plain
 * `.visit-time-select` rule would be overruled on every property they set.
 *
 * The time is a select, so the list can grow — the client expects more slots than
 * the four that ship. Bordered like the calendar beside it rather than carrying
 * app.css's underline, because the two are one control between them.
 *
 * The arrow is an inline SVG data URI in --ink: a select's own arrow is the
 * platform's, in the platform's colour, which is the same mismatch the native date
 * input was replaced for.
 */
.lead-dialog .visit-when .visit-time-select {
	appearance: none;
	background-color: transparent;
	background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="8" viewBox="0 0 12 8" fill="none" stroke="%23292b26" stroke-width="1.4"><path d="M1 1.5 6 6.5 11 1.5"/></svg>');
	background-position: right 12px center;
	background-repeat: no-repeat;
	border: 1px solid var(--line);
	color: var(--ink);
	cursor: pointer;
	font-family: var(--sans);
	font-size: 14px;
	height: auto;
	min-height: 42px;
	padding: 10px 34px 10px 12px;
	width: 100%;
}

.lead-dialog .visit-when .visit-time-select:focus-visible {
	border-color: var(--gold);
	outline: 2px solid var(--gold);
	outline-offset: 1px;
}

@media (max-width: 600px) {

	/* No room for two columns on a phone: the calendar takes the width. */
	.visit-when {
		grid-template-areas:
			"date-label"
			"calendar"
			"time-label"
			"times";
		grid-template-columns: minmax(0, 1fr);
	}

	.visit-when-label {
		margin-top: 6px;
	}



	.visit-calendar {
		padding: 10px 12px 12px;
	}

	.visit-day {
		font-size: 15px;
		padding: 9px 0;
	}
}

/*
 * The short-window tiers again, after the calendar's own rules.
 *
 * Source order decides between two rules of equal weight, and the tiers above are
 * declared before the calendar is — so these have to live at the end of the file to
 * have any effect at all. They were silently doing nothing until a measurement said
 * the box had not changed.
 */
@media (max-height: 800px) {

	.visit-day {
		padding: 5px 0;
	}
}

@media (max-height: 720px) {

	.lead-unit-type {
		font-size: 19px;
	}

	.visit-calendar {
		padding: 8px 10px 10px;
	}

	.visit-calendar-month {
		font-size: 17px;
	}

	.visit-day {
		font-size: 13px;
		padding: 3px 0;
	}
}

/*
 * The booking on a phone
 *
 * Measured at 390x844: the dialog clamps at 760 and the content ran to about 890,
 * so Confirm sat below it. The space is taken from the unit panel (which wraps to
 * two lines there) and from the time pills, which fit four across at this size —
 * "10:00 AM" is 60px of 11px uppercase in a 75px column.
 */
@media (max-width: 600px) {

	.lead-unit-summary {
		gap: 2px 18px;
	}

	.lead-unit-type {
		font-size: 18px;
	}

	.lead-unit-facts {
		gap: 2px 18px;
	}

	.lead-unit-facts dd {
		font-size: 14px;
	}

	.visit-day {
		padding: 5px 0;
	}

	.lead-dialog .lead-description {
		/* app.css sets this margin with !important. */
		margin-bottom: 14px !important;
		margin-top: 10px !important;
	}

	.lead-unit-summary {
		margin-bottom: 12px;
		padding: 10px 12px;
	}

	.lead-dialog {
		padding-bottom: 28px;
		padding-top: 28px;
	}

	.lead-dialog .lead-title {
		font-size: 36px !important;   /* app.css sets this one !important */
	}
}

/* --------------------------------------------------------------------------
 * Two things that sat on top of an open dialog
 *
 * Both only show on a phone, which is why neither turned up at a narrow desktop
 * width.
 *
 * The **chat widget** is `position: fixed` with a z-index of its own, so its
 * launcher and greeting painted over the booking dialog — across the time pills on
 * a 390px screen. An overlay is modal: while one is open there is nothing else to
 * press, and `has-overlay-open` is the class every overlay here already sets.
 *
 * The **close button** is absolutely placed in the dialog's top corner, and the
 * eyebrow runs the full width under it — so "SITE VISIT / DIVINE WORLD PHASE IV ·
 * 01 / 02" ran straight through the X. The eyebrow keeps clear of the corner
 * instead of being shortened.
 * -------------------------------------------------------------------------- */

html.has-overlay-open .engt-wrapper {
	display: none !important;
}

@media (max-width: 600px) {

	.lead-dialog > .eyebrow {
		padding-right: 34px;
	}
}

/*
 * Giving the eyebrow its second line back costs the booking screen 16px on a phone,
 * which it does not have — so the calendar gives them up. Measured at 390x844:
 * Continue lands inside the dialog again.
 */
@media (max-width: 600px) {

	.visit-when {
		gap: 6px 18px;
	}

	.visit-calendar {
		padding: 8px 10px 10px;
	}

	.visit-day {
		padding: 4px 0;
	}

	.lead-unit-summary {
		margin-bottom: 10px;
	}
}

/* --------------------------------------------------------------------------
 * The appointment on the confirmation screen
 *
 * A site visit does not require an email address, so the invitation a mail client
 * would act on by itself does not always exist. This is the same event as two
 * links — Google's own compose URL, and the `.ics` that opens Calendar on iOS and
 * macOS and Outlook on Windows.
 *
 * Ink on the dialog's paper, like everything else in it.
 * -------------------------------------------------------------------------- */

.success-calendar {
	background: rgb(41 43 38 / 4%);
	border-left: 2px solid var(--gold);
	margin: 24px 0 4px;
	padding: 14px 16px;
}

.success-calendar .micro {
	color: var(--color-text-muted);   /* 5.37:1 on #f3f1eb */
	display: block;
	margin-bottom: 10px;
}

.success-calendar-links {
	display: flex;
	flex-wrap: wrap;
	gap: 10px;
}

.success-calendar-links .hero-btn-primary,
.success-calendar-links .hero-btn-secondary {
	flex: 1 1 180px;
	justify-content: center;
	min-height: 44px;
}

/*
 * The secondary button's dark ground is for a photograph; here it sits on paper,
 * so it takes the paper treatment — the same distinction the hero's own buttons
 * make between a hero with an image and one without.
 */
.success-calendar-links .hero-btn-secondary {
	background: transparent;
	border-color: rgb(41 43 38 / 30%);
	color: var(--ink);
}

.success-calendar-links .hero-btn-secondary:hover {
	background: rgb(183 161 124 / 16%);
	border-color: var(--gold);
}

@media (max-width: 600px) {

	.success-calendar-links .hero-btn-primary,
	.success-calendar-links .hero-btn-secondary {
		flex: 1 1 100%;
	}
}

/* --------------------------------------------------------------------------
 * The brand caption in the navigation overlay
 *
 * The capitals used to come from `strtoupper()` in the template. That field is
 * allowed to carry a link — it holds the agency credit — and uppercasing markup
 * uppercases the href with it, so the treatment lives here instead.
 *
 * The link keeps the caption's own ink — 12.67:1 on the overlay's paper — and is
 * marked by a rule under it rather than by a colour. The gold is 3.0:1 as text at
 * this size, which fails AA, and a credit line that is entirely a link has no
 * surrounding text for a colour to distinguish it from anyway. The rule turns gold
 * on hover, which is a state change that costs the text nothing.
 * -------------------------------------------------------------------------- */

/*
 * At the far end of the row, not beside the logo.
 *
 * `.menu-brand` is a full-width flex row, so pushing the caption over is one auto
 * margin — and it is the credit, which belongs at the edge of the page rather than
 * reading as a strapline under the brand. It stops short of the close button,
 * which is absolutely placed in the corner above it.
 */
.menu-brand-caption {
	margin-left: auto;
	text-align: right;
	text-transform: uppercase;
}

.menu-brand-caption a {
	color: inherit;
	text-decoration: underline;
	text-decoration-color: rgb(41 43 38 / 35%);
	text-underline-offset: 3px;
	transition: text-decoration-color .25s ease;
}

.menu-brand-caption a:hover,
.menu-brand-caption a:focus-visible {
	color: inherit;
	text-decoration-color: var(--gold);
}

/*
 * The overlay's logo is a link, so it needs a thumb-sized target.
 *
 * The mark itself is 185x39 on a phone, which is under the 44px a finger wants.
 * The padding buys the height and the negative margin gives it back to the layout,
 * so the row sits exactly where it did.
 */
.menu-brand-home {
	display: inline-block;
	margin: -4px 0;
	padding: 4px 0;
}
