/*
 * REI Hospitality — design system
 *
 * The brief is a hospitality HR consultancy, not a software product, and the
 * two should not look alike. So the page sits on warm sand rather than white,
 * headings are set in a serif, and the brand purple is spent deliberately
 * rather than spread around.
 *
 * On colour specifically: the logo's purple (#5A2982) and gold (#FFDA09) are
 * strong together at logo scale and unpleasant across a whole page. Purple is
 * therefore reserved for things that are interactive or structural — buttons,
 * links, the hero and footer grounds — and gold appears only as a thin rule or
 * a small mark. Gold is never used for text: at 1.5:1 against white it fails
 * contrast badly, and it is not fixable by darkening without ceasing to be the
 * brand colour.
 *
 * Everything is mobile-first. Most candidate traffic is a mid-range Android on
 * mobile data, so there is one stylesheet, no framework, and no layout that
 * depends on JavaScript.
 */

/* ==========================================================================
   1. Tokens
   ========================================================================== */

:root {
  /* Brand, taken from the logo. */
  --purple: #5A2982;
  --purple-mid: #6C389D;
  --purple-deep: #34184B;
  --purple-ink: #241033;
  --gold: #FFDA09;
  /* Only used where gold has to carry meaning on a light ground, such as
     list markers. Dark enough to clear AA; the bright --gold never is. */
  --gold-deep: #7F6900;

  /* Warm neutrals. The page ground is sand, not white — it reads as hospitality
     rather than clinical, and it is easier on the eye in bright light, which is
     how most people will see it here. */
  --sand: #FAF7F2;
  --sand-deep: #F1EBE2;
  --shell: #FFFFFF;

  /* Ink. Warm near-black rather than pure black; pure black on sand looks harsh. */
  --ink: #1F1A22;
  --ink-soft: #5A5260;
  /* Verified against every light ground we use: 4.54:1 at worst, which
     clears WCAG AA for body text. The previous value sat at 3.37:1 on the
     sand-deep panels and failed. */
  --ink-faint: #706778;
  --rule: #E3DCD2;
  --rule-strong: #CFC5B8;

  --positive: #17603C;
  --critical: #A32330;

  --font-display: 'Fraunces', Georgia, 'Times New Roman', serif;
  --font-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

  /* This site is light. Saying so stops Android Chrome's Auto Dark Theme
     repainting it — which it does to any page that has not declared a scheme,
     and which it does worst to the <select> popups. See "The dropdowns". */
  color-scheme: light;

  --radius: 4px;
  --radius-lg: 10px;
  --wrap: 70rem;
  --wrap-narrow: 44rem;

  /* One shadow, used sparingly. The brief rules out the same soft grey shadow
     on every element, so almost nothing here carries one. */
  --lift: 0 1px 2px rgba(31, 26, 34, 0.06), 0 8px 24px rgba(31, 26, 34, 0.06);
}

/* ==========================================================================
   2. Reset and base
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }
body, h1, h2, h3, h4, p, ul, ol, dl, dd, figure, blockquote { margin: 0; }
ul, ol { padding: 0; }
ul[class], ol[class], nav ul, nav ol { list-style: none; }
/* Lists inside a classed container, where the ul itself carries no class. */
.footer-links ul, .footer-legal ul, .contact-list, .steps, .job-list,
.placement-list, .category-grid, .link-list, .proof-grid { list-style: none; }
img, svg { max-width: 100%; height: auto; display: block; }
button, input, select, textarea { font: inherit; color: inherit; }

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

body {
  font-family: var(--font-body);
  overflow-wrap: break-word;
  font-size: 1rem;
  line-height: 1.65;
  color: var(--ink);
  background: var(--sand);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Footer stays at the bottom on short pages without tricks. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

main { flex: 1 0 auto; }

/* ==========================================================================
   3. Typography
   ========================================================================== */

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.015em;
  text-wrap: balance;
}

/* Fraunces carries an optical-size axis. Large headings get the display cut,
   smaller ones the text cut, which keeps them legible rather than spindly. */
h1 {
  font-size: clamp(2rem, 1.3rem + 3.2vw, 3.4rem);
  font-variation-settings: 'opsz' 120, 'SOFT' 20;
  line-height: 1.08;
}
h2 {
  font-size: clamp(1.5rem, 1.2rem + 1.5vw, 2.15rem);
  font-variation-settings: 'opsz' 48;
  margin-bottom: 0.6rem;
}
h3 {
  font-size: clamp(1.1rem, 1.05rem + 0.3vw, 1.25rem);
  font-variation-settings: 'opsz' 24;
  margin-bottom: 0.35rem;
}
h4 { font-size: 1rem; font-variation-settings: 'opsz' 18; }

p + p, p + ul, p + ol, ul + p, ol + p { margin-top: 1rem; }

a {
  color: var(--purple);
  text-decoration-color: rgba(90, 41, 130, 0.35);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: color 0.12s ease, text-decoration-color 0.12s ease;
}
a:hover { color: var(--purple-mid); text-decoration-color: currentColor; }

strong { font-weight: 600; }

.lede {
  font-size: clamp(1.05rem, 1rem + 0.35vw, 1.2rem);
  line-height: 1.55;
  color: var(--ink-soft);
  /* 40ch was tight enough that page-head ledes wrapped into a narrow column
     with the rest of the row empty beside them; 52ch was still leaving 40% of
     a 70rem page head unused. 64ch sits inside the 88-character ceiling at
     this size and is bounded by its column everywhere it appears in one. */
  max-width: 64ch;
  margin-top: 1rem;
}

.eyebrow {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--purple);
  margin-bottom: 0.5rem;
}

.prose { max-width: var(--wrap-narrow); }
.prose h2 { margin-top: 2.5rem; }
.prose h3 { margin-top: 1.75rem; }
/* Prose lists carry the house marker rather than the browser's.
   A default disc is the single clearest tell that nobody styled a page — and
   the same marker already runs down scope lists and rail lists, so this makes
   one pattern instead of two.

   The marker was a 5px round dot, which is what a browser draws anyway, only
   smaller: on the services page it read as a bulleted paragraph rather than a
   list of what an assignment covers. It is a small gold square set on its
   corner now — the one shape on the page that nothing else uses, so a scope
   list is recognisable from across the room.

   --gold-deep, not --gold. A list marker is a meaningful graphic and has a
   3:1 floor; #FFDA09 on sand is 1.5:1 and would be a smudge. #7F6900 is
   4.52:1. */
.prose ul, .prose ol, .job-body ul, .job-body ol { list-style: none; padding-left: 0; }
.prose li, .job-body li { position: relative; padding-left: 1.6rem; }
.prose li + li, .job-body li + li { margin-top: 0.6rem; }

.prose ul > li::before,
.job-body ul > li::before {
  content: "";
  position: absolute;
  left: 0.22rem;
  top: 0.6em;
  width: 7px;
  height: 7px;
  border-radius: 1px;
  background: var(--gold-deep);
  transform: rotate(45deg);
}

/* Ordered lists keep their number, set in the brand purple at the same weight
   as the numbered step markers elsewhere. */
.prose ol, .job-body ol { counter-reset: prose-item; }
.prose ol > li, .job-body ol > li { counter-increment: prose-item; padding-left: 2rem; }
.prose ol > li::before,
.job-body ol > li::before {
  content: counter(prose-item) ".";
  position: absolute;
  left: 0;
  top: 0;
  width: 1.4rem;
  text-align: right;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--purple);
  background: none;
  border-radius: 0;
  height: auto;
}

/* ==========================================================================
   4. Accessibility
   ========================================================================== */

:focus-visible {
  outline: 3px solid var(--purple);
  outline-offset: 2px;
  border-radius: 2px;
}
/* On the dark grounds the purple ring disappears, so gold takes over — one of
   the few places it genuinely earns its keep. */
.hero :focus-visible,
.site-footer :focus-visible,
.closing-cta :focus-visible { outline-color: var(--gold); }

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0);
  white-space: nowrap; border: 0;
}

.skip-link {
  /* Fixed rather than absolute: absolute positions it against the document, so
     it would sit off-screen for anyone who tabs to it after scrolling. */
  position: fixed;
  left: 0.5rem; top: -4rem;
  z-index: 100;
  background: var(--purple);
  color: #fff;
  padding: 0.75rem 1.25rem;
  border-radius: var(--radius);
  text-decoration: none;
  transition: top 0.15s ease;
}
.skip-link:focus { top: 0.5rem; color: #fff; }

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

/* ==========================================================================
   5. Layout
   ========================================================================== */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: 1.25rem;
}
@media (min-width: 40rem) { .wrap { padding-inline: 2rem; } }

.section { padding-block: clamp(2.5rem, 1.5rem + 4vw, 4.5rem); }
.section-tight { padding-block: clamp(2rem, 1.5rem + 2vw, 3rem); }

.page-head { padding-block: clamp(2rem, 1.5rem + 3vw, 3.5rem) 0; }
.page-head h1 { max-width: 20ch; }

/* --- A page that opens as a spread ----------------------------------------
   Title and lede on the left, the paragraph that says what this page is on
   the right. For a page whose opening is two blocks of type -- About is the
   one -- stacking a 20ch heading above a 44rem paragraph inside a 70rem
   container leaves a third of the width empty twice over.

   The prose column is nudged down at desktop so its first line sits with the
   heading rather than level with its cap, which is what stops the two columns
   reading as a table. */
.page-head-split { display: grid; gap: 1.5rem; }
@media (min-width: 60rem) {
  .page-head-split {
    grid-template-columns: 1fr 1fr;
    /* Row gap separate from column gap: the tagline spans row one, and 4rem
       under it left it floating away from the title it introduces. */
    gap: 1.25rem 4rem;
    align-items: start;
    padding-bottom: 0.5rem;
  }
  .page-head-split .prose { padding-top: 0.6rem; }
}

/* --- A heading beside its prose -------------------------------------------
   The same problem in the middle of a page: 44rem of running text, flush left
   in a 70rem container, with nothing on the right. Putting the heading in a
   column of its own uses the width and gives the page a spine down its left
   edge instead of a ragged right one.

   Same shape as .service-detail, which already does this on the services
   page, without the sticky head -- these blocks run three paragraphs and
   there is nothing worth sticking. */
.prose-split { display: grid; gap: 1.25rem; }
@media (min-width: 60rem) {
  .prose-split { grid-template-columns: 17rem 1fr; gap: 4rem; align-items: start; }
}
/* The tagline leads the head and spans it. In the title column its box was
   448px at a 1024px viewport, which caps it at 18.9px on one line — smaller
   than the standing tagline on every other page, which is the wrong way round
   for the page meant to carry it loudest. Across the head it has 960px.
   First in the markup, so it takes row one and the title and prose fill row
   two; under them it dangled below a two-line heading with an empty column
   above it. */
.page-head-split > .site-tagline {
  grid-column: 1 / -1;
  margin-top: 0;
  margin-bottom: 0.5rem;
}

.prose-split-head h2 { margin: 0; }
/* The heading is out of .prose now, so .prose's first-child spacing does not
   apply and the top margin it would have inherited has to go. */
.prose-split .prose > :first-child { margin-top: 0; }

.band-shell { background: var(--shell); }
.band-sand { background: var(--sand-deep); }

.section-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  margin-bottom: 1.5rem;
}
.section-head h2 { margin-bottom: 0; }
/* The "see all" link beside a section heading is a real tap target, not body
   copy, so it gets a real tap target's height on touch screens. */
.section-head a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: 0.95rem;
  font-weight: 500;
}

.two-col { display: grid; gap: 2rem; }
@media (min-width: 48rem) { .two-col { grid-template-columns: 1fr 1fr; gap: 3.5rem; } }

/* A hairline of gold under section headings — the accent doing structural work
   rather than decorating. */
/* A background gradient rather than a border, so the rule can be drawn in
   from the left as the heading arrives. Identical at rest. */
.ruled {
  padding-bottom: 0.75rem;
  display: inline-block;
  background-image: linear-gradient(var(--gold), var(--gold));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 2px;
}

/* ==========================================================================
   6. Buttons
   ========================================================================== */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.8rem 1.5rem;
  min-height: 48px;          /* comfortable touch target */
  border: 1.5px solid var(--purple);
  border-radius: var(--radius);
  background: transparent;
  color: var(--purple);
  font-weight: 600;
  font-size: 0.975rem;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.14s ease, color 0.14s ease, border-color 0.14s ease;
}
.button:hover { text-decoration: none; }

.button-primary { background: var(--purple); border-color: var(--purple); color: #fff; }

.button-large { padding: 0.95rem 1.9rem; font-size: 1.05rem; min-height: 54px; }
.button-block { width: 100%; }

/* On dark grounds the outline button inverts. */
.on-dark .button { border-color: rgba(255, 255, 255, 0.45); color: #fff; }
.on-dark .button-primary { background: #fff; border-color: #fff; color: var(--purple-ink); }

/* --- Hover: the button turns inside out -----------------------------------
   One rule for every button on the site. The border does not move and does
   not change colour; the fill and the label swap places. A filled button
   empties, an empty one fills, and the silhouette stays exactly where the eye
   left it. There is no third behaviour to remember.

   It replaces what was there before -- a 7% purple wash on the outline button
   and a half-step to --purple-deep on the filled one. Both were changes you
   had to look for. This one you cannot miss, which is the point of a hover
   state.

   Measured, because an inverted state is where contrast usually breaks:
     #FFFFFF on #5A2982   10.12:1
     #5A2982 on #FFFFFF   10.12:1
     #241033 on #FFFFFF   17.60:1
   All well clear of 4.5:1.

   Behind (hover: hover) so a tap on a phone does not leave a button stuck
   inside out. Touch already has the 1px press and the focus ring. */
@media (hover: hover) {
  .button:hover { background: var(--purple); color: #fff; }
  .button-primary:hover { background: #fff; color: var(--purple); }
  .on-dark .button:hover { background: #fff; border-color: #fff; color: var(--purple-ink); }
  /* The white edge is kept on the dark ground so the shape still reads once
     the fill drops to purple against a purple-ink band. */
  .on-dark .button-primary:hover { background: var(--purple); color: #fff; }
}

/* ==========================================================================
   7. Header
   ========================================================================== */

/* --- Contact strip --------------------------------------------------------
   Above the header and not sticky: it scrolls away and the navigation stays.
   Hidden on phones, where the sticky call and WhatsApp buttons already cover
   this and sit under a thumb rather than at the top of a scroll. */

.utility-bar {
  display: none;
  background: var(--purple-ink);
  color: #D9CFE2;
  font-size: 0.85rem;
}
@media (min-width: 48rem) { .utility-bar { display: block; } }

.utility-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  min-height: 40px;
}
.utility-contact { display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap; }
.utility-contact li { display: inline-flex; align-items: center; gap: 0.4rem; }
.utility-contact a { color: #fff; text-decoration: none; }
.utility-contact a:hover { color: var(--gold); text-decoration: none; }
.utility-contact .icon { color: var(--gold); flex: none; }
/* The platform colour still applies here — on this ground it is the signature
   green rather than the teal the light pages use. */
.utility-contact .icon-whatsapp.icon-brand { color: var(--brand-whatsapp-on-dark); }
.utility-note { color: #B4A5C1; margin: 0; }

/* --- Services menu --------------------------------------------------------
   The parent is a real link with a separate disclosure button beside it, so
   the overview page is always reachable and the menu is always operable from
   the keyboard. Open state is driven by aria-expanded on the button rather
   than by :hover, so it behaves the same with a mouse, a finger and a Tab
   key. Hover opens it as well on pointer devices, as a convenience only. */

.nav-has-menu { position: relative; }
.nav-parent { display: flex; align-items: center; gap: 0.15rem; }

.nav-menu-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 2rem;
  min-height: 44px;
  background: none;
  border: 0;
  cursor: pointer;
  color: inherit;
}
.nav-chevron {
  width: 0.45rem; height: 0.45rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform var(--motion-fast) var(--ease);
}
.nav-menu-toggle[aria-expanded="true"] .nav-chevron {
  transform: rotate(-135deg) translate(-1px, -1px);
}

.site-header {
  background: var(--sand);
  border-bottom: 1px solid var(--rule);
  position: sticky;
  top: 0;
  z-index: 40;
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-height: 68px;
}

/* Mark and wordmark on one baseline, sized so the lockup never touches the
   header's edges the way the stacked logo did. */
.brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex: none;
  text-decoration: none;
  /* The mark is 34px, so the link needs padding to clear a 44px touch target. */
  padding-block: 0.4rem;
  min-height: 44px;
}
.brand:hover { text-decoration: none; }
/* The mark is an SVG with a 229x100 viewBox, so height alone sizes it and the
   aspect ratio cannot drift. The <picture> element this replaced was widening
   it to 124x40 against the artwork's own 91.5x40. */
.brand-mark { display: block; width: auto; height: 34px; }

.brand-words {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
  padding-top: 0.1rem;
}
.brand-word {
  /* Not below 0.78rem: letterspaced caps under 12px stop being readable. */
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--purple);
}
.brand-word-sub {
  font-weight: 500;
  letter-spacing: 0.3em;
  color: var(--ink-faint);
}

@media (min-width: 60rem) {
  .brand { gap: 0.7rem; }
  .brand-mark { height: 40px; }
  .brand-word { font-size: 0.8rem; }
}

.brand-footer { margin-bottom: 1.25rem; padding-block: 0; min-height: 0; }
.brand-footer .brand-mark { height: 42px; }
.brand-footer .brand-word { color: #fff; font-size: 0.8rem; }
.brand-footer .brand-word-sub { color: var(--gold); }

.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px; height: 48px;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: transparent;
  cursor: pointer;
  flex: none;
}
.nav-toggle-bar,
.nav-toggle-bar::before,
.nav-toggle-bar::after {
  content: "";
  display: block;
  width: 19px; height: 2px;
  background: var(--ink);
  border-radius: 2px;
  transition: transform 0.18s ease, opacity 0.18s ease;
}
.nav-toggle-bar { position: relative; }
.nav-toggle-bar::before { position: absolute; top: -6px; }
.nav-toggle-bar::after { position: absolute; top: 6px; }

.nav-toggle[aria-expanded="true"] .nav-toggle-bar { background: transparent; }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar::before { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar::after { transform: translateY(-6px) rotate(-45deg); }

.site-nav { display: none; }
.site-nav.is-open {
  display: block;
  position: absolute;
  left: 0; right: 0; top: 100%;
  background: var(--sand);
  border-bottom: 1px solid var(--rule);
  box-shadow: var(--lift);
  padding: 0.5rem 1.25rem 1.25rem;
}
.site-nav ul { display: flex; flex-direction: column; }
.site-nav li + li { border-top: 1px solid var(--rule); }

/* In the drawer the service lines are simply listed, indented under their
   parent. A disclosure inside a disclosure is one level too many on a phone,
   and there is no hover to open it with. */
.site-nav .nav-menu { padding-left: 1rem; }
.site-nav .nav-menu a { font-size: 0.92rem; color: var(--ink-soft); }
.site-nav .nav-menu li:first-child { border-top: 1px solid var(--rule); }
.site-nav a {
  display: block;
  padding: 0.85rem 0;
  font-weight: 500;
  color: var(--ink);
  text-decoration: none;
}
.site-nav a:hover, .site-nav a[aria-current="page"] { color: var(--purple); }
.site-nav a.nav-cta {
  margin-top: 1rem;
  color: #fff;
  background: var(--purple);
  border-color: var(--purple);
}

@media (min-width: 60rem) {
  .nav-toggle { display: none; }
  .site-nav { display: flex !important; position: static; align-items: center; gap: 2rem; padding: 0; box-shadow: none; border: 0; background: none; }
  .site-nav ul { flex-direction: row; gap: 1.75rem; }
  .site-nav li + li { border: 0; }
  .site-nav a { padding: 0.35rem 0; font-size: 0.95rem; position: relative; }
  /* A gold underline marks the current page — small, and the only decorative
     use of the accent in the chrome. */
  .site-nav a[aria-current="page"]::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: -2px;
    height: 2px; background: var(--gold);
  }
  /* .site-nav a is more specific than .button-primary, so the CTA's colours
     have to be restated here or the link colour wins and the label goes dark. */
  .site-nav a.nav-cta {
    margin-top: 0;
    flex: 0 0 auto;
    width: auto;
    padding: 0.6rem 1.15rem;
    min-height: 42px;
    color: #fff;
    background: var(--purple);
    border-color: var(--purple);
  }
  /* Same inversion as every other button, restated at this specificity for
     the same reason the resting colours are. */
  .site-nav a.nav-cta:hover { background: #fff; border-color: var(--purple); color: var(--purple); }
  .site-nav a.nav-cta[aria-current="page"]::after { display: none; }

  /* Desktop: the list becomes a panel and the disclosure button appears. */
  .nav-menu-toggle { display: inline-flex; }
  .site-nav .nav-menu {
    /* .site-nav ul turns rows at this breakpoint; the panel is a column and
       has to say so, or the five lines lay out across the header. */
    flex-direction: column;
    gap: 0;
    position: absolute;
    top: calc(100% + 0.5rem);
    left: -0.9rem;
    z-index: 50;
    min-width: 14rem;
    padding: 0.4rem 0;
    background: var(--shell);
    border: 1px solid var(--rule);
    border-radius: var(--radius-lg);
    box-shadow: var(--lift);
    /* Hidden rather than display:none so it can be transitioned, and
       inert to the keyboard while closed. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(-4px);
    transition: opacity var(--motion-fast) var(--ease),
                transform var(--motion-fast) var(--ease),
                visibility 0s linear var(--motion-fast);
  }
  .site-nav .nav-menu li:first-child { border-top: 0; }
  .site-nav .nav-menu a {
    display: block;
    padding: 0.55rem 1rem;
    font-size: 0.92rem;
    color: var(--ink);
    min-height: 44px;
    display: flex;
    align-items: center;
  }
  .site-nav .nav-menu a:hover { background: var(--sand-deep); color: var(--purple); }
  .site-nav .nav-menu a::after { display: none; }

  .nav-has-menu:hover .nav-menu,
  .nav-has-menu:focus-within .nav-menu,
  .nav-menu-toggle[aria-expanded="true"] + .nav-menu,
  .nav-parent:has(.nav-menu-toggle[aria-expanded="true"]) ~ .nav-menu {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition-delay: 0s;
  }
}

/* ==========================================================================
   8. Breadcrumbs
   ========================================================================== */

.breadcrumbs { padding-block: 1rem 0; font-size: 0.85rem; color: var(--ink-faint); }
.breadcrumbs ol { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.breadcrumbs li + li::before { content: "/"; margin-right: 0.35rem; color: var(--rule-strong); }
.breadcrumbs a { color: var(--ink-soft); }

/* ==========================================================================
   9. Hero
   ========================================================================== */

.hero {
  /* A shallow vertical gradient rather than a flat fill. Both stops are brand
     purples and the range is small enough that it reads as depth rather than
     as a gradient, which is the difference between considered and decorated.
     
     PLACEHOLDER IMAGE — static/img/office/hero.jpg is a test file, there to
     prove the plumbing rather than to be looked at. Replace it and nothing
     else changes; see static/img/office/README.md for the size.
     
     The photograph sits UNDER the gradient, and the gradient stays almost
     opaque: the picture is there to stop the panel reading as flat colour,
     not to be seen. Anything more and the headline stops clearing contrast,
     which is checked by the a11y audit and is not negotiable. */
  background:
    linear-gradient(168deg, rgba(52, 24, 75, 0.94) 0%, rgba(36, 16, 51, 0.97) 62%),
    var(--purple-deep) url("../img/office/hero.1ec0f8904699.jpg") center / cover no-repeat;
  color: #fff;
  position: relative;
  overflow: hidden;
}
/* The flat fill is kept as the fallback beneath, so a failed or slow image
   leaves the hero looking exactly as it did before rather than washing out. */
@media (prefers-reduced-data: reduce) {
  .hero { background: linear-gradient(168deg, var(--purple-deep) 0%, var(--purple-ink) 62%); }
}
/* A single gold hairline across the top of the hero. That is the whole
   decorative budget for this page. */
.hero::before {
  content: "";
  position: absolute; inset: 0 0 auto 0;
  height: 3px;
  background: linear-gradient(90deg, var(--gold) 0%, var(--gold) 22%, transparent 22%);
}

.hero-inner { padding-block: clamp(3rem, 2rem + 6vw, 6rem); }
.hero-body { max-width: 44rem; }
/* margin-inline stays at 0 here: the content aligns to the page grid rather
   than re-centring inside its own narrower box. */

/* Once there is room for two columns the proof sits beside the headline
   instead of under it. Below that width the hero was already the right shape
   and the tiles simply follow the buttons. */
@media (min-width: 62rem) {
  .hero-inner {
    display: grid;
    grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.75fr);
    gap: clamp(2.5rem, 1rem + 4vw, 4.5rem);
    align-items: center;
  }
  .hero-body { max-width: none; }
}
.hero h1 { color: #fff; }
.hero-lede { color: #D9CFE2; margin-top: 1.25rem; max-width: 54ch; font-size: clamp(1.05rem, 1rem + 0.4vw, 1.25rem); line-height: 1.55; }

.hero-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 2rem; }
.hero-actions .button { flex: 1 1 auto; min-width: 13rem; }
@media (min-width: 32rem) { .hero-actions .button { flex: 0 0 auto; } }

.hero-note {
  margin-top: 2rem; padding-top: 1.25rem;
  border-top: 1px solid rgba(255, 255, 255, 0.16);
  font-size: 0.9rem; color: #B4A5C1; max-width: 56ch;
}

/* ==========================================================================
   10. Proof strip
   ========================================================================== */

.proof { background: var(--shell); border-bottom: 1px solid var(--rule); }
.proof-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1.5rem 1rem;
  padding-block: 2rem;
}
@media (min-width: 48rem) { .proof-grid { grid-template-columns: repeat(4, 1fr); gap: 2rem; } }

.proof-item { border-left: 2px solid var(--gold); padding-left: 0.85rem; }
.proof-figure {
  display: block;
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 60;
  font-size: clamp(1.6rem, 1.3rem + 1.2vw, 2.1rem);
  font-weight: 600;
  line-height: 1.1;
  color: var(--purple);
}
.proof-label { display: block; font-size: 0.85rem; color: var(--ink-soft); margin-top: 0.15rem; }

/* The strip's dark colourway, used in the hero. The figure goes white rather
   than gold: gold reads well here, but four gold numerals would spend the
   whole page's accent budget in the first screen. The gold stays on the rule,
   which is the job it does everywhere else. */
.hero-proof { margin-top: 2.5rem; }
.hero-proof .proof-figure { color: #fff; }
.hero-proof .proof-label { color: #B4A5C1; }
.hero-proof .proof-item { border-left-color: var(--gold); }

/* Two of the four tiles carry a phrase rather than a number. They keep the
   display face so the row scans as one thing, a step down in size so a long
   phrase does not out-shout a figure. */
.proof-figure-short {
  font-size: clamp(1.05rem, 0.95rem + 0.5vw, 1.3rem);
  line-height: 1.25;
  font-variation-settings: 'opsz' 24;
}

@media (min-width: 62rem) {
  .hero-proof { margin-top: 0; }
  .hero-proof.proof-grid { grid-template-columns: repeat(2, 1fr); gap: 1.75rem 1.25rem; padding-block: 0; }
}

/* ==========================================================================
   11. Steps
   ========================================================================== */

.steps { counter-reset: step; display: grid; gap: 1.75rem; }
/* Column counts chosen so the last row is never left with a single item:
   five steps go 5-up, then 3+2, then 2 and 1. auto-fit gave 4+1 at 1024. */
@media (min-width: 44rem) { .steps { grid-template-columns: repeat(3, 1fr); gap: 2rem; } }
@media (min-width: 72rem) { .steps { grid-template-columns: repeat(5, 1fr); gap: 1.5rem; } }

/* Numbering is appropriate here because this genuinely is a sequence. */
.steps li { counter-increment: step; }
.steps li::before {
  content: counter(step);
  display: flex; align-items: center; justify-content: center;
  width: 2.1rem; height: 2.1rem;
  margin-bottom: 0.85rem;
  border: 1.5px solid var(--purple);
  border-radius: 50%;
  font-family: var(--font-display);
  font-size: 0.95rem; font-weight: 600;
  color: var(--purple);
}
.steps h3 { margin-bottom: 0.3rem; }
.steps p { color: var(--ink-soft); font-size: 0.95rem; }

/* --- The process, drawn as one -------------------------------------------
   The connector is a per-step rule reaching to the next marker, not a single
   line behind the row: at the three-column breakpoint five steps leave two on
   the second row, and one rule across the block would run into empty space.
   So it is drawn where it is true — down the column on a phone, across the
   row once all five fit — and left off in between. */

.steps-linked li { position: relative; }
.steps-linked li::before { background: var(--sand); position: relative; z-index: 1; }

/* On a phone the whole thing is a timeline: the marker sits in a gutter of
   its own and the step's words are indented past it, so the connector has an
   empty column to run down.

   It did not, and the line ran straight through the copy — down the left edge
   of "Your vision", "Assess", "Design". The marker is display:flex, so it was
   a block at the top of the item with the text starting back at zero beneath
   it, while the connector was drawn as though the two sat side by side. It
   was only ever right at 72rem, where the row genuinely is horizontal.

   None of the nine audits looks for a rule crossing a word. They check
   overflow, targets, contrast and text size, and every one of them passed on
   this screen. */
@media (max-width: 43.99rem) {
  .steps-linked li { padding-left: 3.4rem; min-height: 2.1rem; }
  .steps-linked li::before {
    position: absolute;
    left: 0;
    top: 0;
    margin-bottom: 0;
  }
  .steps-linked li:not(:last-child)::after {
    content: "";
    position: absolute;
    /* Half a pixel back, so a 1px rule is centred under a 2.1rem marker. */
    left: calc(1.05rem - 0.5px);
    top: 2.1rem;
    width: 1px;
    height: calc(100% - 2.1rem + 1.75rem);
    background: var(--rule-strong);
  }
}

@media (min-width: 72rem) {
  .steps-linked li:not(:last-child)::after {
    content: "";
    position: absolute;
    left: 2.1rem;
    top: 1.05rem;
    width: calc(100% - 2.1rem + 1.5rem);
    height: 1px;
    background: var(--rule-strong);
  }
}

/* The band behind this section is sand-deep, so the marker's own ground has
   to match or the connector shows through the circle. */
.band-sand .steps-linked li::before { background: var(--sand-deep); }

@media (prefers-reduced-motion: no-preference) and (hover: hover) {
  .steps-linked li::before {
    transition: background-color var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                transform var(--motion) var(--ease-out);
  }
  .steps-linked li:hover::before,
  .band-sand .steps-linked li:hover::before {
    background: var(--purple);
    color: #fff;
    transform: scale(1.12);
  }
  .steps-linked li h3 { transition: color var(--motion-fast) var(--ease); }
  .steps-linked li:hover h3 { color: var(--purple); }
}

/* ==========================================================================
   12. Job board
   ========================================================================== */

.jobs-layout { display: grid; gap: 2rem; padding-block: 2rem 3.5rem; }
@media (min-width: 60rem) {
  .jobs-layout { grid-template-columns: 16.5rem 1fr; gap: 3rem; align-items: start; }
  .job-filters { position: sticky; top: 88px; }
  /* Always open on wider screens: there is room, and the summary becomes a
     plain heading rather than a control. */
  .filters-summary { cursor: default; margin-bottom: 1rem; padding-bottom: 0.85rem; border-bottom: 1px solid var(--rule); pointer-events: none; }
  .filters-summary::after { display: none; }
}

.job-filters {
  background: var(--shell);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: 1.25rem;
}
.filters-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  min-height: 44px;
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 24;
  font-size: 1.05rem;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}
.filters-summary::-webkit-details-marker { display: none; }
.filters-summary::after {
  content: "";
  width: 0.55rem; height: 0.55rem;
  border-right: 2px solid var(--ink-soft);
  border-bottom: 2px solid var(--ink-soft);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 0.16s ease;
  flex: none;
}
.job-filters[open] .filters-summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.job-filters[open] .filters-summary { margin-bottom: 1rem; padding-bottom: 0.85rem; border-bottom: 1px solid var(--rule); }

.filters-active {
  font-family: var(--font-body);
  font-size: 0.75rem; font-weight: 600;
  padding: 0.1rem 0.5rem;
  border-radius: 2px;
  background: var(--gold);
  color: var(--purple-ink);
}
.job-filters .field + .field { margin-top: 0.9rem; }
.job-filters .button { margin-top: 1.25rem; }
.clear-filters {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 44px;
  margin-top: 0.5rem;
  font-size: 0.9rem;
}

.result-count { color: var(--ink-soft); font-size: 0.925rem; margin-bottom: 0.5rem; }

.job-list { border-top: 1px solid var(--rule); }

/* Rows, not cards. Fifty of these on a phone need density and hierarchy, and
   fifty shadowed boxes give neither. */
/* The row keeps its density — fifty of these on a phone is the case that
   matters — but the whole row is the target, not the twelve words of the
   title, and the state lives on the article rather than the li because the
   article is the box that bleeds past the text to make the highlight.
   Everything to do with hover and focus is here, in one place. */
.job-row { border-bottom: 1px solid var(--rule); }
.job-row > article {
  position: relative;
  padding-block: 1.15rem;
  padding-inline: 0.75rem;
  /* Bleeds the highlight outward while the text stays on the page grid. */
  margin-inline: -0.75rem;
  border-radius: var(--radius);
  transition: background-color var(--motion-fast) var(--ease);
}
.job-row > article::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
  border-radius: var(--radius) 0 0 var(--radius);
  background: var(--gold);
  opacity: 0;
  transition: opacity var(--motion-fast) var(--ease);
}
.job-row-title { font-size: 1.15rem; margin-bottom: 0.1rem; }
.job-row-title a { color: var(--ink); text-decoration: none; }
/* The whole row, not twelve words of title. */
.job-row-title a::after { content: ""; position: absolute; inset: 0; }
.job-row-title a:hover { color: var(--purple); text-decoration: none; }

/* focus-within, not :focus — the focus lands on the anchor inside, and the
   row is what should read as selected. A keyboard user tabbing the board
   otherwise sees only a ring around the title. */
.job-row:hover > article,
.job-row:focus-within > article { background: var(--sand-deep); }
.job-row:hover > article::before,
.job-row:focus-within > article::before { opacity: 1; }
.job-row:hover .job-row-title a,
.job-row:focus-within .job-row-title a { color: var(--purple); }

.job-row-employer { color: var(--ink-soft); font-size: 0.925rem; }

/* Candidates scan for two things: where it is and what it pays. Those two
   carry the weight; work type and experience level are qualifiers and are
   set back so they do not compete. */
.job-row-meta {
  display: flex; flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 0.9rem;
  margin-top: 0.6rem;
  font-size: 0.85rem; color: var(--ink-faint);
}
.job-row-meta .meta-location { font-weight: 600; color: var(--ink); font-size: 0.9rem; }
.job-row-meta .meta-salary { font-weight: 600; color: var(--ink); font-size: 0.9rem; }
/* A hairline between the two qualifiers and the two facts, so the eye can
   tell which is which without reading them. */
.job-row-meta .meta-salary { margin-left: auto; }
@media (max-width: 34rem) { .job-row-meta .meta-salary { margin-left: 0; } }

.job-row-posted { margin-top: 0.45rem; font-size: 0.8rem; color: var(--ink-faint); }
.closing-soon {
  display: inline-block; margin-left: 0.4rem;
  padding: 0.05rem 0.45rem;
  border: 1px solid var(--critical); border-radius: 2px;
  color: var(--critical); font-weight: 600; font-size: 0.75rem;
}

.pagination {
  display: flex; gap: 1.25rem; align-items: center; justify-content: center;
  padding-block: 2rem; font-size: 0.95rem;
}
.pagination span { color: var(--ink-soft); }

.empty-state {
  background: var(--shell);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: clamp(1.5rem, 1rem + 2vw, 2.5rem);
  text-align: center;
}
.empty-state h2 { margin-bottom: 0.5rem; }
.empty-state p { color: var(--ink-soft); max-width: 46ch; margin-inline: auto; }
.empty-state .button { margin-top: 1.25rem; }

/* ==========================================================================
   13. Job detail
   ========================================================================== */

.job-detail { padding-block: 1.5rem 3.5rem; max-width: 48rem; }
.job-detail-head { padding-bottom: 1.5rem; border-bottom: 1px solid var(--rule); }
.job-detail-employer { color: var(--ink-soft); font-size: 1.05rem; margin-top: 0.4rem; }
.employer-logo { margin-top: 1.25rem; width: auto; max-height: 56px; }

.job-meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: 1.25rem;
  padding-block: 1.5rem;
  border-bottom: 1px solid var(--rule);
}
.job-meta dt { font-size: 0.775rem; color: var(--ink-faint); }
.job-meta dd { font-weight: 600; margin-top: 0.15rem; }

.job-body h2 { margin-top: 2.25rem; }
.job-body ul { margin-top: 0.75rem; }
.job-body li::marker { color: var(--gold-deep); }

.apply {
  margin-top: 2.5rem;
  background: var(--shell);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: clamp(1.25rem, 1rem + 1.5vw, 2rem);
}
.apply-intro { color: var(--ink-soft); margin-bottom: 1.5rem; }
.similar { margin-top: 3rem; }

/* ==========================================================================
   14. Forms

   Someone applying for a job is often doing it on a phone, on mobile data,
   possibly at the end of a shift. The form should feel calm and obvious: one
   column, generous targets, a clear focus state, and errors that say what to
   do rather than what went wrong.
   ========================================================================== */

.field { margin-bottom: 1.4rem; }

.field > label {
  display: block;
  font-size: 0.9rem;
  font-weight: 600;
  letter-spacing: 0.005em;
  color: var(--ink);
  margin-bottom: 0.45rem;
}

.field input, .field select, .field textarea,
.job-filters input, .job-filters select {
  width: 100%;
  padding: 0.85rem 0.95rem;
  min-height: 52px;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--shell);
  color: var(--ink);
  /* 16px minimum, or iOS zooms the page when a field takes focus. */
  font-size: 1rem;
  line-height: 1.4;
  transition: border-color 0.14s ease, box-shadow 0.14s ease, background-color 0.14s ease;
}

.field input::placeholder, .field textarea::placeholder { color: var(--ink-faint); }

.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--ink-faint); }

/* One considered focus state rather than the browser's default ring: the
   border takes the brand colour and a soft halo lifts the field off the page. */
.field input:focus, .field select:focus, .field textarea:focus,
.job-filters input:focus, .job-filters select:focus {
  outline: none;
  border-color: var(--purple);
  box-shadow: 0 0 0 3px rgba(90, 41, 130, 0.14);
}
/* Keyboard users still get the visible outline; pointer users get the halo. */
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible {
  outline: 2px solid var(--purple);
  outline-offset: 1px;
}

.field textarea { min-height: 8rem; resize: vertical; line-height: 1.6; padding-block: 0.8rem; }

/* --- The dropdowns --------------------------------------------------------
   A <select> with appearance: none is a custom control as far as the browser
   is concerned, and two things then go wrong on a phone that never go wrong on
   a desktop, both of them in the options list rather than the closed field:

   1. Android Chrome's Auto Dark Theme inverts any page that has not said it is
      a light page. It leaves the closed control looking plausible and repaints
      the popup, and an appearance: none select is exactly the case its
      heuristics handle worst — dark text on a dark sheet, which reads as an
      empty list.
   2. On iOS a select inside a dark UI renders the popup on the system's dark
      sheet while each option keeps the colour it inherits from the page. Dark
      on dark again.

   color-scheme: light on :root fixes the first by opting out of auto-darkening,
   and the explicit option colours below fix the second by not leaving the
   popup's colours to be inherited. Neither changes anything on a browser that
   was already rendering these correctly. */
.field select option,
.job-filters select option {
  background-color: var(--shell);
  color: var(--ink);
}

.field select {
  appearance: none;
  padding-right: 2.5rem;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--ink-soft) 50%),
    linear-gradient(135deg, var(--ink-soft) 50%, transparent 50%);
  background-position: calc(100% - 20px) calc(50% + 2px), calc(100% - 14px) calc(50% + 2px);
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  cursor: pointer;
}

/* The file input is the least pleasant native control on the web, so the
   label is styled as a dropzone and the real input sits inside it. */
.field-file .file-control {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 1rem 1.1rem;
  border: 1.5px dashed var(--rule-strong);
  border-radius: var(--radius);
  background: var(--sand);
  transition: border-color 0.14s ease, background-color 0.14s ease;
}
.field-file .file-control:hover { border-color: var(--purple); background: var(--shell); }
.field-file input[type="file"] {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  min-height: 0; padding: 0;
  opacity: 0;
  cursor: pointer;
}
.field-file .file-icon {
  flex: none;
  width: 2.25rem; height: 2.25rem;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--shell);
  border: 1px solid var(--rule);
  color: var(--purple);
}
.field-file .file-copy { min-width: 0; }
.field-file .file-prompt { font-weight: 600; font-size: 0.95rem; }
.field-file .file-formats { font-size: 0.825rem; color: var(--ink-soft); }
.field-file.has-file .file-control { border-style: solid; border-color: var(--positive); background: var(--shell); }
.field-file.has-file .file-icon { color: var(--positive); border-color: var(--positive); }

.file-chosen {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--positive);
  margin-top: 0.5rem;
  overflow-wrap: anywhere;
}

.field-help { font-size: 0.85rem; color: var(--ink-soft); margin-top: 0.4rem; }

/* Errors read as instructions, not alarms. */
.field-message {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--critical);
  margin-top: 0.45rem;
}
.field-message::before {
  content: "";
  flex: none;
  width: 1rem; height: 1rem;
  margin-top: 0.15rem;
  border-radius: 50%;
  background: var(--critical);
  /* An exclamation mark cut out of the dot, so the meaning survives in
     greyscale and for anyone who cannot distinguish the colour. */
  -webkit-mask: radial-gradient(circle at 50% 78%, #000 0 1.2px, transparent 1.2px),
                linear-gradient(#000 0 0) 50% 22% / 2px 34% no-repeat;
  mask: radial-gradient(circle at 50% 78%, #000 0 1.2px, transparent 1.2px),
        linear-gradient(#000 0 0) 50% 22% / 2px 34% no-repeat;
  -webkit-mask-composite: source-over;
  mask-composite: add;
}
.field-error input, .field-error select, .field-error textarea { border-color: var(--critical); }
.field-error input:focus, .field-error select:focus, .field-error textarea:focus {
  box-shadow: 0 0 0 3px rgba(163, 35, 48, 0.14);
}

.required { color: var(--critical); font-weight: 400; }

/* Consent sits in its own panel so it reads as a decision, not a footnote. */
.field-checkbox {
  background: var(--sand-deep);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 1.1rem 1.2rem;
  margin-block: 1.75rem 1.5rem;
  transition: border-color 0.14s ease, background-color 0.14s ease;
}
.field-checkbox:focus-within { border-color: var(--purple); background: var(--shell); }
.field-checkbox > label {
  display: flex;
  gap: 0.8rem;
  align-items: flex-start;
  font-weight: 400;
  font-size: 0.925rem;
  line-height: 1.55;
  margin-bottom: 0;
  cursor: pointer;
}
.field-checkbox input {
  width: 22px; height: 22px;
  min-height: 0;
  flex: none;
  margin-top: 0.1rem;
  accent-color: var(--purple);
  cursor: pointer;
}
.field-checkbox.field-error { border-color: var(--critical); background: var(--shell); }

/* Grouping and rhythm */
.stacked-form { max-width: 33rem; }
.stacked-form .button, .apply .button { margin-top: 0.25rem; }
/* A submit button narrower than the fields above it looks unfinished on a
   phone, where every other control runs the full width. */
.stacked-form button[type="submit"] { width: 100%; }
@media (min-width: 34rem) { .stacked-form button[type="submit"] { width: auto; } }
.form-footnote { font-size: 0.85rem; color: var(--ink-soft); margin-top: 1rem; }

/* Two short fields sit side by side once there is room for them. */
@media (min-width: 34rem) {
  .field-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.25rem; }
}

/*
 * The honeypot must never be visible, whatever else changes.
 *
 * Matched two ways on purpose: by class, and by the field name from settings.
 * If either the class or the setting is changed without the other, the field
 * stays hidden rather than appearing in the middle of a form.
 */
/* The bot trap. Not rendered at all, rather than moved off screen: browser
   autofill fills off-screen fields and skips only unrendered ones, and an
   off-screen trap was catching real applicants. See HoneypotWidget. */
.honeypot-field,
input[name="extra_notes"] {
  display: none !important;
}

/* ==========================================================================
   15. Notices
   ========================================================================== */

.notice {
  padding: 1.15rem 1.35rem;
  border-radius: var(--radius);
  border-left: 3px solid var(--purple);
  background: var(--shell);
  margin-block: 1.25rem;
}
.notice h2 { font-size: 1.2rem; }
.notice p { color: var(--ink-soft); }
.notice p + p { margin-top: 0.6rem; }
.notice-success { border-left-color: var(--positive); }
.notice-error, .notice-closed { border-left-color: var(--critical); }
.closed-alternatives { margin-top: 0.75rem; padding-left: 1.25rem; list-style: revert; }
.closed-alternatives li + li { margin-top: 0.3rem; }

/* ==========================================================================
   16. FAQ — question-shaped headings with the answer immediately after,
   which is what answer engines extract.
   ========================================================================== */

/* The FAQ block is also the .wrap, whose margin-inline: auto centred it while
   every other section on the page stayed left-aligned. Constrain the list
   instead of the container. */
.faq { max-width: var(--wrap); }
.faq dl { max-width: var(--wrap-narrow); }
.faq dt {
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 24;
  font-size: 1.075rem; font-weight: 600;
  margin-top: 1.75rem;
  padding-top: 1.75rem;
  border-top: 1px solid var(--rule);
}
.faq dl > dt:first-child { border-top: 0; padding-top: 0; margin-top: 0; }
.faq dd { color: var(--ink-soft); margin-top: 0.5rem; }

/* ==========================================================================
   17. Lists and tables
   ========================================================================== */

.link-list { display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; }

.category-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0 1.5rem; }
.category-grid li { padding: 0.65rem 0; border-bottom: 1px solid var(--rule); font-size: 0.95rem; }

.placement-list li {
  display: flex; justify-content: space-between; gap: 0.5rem 1.5rem;
  flex-wrap: wrap; padding-block: 0.85rem;
  border-bottom: 1px solid var(--rule);
}
.placement-list time { color: var(--ink-faint); font-size: 0.9rem; }

.table-scroll { overflow-x: auto; margin-block: 1.25rem; }
.data-table { width: 100%; border-collapse: collapse; min-width: 28rem; }
.data-table th, .data-table td { text-align: left; padding: 0.7rem 0.85rem; border-bottom: 1px solid var(--rule); }
.data-table th { background: var(--sand-deep); font-size: 0.875rem; }

.placeholder-note {
  color: var(--ink-faint); font-style: italic;
  padding: 1rem 1.25rem; border: 1px dashed var(--rule-strong); border-radius: var(--radius);
}
.updated { color: var(--ink-faint); font-size: 0.9rem; margin-top: 0.75rem; }

/* --- Category hub ---------------------------------------------------------
   The introduction used to be a narrow column inside a centred .wrap, which
   centred it too — it sat indented from both edges and read as a mistake. It
   is now a grid cell, so it starts on the page grid like everything else, and
   the rail gives the leftover column a job.

   The rail is also the differentiator: before it, a hub looked like the job
   board with different words. */

.hub-layout { display: grid; gap: 2rem; }
@media (min-width: 62rem) {
  .hub-layout { grid-template-columns: minmax(0, 1fr) 18rem; gap: 3.5rem; align-items: start; }
}
.hub-intro { max-width: var(--wrap-narrow); }
.hub-intro p + p { margin-top: 1rem; }

.hub-rail {
  padding: 1.5rem;
  background: var(--sand-deep);
  border-radius: var(--radius-lg);
}
.hub-rail-count {
  font-family: var(--font-display);
  font-size: 1.05rem;
  line-height: 1.35;
  margin: 0 0 1rem;
  padding-bottom: 1rem;
  border-bottom: 2px solid var(--gold);
}
.hub-rail-count strong { font-weight: 600; color: var(--purple); }
.hub-rail-heading {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 0.5rem;
}
.hub-rail-towns { display: grid; gap: 0.35rem; margin-bottom: 1.25rem; }
.hub-rail-towns li { display: flex; align-items: center; gap: 0.4rem; font-size: 0.92rem; }
.hub-rail-towns .icon { color: var(--purple); flex: none; }
.hub-rail .action-list { margin-top: 0; }
.hub-rail .action-title { font-size: 0.95rem; }

/* ==========================================================================
   18. Contact
   ========================================================================== */

.contact-layout { display: grid; gap: 2.5rem; }
@media (min-width: 52rem) { .contact-layout { grid-template-columns: 1fr 1.2fr; gap: 4rem; align-items: start; } }
.contact-details address { font-style: normal; line-height: 1.8; margin-bottom: 1.25rem; }
.contact-list li { padding-block: 0.35rem; border-bottom: 1px solid var(--rule); }

/* A list where every row is a link needs less decoration than a link inside a
   sentence. There is no surrounding prose here for a link to be mistaken for,
   and each row already carries an icon and a hairline of its own, so the two
   numbers are left undecorated: a phone number that looks like a phone number.
   The address keeps its underline, because an address set in body text is the
   one item on this list a reader could otherwise take for plain type. All
   three underline on hover and on focus. */
.contact-list a { text-decoration: none; }
.contact-list a[href^="mailto:"] { text-decoration: underline; }
.contact-list a:hover,
.contact-list a:focus-visible { text-decoration: underline; }

/* ==========================================================================
   19. Closing CTA
   ========================================================================== */

.closing-cta { background: var(--purple-ink); color: #fff; }
.closing-cta h2 { color: #fff; }
/* 42ch, not 34. This project's own layout audit calls 42 characters the floor
   for running prose and 34 was under it: in a half-width column with 150px
   going spare the paragraph broke early and sat with a ragged edge and a hole
   beside it. */
.closing-cta p { color: #D9CFE2; margin-block: 0.6rem 1.5rem; max-width: 42ch; }
.closing-grid { display: grid; gap: 2.5rem; padding-block: clamp(2.5rem, 2rem + 3vw, 4rem); }
@media (min-width: 48rem) { .closing-grid { grid-template-columns: 1fr 1fr; gap: 4rem; } }
.closing-grid > div + div { padding-top: 2.5rem; border-top: 1px solid rgba(255,255,255,0.16); }
@media (min-width: 48rem) {
  .closing-grid > div + div { padding-top: 0; padding-left: 4rem; border-top: 0; border-left: 1px solid rgba(255,255,255,0.16); }
}

/* ==========================================================================
   20. Footer
   ========================================================================== */

.site-footer {
  background: var(--purple-ink);
  color: #C9BDD4;
  font-size: 0.925rem;
  flex-shrink: 0;
}
/* No resting underline down here. Every link in the footer sits in a list, a
   contact row or the legal strip -- not one of them is inside a sentence -- so
   there is nothing for a link to be mistaken for, and twenty faint underlines
   made the whole footer read as small print. They colour to gold and draw an
   underline on hover and on focus instead.

   Drawn as a background gradient rather than text-decoration so it can grow:
   the same gesture a link in running prose already uses. */
.site-footer a:not(.link-arrow):not(.button) {
  color: #fff;
  text-decoration: none;
  background-image: linear-gradient(var(--gold), var(--gold));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0 1px;
  padding-bottom: 2px;
  transition: color var(--motion-fast) var(--ease),
              background-size var(--motion-fast) var(--ease);
}
.site-footer a:not(.link-arrow):not(.button):hover,
.site-footer a:not(.link-arrow):not(.button):focus-visible {
  color: var(--gold);
  background-size: 100% 1px;
}

.footer-grid { display: grid; gap: 2.25rem; padding-block: clamp(2.5rem, 2rem + 2vw, 3.5rem) 2rem; }
/* Stacked: the second link column needs breathing room from the first. */
@media (max-width: 43.999rem) { .footer-links + .footer-links { margin-top: 1.75rem; } }
@media (min-width: 44rem) { .footer-grid { grid-template-columns: 1fr 1fr; gap: 2.5rem; } }
@media (min-width: 62rem) { .footer-grid { grid-template-columns: repeat(3, 1fr); gap: 3rem 2.5rem; } }

/* The brand block takes a row of its own rather than the first column.
   Measured, and the measurement is the whole reason it moved: in the 1.5fr
   column it used to sit in, the tagline's box was 263px wide at a 1024px
   viewport. Holding that string on one line inside 263px needs a font under
   12px, and 12px is this site's floor for text. No size satisfied both, so the
   column gave way rather than the line. It also gives the tagline room to read
   as the statement it is, which is the point of putting it there. */
@media (min-width: 44rem) { .footer-about { grid-column: 1 / -1; } }

/* Was 32ch, then 44ch. Across the full width it can take the ceiling. */
.footer-about p { max-width: 74ch; line-height: 1.6; }

.site-footer h2 {
  font-family: var(--font-body);
  font-size: 0.775rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #fff;
  margin-bottom: 0.85rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.18);
}
.site-footer address { font-style: normal; line-height: 1.75; }
/* The business name, on whichever ground it sits. Scoping the white to the
   footer matters: the same class is used on the contact page, where white on
   sand rendered it invisible. */
.org-name { font-weight: 600; color: var(--ink); }
.site-footer .org-name { color: #fff; }

.footer-contact, .footer-links ul { display: flex; flex-direction: column; gap: 0.45rem; }
.footer-contact { margin-top: 0.85rem; }


/* --- The careers column, sorted by reader ---------------------------------
   Two audiences, labelled, rather than six flat links an employer had to read
   past to find the one that was for them. The category hubs sit behind a
   disclosure, which is the same <details> control the FAQ uses on six pages --
   one control on the site, not two. */

.footer-audience {
  /* 0.78rem, not 0.72. 0.72rem computes to 11.52px and the floor is 12 — the
     same slip as .service-index-num, caught by the same audit. */
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #9B8DA8;
  margin-top: 1.2rem;
  margin-bottom: 0.5rem;
}
.footer-careers .footer-audience:first-of-type { margin-top: 0.1rem; }

.footer-tree > summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  /* Matches the links either side of it rather than standing 44px tall among
     22px rows. Still clears the 24px minimum for a control. */
  padding-block: 0.2rem;
  color: #fff;
  cursor: pointer;
  list-style: none;
}
.footer-tree > summary::-webkit-details-marker { display: none; }
.footer-tree > summary:hover .footer-tree-label,
.footer-tree > summary:focus-visible .footer-tree-label { color: var(--gold); }
.footer-tree > summary:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 3px;
  border-radius: var(--radius);
}
.footer-tree-label { transition: color var(--motion-fast) var(--ease); }

/* The same chevron the services menu uses in the header. */
.footer-tree-marker {
  flex: none;
  width: 0.4rem; height: 0.4rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform var(--motion-fast) var(--ease);
}
.footer-tree[open] > summary .footer-tree-marker {
  transform: rotate(-135deg) translate(-2px, -2px);
}

.footer-tree > ul {
  margin-bottom: 0.35rem;
  margin-top: 0.55rem;
  margin-left: 0.15rem;
  padding-left: 0.85rem;
  border-left: 1px solid rgba(255, 255, 255, 0.2);
}

.footer-legal {
  border-top: 1px solid rgba(255, 255, 255, 0.18);
  padding-block: 1.5rem 2.5rem;
}

/* The no-fees line is the single most important sentence on the site for a
   candidate, so it is set apart rather than buried in small print. */
.no-fees {
  background: rgba(255, 218, 9, 0.09);
  border-left: 3px solid var(--gold);
  padding: 0.9rem 1.15rem;
  border-radius: 0 var(--radius) var(--radius) 0;
  color: #F2ECF6;
  /* It runs the width of the footer's legal row, which is 70rem. At 62ch it
     used a little over half of that and wrapped with a third of the band
     empty beside it. 82ch keeps it inside the 88-character ceiling. */
  max-width: 82ch;
  margin-bottom: 1.5rem;
}
.no-fees strong { color: var(--gold); }

.footer-legal ul { display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; margin-bottom: 1rem; font-size: 0.9rem; }
.odpc, .copyright { font-size: 0.85rem; color: #9B8DA8; }
.copyright { margin-top: 0.5rem; }

/* The legal row sits to the right from the tablet breakpoint up.
   Not on a phone: in one column the links wrap across two or three lines, and
   a ragged left edge on wrapped links is harder to read than a ragged right
   one. The no-fees panel stays where it is either way — it is a bordered
   callout with a gold bar down its left edge, and that bar belongs against
   the page's own left edge, not floating in the middle of the footer. */
@media (min-width: 48rem) {
  .footer-legal ul { justify-content: flex-end; }
  .footer-legal .odpc,
  .footer-legal .copyright { text-align: right; }
}

/* ==========================================================================
   21. Print — someone will print a job listing to take to an interview.
   ========================================================================== */

@media print {
  .site-header, .site-footer, .breadcrumbs, .apply, .similar,
  .closing-cta, .skip-link, .nav-toggle { display: none !important; }
  body { background: #fff; color: #000; font-size: 11pt; }
  .wrap { max-width: none; padding: 0; }
  a { text-decoration: none; color: #000; }
  .job-detail a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; }
}

/* ==========================================================================
   22. Motion

   The rule here is that motion should be noticed only in its absence. Short
   durations, small distances, one easing curve, and nothing that moves while
   someone is trying to read.

   Everything is opt-in from a visible starting state: sections are fully
   visible by default and only hidden once JavaScript has confirmed it can
   reveal them again. If the script fails, nothing is stranded invisible.
   ========================================================================== */

:root {
  /* Two curves, each with a job.
     --ease decelerates gently: right for a hover or a colour change, where
     the eye is already on the element and a long tail would feel sluggish.
     --ease-out decelerates hard and late. It is what makes an entrance read
     as weighted rather than linear — the element arrives quickly, then
     settles, the way a physical object with mass does. Used for anything
     that travels. */
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

  --motion-fast: 140ms;
  --motion: 420ms;
  --motion-slow: 720ms;
}

/* Entrance.
   18px and a fractional scale, on the weighted curve, over a longer duration.
   The scale is 1.5% — far too small to read as zooming, but enough that the
   element resolves into place rather than sliding to a stop. The opacity runs
   shorter than the transform so the content is legible before it has finished
   settling. */
.reveal-ready [data-reveal] {
  opacity: 0;
  transform: translateY(18px) scale(0.985);
}
.reveal-ready [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--motion) var(--ease-out),
    transform var(--motion-slow) var(--ease-out);
}

/* Where a group arrives together, a small stagger reads as one movement
   rather than several. Runs to eight because the service, value and project
   grids hold six: with four, the first four arrived in sequence and the rest
   landed together, which reads as a stutter rather than a stagger. */
.reveal-ready [data-reveal] [data-reveal-child] { transition-delay: 0ms; }
.reveal-ready [data-reveal].is-revealed > *:nth-child(1) { transition-delay: 0ms; }
.reveal-ready [data-reveal].is-revealed > *:nth-child(2) { transition-delay: 45ms; }
.reveal-ready [data-reveal].is-revealed > *:nth-child(3) { transition-delay: 90ms; }
.reveal-ready [data-reveal].is-revealed > *:nth-child(4) { transition-delay: 135ms; }
.reveal-ready [data-reveal].is-revealed > *:nth-child(5) { transition-delay: 180ms; }
.reveal-ready [data-reveal].is-revealed > *:nth-child(6) { transition-delay: 225ms; }
.reveal-ready [data-reveal].is-revealed > *:nth-child(7) { transition-delay: 260ms; }
.reveal-ready [data-reveal].is-revealed > *:nth-child(8) { transition-delay: 290ms; }

/* Links and buttons: a colour change, and nothing that shifts layout. */
.button { transition: background-color var(--motion-fast) var(--ease),
                      color var(--motion-fast) var(--ease),
                      border-color var(--motion-fast) var(--ease); }

/* The header gains a hairline only once the page has scrolled beneath it. */
.site-header { transition: box-shadow var(--motion-fast) var(--ease); }
.site-header.is-stuck { box-shadow: 0 1px 0 var(--rule), 0 6px 20px rgba(31, 26, 34, 0.05); }

/* The mobile menu opens rather than appearing. */
.site-nav.is-open { animation: nav-open 200ms var(--ease); }
@keyframes nav-open {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

/* Success notices settle in, since they usually arrive on page load after a
   redirect and a static block can be missed entirely. */
.notice-success { animation: notice-in 360ms var(--ease); }
@keyframes notice-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* A disabled submit button should look deliberately held, not broken. */
.button:disabled { opacity: 0.72; cursor: progress; }

/*
 * Reduced motion: everything above collapses to nothing. This repeats the
 * blanket rule in section 4 on purpose, because these animations are defined
 * after it and would otherwise win on source order.
 */
@media (prefers-reduced-motion: reduce) {
  .reveal-ready [data-reveal],
  .reveal-ready [data-reveal].is-revealed {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  .site-nav.is-open, .notice-success { animation: none !important; }
}

/* The jump-to-form button at the top of a job listing. */
.apply-jump { margin-top: 1.5rem; }

/* A fragment target under a sticky header lands with the heading hidden behind
   it, so every anchored section reserves the header's height. */
:target { scroll-margin-top: 5.5rem; }

/* ===========================================================================
   Consultancy front door
   ---------------------------------------------------------------------------
   Components added when the site was repositioned from a recruitment agency to
   a hospitality consultancy. Everything below reuses the existing tokens: no
   new colours, no new typefaces.
   =========================================================================== */

/* --- The tagline ----------------------------------------------------------
   "Guiding Hospitality. Transforming Destinations." It appeared in the footer
   and as the About page's lede, which is to say almost nowhere. Dr Kangangi
   asked for it to be dominant on every page: it now stands above the title
   sitewide, and inside the dark head on the two pages that have one.

   Set in the display face and in purple, which nothing else on a light ground
   is — that is what makes it read as a statement rather than as a subtitle.
   Gold on the dark grounds, where #FFDA09 on #34184B measures 11.09:1. Gold on
   sand is 1.5:1 and never carries type.

   It replaced .hero-eyebrow, which read "REI Hospitality Kenya": the logo says
   that two inches above it, and the name is also in the h1, the title, the
   structured data and the footer. The tagline was in none of them. That rule
   is gone rather than left behind styled and used by nothing, which this
   stylesheet has done before. */
.site-tagline {
  font-family: var(--font-display);
  font-weight: 600;
  /* One line, at every width, which is what Dr Kangangi asked for.

     The size is measured, not calculated, and it cannot be calculated:
     Fraunces carries an optical-size axis, so the string's width in ems is not
     constant across sizes. A probe set at 100px made it 18.78em wide; the same
     string set at 20px measured 23.65em. So the largest size that holds one
     line was binary-searched on the real element in its real box at twelve
     viewport widths, and 3.8vw is the largest constant that stays under that
     curve everywhere. The cap stops it growing into a headline on a desktop.

     At 320px this computes to 12.16px against a measured ceiling of 12.18px
     and this site's 12px floor for text. That window is real but it is two
     hundredths of a pixel wide, which is why the tracking below is part of the
     fit rather than a flourish. Below about 316px the clamp holds 12px and the
     line wraps: the floor wins, because unreadable on one line is worse than
     readable on two. */
  font-size: clamp(0.75rem, 3.8vw, 1.32rem);
  line-height: 1.35;
  letter-spacing: -0.02em;
  color: var(--purple);
  margin-top: clamp(1.75rem, 1.25rem + 2vw, 2.5rem);
}
/* The tagline's row, which on a detail page also holds the way back out.

   Stacked by default: the tagline was sized by binary search to hold one line
   at every width, and that rule outranks this one, so a narrow screen gives
   the back link its own line rather than squeezing the tagline onto two.

   From 62rem there is room for both — the tagline stops growing at its 1.32rem
   cap around 556px, so above that it is a fixed ~500px against a container
   approaching 70rem, and the back link sits in space that was empty. */
/* The row carries the top margin, not the tagline. align-items: center
   centres an item's MARGIN box, so leaving the 2.5rem on the tagline sat it
   20px below the back link it was supposed to be level with. */
.tagline-row { margin-top: clamp(1.75rem, 1.25rem + 2vw, 2.5rem); }
.tagline-row > .site-tagline { margin-top: 0; margin-bottom: 0; }
.tagline-row .back-link { margin-top: 0.75rem; margin-bottom: 0; }
@media (min-width: 62rem) {
  .tagline-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.5rem;
  }
  .tagline-row .back-link { margin-top: 0; }
}

.site-tagline.on-dark,
.on-dark .site-tagline { color: var(--gold); }

/* The page title follows immediately, so it does not also need its own full
   top padding.

   Both selectors, and both are live: the tagline sits inside .tagline-row on
   every page using the standard block, and the bare form is kept for any page
   that renders the tagline on its own. Losing this rule when the tagline moved
   into a row pushed every h1 on the site 19px down — a site-wide change out of
   a two-line edit, and exactly the kind only a measurement catches. */
main > .tagline-row ~ .page-head,
main > .site-tagline ~ .page-head { padding-block-start: 1.1rem; }

/* Same curve, a larger cap: the hero's column is wide enough to carry it, and
   its measured ceiling at 1024 is 24.9px. */
.site-tagline-hero {
  margin-top: 0;
  margin-bottom: 1.1rem;
  font-size: clamp(0.75rem, 3.8vw, 1.45rem);
}

/* Set strong on About, as asked, and given the full width of the head to be
   strong in — see .page-head-split below. */
.site-tagline-strong {
  font-size: clamp(0.75rem, 3.8vw, 1.6rem);
  margin-top: 1.3rem;
  /* 700 needs about 4% more width than 600 for the same string, and at 320px
     there is not 4% to give: the ceiling for 700 there measures 11.74px, under
     the floor. So the extra weight starts where the room for it does. */
  font-weight: 600;
}
@media (min-width: 30rem) { .site-tagline-strong { font-weight: 700; } }

/* --- A dark page head ------------------------------------------------------
   The homepage's colour, on About. Same gradient as the hero with no
   photograph behind it: repeating the image would open both pages identically,
   and this head is shallower than a 16:9 crop wants. */
.page-head-dark {
  background: linear-gradient(168deg, var(--purple-deep) 0%, var(--purple-ink) 78%);
  color: #fff;
}
.page-head-dark h1 { color: #fff; }
.page-head-dark .prose p { color: #D9CFE2; }
.page-head-dark .page-head-split {
  padding-block: clamp(2.5rem, 1.75rem + 4vw, 4.5rem);
}

/* The positioning line under the hero. Wide measure, quiet weight: it is a
   statement to be read once, not a paragraph to be studied. */
.statement {
  background: var(--purple-ink);
  color: #EDE6F2;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.statement p {
  font-family: var(--font-display);
  font-size: clamp(1.1rem, 1rem + 0.7vw, 1.5rem);
  line-height: 1.45;
  max-width: 54ch;
  padding-block: clamp(1.75rem, 1.25rem + 2vw, 2.75rem);
  margin: 0;
}

/* --- Service cards ------------------------------------------------------- */

.service-grid {
  display: grid;
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  overflow: hidden;
  margin-top: 2rem;
}
@media (min-width: 40rem) { .service-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .service-grid { grid-template-columns: repeat(3, 1fr); } }

/* Hairline separators rather than gaps, so the block reads as one object. */
.service-card {
  background: var(--shell);
  padding: 1.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.service-card h3 { font-size: 1.15rem; margin: 0; }
.service-card p { color: var(--ink-soft); font-size: 0.95rem; margin: 0; }
.service-card-cta { background: var(--sand-deep); }

/* The heading holds the link, but the whole card is the hit area: a stretched
   pseudo-element over the card. The card needs a stacking context for it, and
   the text above it, or the ::after swallows selection. */
.service-card { position: relative; }
.service-card h3 a { color: inherit; text-decoration: none; }
.service-card h3 a::after { content: ""; position: absolute; inset: 0; }
.service-card h3 a:hover { color: var(--purple); text-decoration: none; }
.service-card p, .service-card .link-arrow { position: relative; }

/* The icon sits in its own tinted square rather than loose above the heading,
   which is what stops a grid of six cards reading as six paragraphs. */
.service-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius);
  background: var(--sand-deep);
  color: var(--purple);
  margin-bottom: 0.4rem;
}
.service-card-cta .service-icon { background: var(--shell); }

/* The CTA card's arrow is a span, not a link: the whole card already goes to
   the consultation page, and a second focusable element pointing at the same
   place is just a duplicate tab stop. It stays readable rather than
   aria-hidden, because it is the phrase that says what the card does. */
.service-card-cta .link-arrow { color: var(--purple); }

/* Padded to a 44px touch target rather than sitting at the text's own height.
 *
 * The rule under a text CTA is drawn on the label rather than left to
 * text-decoration. An underline on the link painted a second, detached
 * segment beneath the arrow — the arrow is a flex item, so it sits in its own
 * box and the decoration ran under it with a gap in between. Wrapping the
 * words in .link-arrow-label puts the rule exactly under the words and
 * nowhere else, and makes it something that can be animated. */
.link-arrow {
  font-weight: 500;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: auto;
  padding-block: 0.7rem;
  min-height: 44px;
  text-decoration: none;
  color: var(--purple);
}
.link-arrow:hover { text-decoration: none; }
.link-arrow-label {
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 1px;
  padding-bottom: 3px;
  transition: background-size var(--motion-fast) var(--ease);
}
.link-arrow::after {
  content: "→";
  transition: transform var(--motion-fast) var(--ease-out);
}
.link-arrow:hover .link-arrow-label,
.link-arrow:focus-visible .link-arrow-label { background-size: 100% 2px; }
.link-arrow:hover::after,
.link-arrow:focus-visible::after { transform: translateX(4px); }

/* On the dark grounds the CTA takes the gold it already uses for rules. */
.on-dark .link-arrow, .site-footer .link-arrow { color: var(--gold); }

/* --- The two moments on the homepage --------------------------------------
   Prose set as two panels with a stated stage, a line that carries the idea,
   and the detail under it. The point is that the section survives being read
   from four feet away, which two columns of running text does not. */

.moment-pair { display: grid; gap: 1.5rem; margin-top: 2rem; }
@media (min-width: 52rem) { .moment-pair { grid-template-columns: repeat(2, 1fr); gap: 2rem; } }

.moment {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 1.75rem 1.75rem 1.25rem 2rem;
  background: var(--sand);
  border-radius: var(--radius-lg);
  border: 1px solid var(--rule);
  transition:
    border-color var(--motion) var(--ease-out),
    box-shadow var(--motion) var(--ease-out);
}
/* The heading is the link and the whole panel is the target, the same
   stretched pseudo-element the service cards use. Everything else in the
   panel is lifted above it so the copy stays selectable. */
.moment h3 a { color: inherit; text-decoration: none; }
.moment h3 a::after { content: ""; position: absolute; inset: 0; }
.moment > p, .moment .link-arrow { position: relative; }
/* The gold marker is a bar rather than a border, so it can grow on hover
   without the panel's own edge moving. */
.moment::before {
  content: "";
  position: absolute;
  left: 0; top: 1.75rem; bottom: 1.75rem;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: var(--gold);
  transition: top var(--motion) var(--ease-out), bottom var(--motion) var(--ease-out);
}
.moment-stage {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 0.4rem;
}
.moment h3 { font-size: clamp(1.2rem, 1.1rem + 0.5vw, 1.45rem); margin: 0; }
/* The one line that has to land. Larger than the body, and it is the only
   thing in the panel set at this size. */
.moment-line {
  font-size: clamp(1.02rem, 0.98rem + 0.3vw, 1.15rem);
  line-height: 1.5;
  color: var(--ink);
  margin: 0.75rem 0 0;
}
/* :last-of-type, not :last-child — the CTA is the last child now. */
.moment > p:last-of-type { color: var(--ink-soft); font-size: 0.95rem; margin: 0.9rem 0 0; }
.moment .link-arrow { font-size: 0.95rem; }

/* --- The panel responds as one -------------------------------------------
   All of it, on hover and on keyboard focus alike: the gold bar reaches
   further, the edge takes the interactive colour, the stage label goes purple
   and the arrow travels. It is one gesture, not four, and :focus-within gets
   the same one — the panel is a link, and a link that only answers a mouse is
   half a link. */
.moment:focus-within,
.moment:hover { border-color: var(--purple); box-shadow: var(--lift); }
.moment:focus-within .moment-stage,
.moment:hover .moment-stage { color: var(--purple); }
.moment:focus-within::before,
.moment:hover::before { top: 1rem; bottom: 1rem; }
.moment:focus-within .link-arrow-label,
.moment:hover .link-arrow-label { background-size: 100% 2px; }
.moment:focus-within .link-arrow::after,
.moment:hover .link-arrow::after { transform: translateX(4px); }

/* The lift is the only part that moves, so it is the only part behind the
   motion guard. */
@media (prefers-reduced-motion: no-preference) {
  .moment { transition:
    border-color var(--motion) var(--ease-out),
    box-shadow var(--motion) var(--ease-out),
    transform var(--motion) var(--ease-out); }
  .moment:focus-within,
  .moment:hover { transform: translateY(-2px); }
}

/* --- Service detail sections --------------------------------------------- */

.service-detail { display: grid; gap: 1.5rem; }
@media (min-width: 60rem) {
  .service-detail { grid-template-columns: 20rem 1fr; gap: 3.5rem; align-items: start; }
  .service-detail-head { position: sticky; top: 6rem; }
}
.service-detail-head h2 { margin-bottom: 0.5rem; }
.service-detail-head .lede { font-size: 1rem; color: var(--ink-soft); }
.service-detail .prose { max-width: 60ch; }

/* --- The services index ---------------------------------------------------
   Five jump links under the page head.

   It used to be a row of plain links between two hairlines, which is the
   shape of a breadcrumb trail -- a thing whose job is to tell you where you
   already are. This row's job is the opposite: to say what is on the page and
   take you to it. So it is five cards now, each carrying the same 01-05 the
   eyebrow on its section carries, which makes the index and the page below it
   visibly one list rather than two.

   Five across only at 60rem; two up from 34rem; one on a phone, where five
   full-width rows are easier to hit than five sixths of a line each. */
.service-index {
  display: grid;
  gap: 0.5rem;
  padding: 0;
  font-size: 0.95rem;
}
/* Five items split badly. Two columns leave the fifth alone on a row of its
   own, so in that band it takes the full width and the block reads 2-2-1 by
   design. Three columns give 3-2, which needs no help. */
@media (min-width: 34rem) {
  .service-index { grid-template-columns: repeat(2, 1fr); }
  .service-index a { flex-direction: column; align-items: flex-start; gap: 0.35rem; }
  .service-index li:last-child { grid-column: 1 / -1; }
}
@media (min-width: 48rem) {
  .service-index { grid-template-columns: repeat(3, 1fr); }
  .service-index li:last-child { grid-column: auto; }
}
@media (min-width: 60rem) { .service-index { grid-template-columns: repeat(5, 1fr); gap: 0.75rem; } }

.service-index a {
  display: flex;
  /* One line per card on a phone. Stacked, five cards ran 730px -- a whole
     screen spent on an index of what is below it. Side by side they take 280
     and the page starts where it should. */
  align-items: center;
  gap: 0.75rem;
  height: 100%;
  min-height: 56px;
  padding: 0.85rem 1rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--shell);
  color: var(--ink);
  text-decoration: none;
  transition:
    background-color var(--motion-fast) var(--ease),
    border-color var(--motion-fast) var(--ease),
    color var(--motion-fast) var(--ease);
}
.service-index-num {
  /* 0.78rem, not 0.72: 0.72 computes to 11.52px and the floor is 12. */
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  color: var(--ink-faint);
  transition: color var(--motion-fast) var(--ease);
}
.service-index-label { font-weight: 600; line-height: 1.3; }

/* Fills, like every other interactive thing on the site now does. The number
   goes gold rather than grey: #FFDA09 on #5A2982 measures 7.37:1, so on this
   ground -- and only on this ground -- gold can carry type. */
.service-index a:focus-visible,
.service-index a:active {
  background: var(--purple);
  border-color: var(--purple);
  color: #fff;
}
.service-index a:focus-visible .service-index-num,
.service-index a:active .service-index-num { color: var(--gold); }
@media (hover: hover) {
  .service-index a:hover {
    background: var(--purple);
    border-color: var(--purple);
    color: #fff;
  }
  .service-index a:hover .service-index-num { color: var(--gold); }
}

/* --- Founder ------------------------------------------------------------- */

.founder-block { display: grid; gap: 2rem; }
.founder-body { max-width: 60ch; }
.founder-title {
  color: var(--ink-soft);
  margin-top: -0.25rem;
  margin-bottom: 1.5rem;
}

.pull-quote {
  border-left: 3px solid var(--gold);
  padding-left: 1.25rem;
  margin-block: 1.75rem;
}
.pull-quote p {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 1rem + 0.5vw, 1.3rem);
  line-height: 1.45;
  color: var(--ink);
}
.pull-quote cite {
  display: block;
  margin-top: 0.6rem;
  font-style: normal;
  font-size: 0.9rem;
  color: var(--ink-faint);
}

/* --- Values -------------------------------------------------------------- */

.value-grid { display: grid; gap: 1.75rem; margin-top: 2rem; }
@media (min-width: 40rem) { .value-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .value-grid { grid-template-columns: repeat(3, 1fr); gap: 2rem 2.5rem; } }
/* The gold mark over each value.
   A rule the width of the column reads as a cap on a column, which is what it
   is at two and three across. In one column it becomes a full-bleed yellow
   band, and six of them stacked turn the section into a warning notice --
   about ten times the gold the rest of the page allows itself. So below the
   two-column breakpoint it is a short mark instead of a rule.
   Drawn as a background rather than a border because a border cannot be given
   a width without constraining the heading it sits over. */
.value-grid h3 {
  font-size: 1.05rem;
  padding-top: 0.75rem;
  margin-bottom: 0.4rem;
  background-image: linear-gradient(var(--gold), var(--gold));
  background-repeat: no-repeat;
  background-position: 0 0;
  background-size: 3.5rem 2px;
}
@media (min-width: 40rem) { .value-grid h3 { background-size: 100% 2px; } }
.value-grid p { color: var(--ink-soft); font-size: 0.95rem; }

/* --- Projects ------------------------------------------------------------ */

/* align-items: start, and it was tried the other way first. Stretching every
   card to its row's height looks right while every card has a scope list --
   and looks much worse the moment one does not, because a property with no
   write-up yet becomes a tall empty box, which draws the eye straight to the
   thing that is missing. Four of the nine are in that state. A short card
   beside a tall one is ordinary; an empty one is not. */
/* `stretch`, not `start`. With the write-ups folded away the cards in a row
   differ by at most the height of one summary — a card with nothing written
   about it has no fold — and levelling them is what makes the group read as a
   grid rather than as boxes of assorted sizes. Opening a fold grows its row,
   which is what a reader asking for more should expect. */
.project-grid { display: grid; gap: 1.5rem; align-items: stretch; }
/* Until one is opened. A card stretched to match a neighbour showing a
   paragraph and six scope items is a tall white box with nothing in the
   bottom two thirds of it, which is the fault this was levelling up to avoid
   in the first place — so the moment a fold is open, every card goes back to
   its own height. The resting state, which is the one everybody sees, stays
   level. */
.project-grid:has(.project-fold[open]) { align-items: start; }
@media (min-width: 48rem) { .project-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 76rem) { .project-grid { grid-template-columns: repeat(3, 1fr); } }

/* A group with one property in it. One card in a three-column row is
   two-thirds of a row of nothing, which is the fault Dr Kangangi raised about
   the rest of the site. It takes the full width and sets its name larger
   instead: one ongoing assignment is a thing to state, not to tuck into a
   corner. */
/* Same guard as the homepage strip: the portfolio grows, and a card left
   alone on a final row reads as a dropped item. */
@media (min-width: 48rem) {
  .project-grid li:last-child:nth-child(odd) { grid-column: 1 / -1; }
}
@media (min-width: 76rem) {
  .project-grid li:last-child:nth-child(odd) { grid-column: auto; }
  .project-grid li:last-child:nth-child(3n + 1) { grid-column: 1 / -1; }
}

/* Except at exactly four, which goes two-by-two.

   The rule above turns a fourth card into a full-width band under a row of
   three. That is right for a seventh card under two full rows; for the fourth
   it is a single wide card carrying one property's name, which the fit audit
   reads as an orphan row and a reader reads as a page that ran out. Two even
   rows are the same four cards with nothing left over.

   It appeared the day the ongoing group went from three properties to four,
   and it has to come after the rule it overrides — both halves, or the fourth
   card keeps the full-width span it was given above and the third is left
   alone beside it. Counting in CSS is what :has is for. */
@media (min-width: 76rem) {
  .project-grid:has(> li:nth-child(4):last-child) { grid-template-columns: repeat(2, 1fr); }
  .project-grid:has(> li:nth-child(4):last-child) li:last-child { grid-column: auto; }
}

.project-grid-single { grid-template-columns: 1fr; }
@media (min-width: 48rem) {
  .project-grid-single { grid-template-columns: 1fr; }
  /* Shallow, not square. Full width at card height is a large empty box, and
     the point of taking the full width was to stop the row reading as mostly
     nothing. A band the depth of its own content reads as a statement. */
  .project-grid-single .project-card {
    min-height: 0;
    padding: 1.6rem 2rem;
  }
  .project-grid-single .project-card h3 { font-size: 1.45rem; }
}

.project-card {
  background: var(--shell);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: 1.75rem;
  display: flex;
  flex-direction: column;
  /* A card carrying only a property's name is otherwise a sliver among
     cards four times its height. This is not enough to read as empty and is
     enough to read as a card. */
  min-height: 7.5rem;
  justify-content: center;
}
/* Content returns to the top as soon as there is any. */
.project-card:has(.project-meta),
.project-card:has(.project-scope),
.project-card:has(.project-objective) { justify-content: flex-start; }
.project-card-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
}
/* The heading takes the slack so the status badge stays hard right whether
   or not a logo sits to its left. */
.project-card-head h3 { flex: 1; }
.project-card h3 { font-size: 1.25rem; margin: 0; }

/* --- The two portfolio groups ---------------------------------------------
   The per-card status badge was removed when the page split into "Ongoing
   Projects" and "Past Projects": a card under a heading that says Ongoing does
   not also need a label saying Ongoing, and eight cards each repeating "Past"
   is noise. Its rules went with it rather than being left behind styled and
   unused, which this stylesheet has done before. */
.project-group + .project-group { padding-top: 0; }
.project-group-count {
  font-size: 0.85rem;
  color: var(--ink-faint);
  margin: 0;
}

.project-meta {
  color: var(--ink-soft);
  font-size: 0.9rem;
  margin-top: 0.5rem;
}
/* The separator is a real element written by the template, which is the only
   place that knows whether there are two things to separate. Do not move it
   back into a ::before here: the project type renders as a bare text node, so
   no structural selector on .project-location can tell the two cases apart. */
.project-sep { color: var(--rule-strong); }
.project-objective {
  font-size: 0.95rem;
  color: var(--ink-soft);
  margin-top: 0.9rem;
}
.project-scope-heading {
  font-family: var(--font-body);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-top: 1.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--rule);
}
/* --- The fold on a portfolio card ----------------------------------------
   The objective and the scope sit behind a disclosure, so a card is its name,
   its place and one row until somebody asks for more. Before it, a group of
   four cards where one had a paragraph and six scope items and three had
   nothing was four cards of four different heights — the short ones sitting in
   a hole beside the tall one.

   Same chevron as the footer's careers tree, the services menu and the areas
   beside the map. One disclosure gesture across the site, not four. */
.project-fold { margin-top: 1rem; }
.project-fold > summary {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  /* A row, so it is a finger-sized target rather than a line of text. */
  min-height: 44px;
  padding-top: 0.85rem;
  border-top: 1px solid var(--rule);
  cursor: pointer;
  list-style: none;
}
.project-fold > summary::-webkit-details-marker { display: none; }
.project-fold > summary:focus-visible {
  outline: 3px solid var(--purple);
  outline-offset: 2px;
  border-radius: var(--radius);
}
.project-fold-label {
  flex: 1;
  font-family: var(--font-body);
  font-size: 0.78rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.project-fold-marker {
  flex: none;
  width: 0.45rem;
  height: 0.45rem;
  border-right: 1.5px solid var(--purple);
  border-bottom: 1.5px solid var(--purple);
  transform: rotate(-45deg);
}
.project-fold[open] > summary .project-fold-marker { transform: rotate(45deg); }
@media (prefers-reduced-motion: no-preference) {
  .project-fold-marker { transition: transform var(--motion-fast) var(--ease); }
}
@media (prefers-reduced-motion: no-preference) and (hover: hover) {
  .project-fold-label { transition: color var(--motion-fast) var(--ease); }
}
.project-fold > summary:hover .project-fold-label { color: var(--purple); }

/* The objective already carries its own top margin, and the scope heading its
   own rule. Inside the fold both would double up on the summary's border. */
.project-fold-body > .project-objective:first-child { margin-top: 0.9rem; }
.project-fold-body > .project-scope-heading:first-child {
  margin-top: 0.9rem;
  padding-top: 0;
  border-top: 0;
}

.project-scope { margin-top: 0.6rem; font-size: 0.92rem; color: var(--ink-soft); }
.project-scope li { padding-left: 0.9rem; position: relative; }
.project-scope li + li { margin-top: 0.25rem; }
.project-scope li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  /* The same gold mark the prose lists carry, one size down for the smaller
     type it sits beside. One marker on the site, not two. */
  width: 5px;
  height: 5px;
  border-radius: 1px;
  background: var(--gold-deep);
  transform: rotate(45deg);
}

/* The homepage strip: names only, as a taste of the full portfolio. */
.project-strip { display: grid; gap: 1px; background: var(--rule); margin-top: 1.5rem; }
@media (min-width: 40rem) { .project-strip { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .project-strip { grid-template-columns: repeat(3, 1fr); } }

/* The homepage shows every published property now rather than the first six,
   so the count is whatever the portfolio happens to hold and no column count
   can assume a multiple of it. A final row left with a single cell reads as a
   mistake, so the last item takes the whole row when it would otherwise be
   alone — which is true at any count, today's nine included. */
@media (min-width: 40rem) {
  .project-strip li:last-child:nth-child(odd) { grid-column: 1 / -1; }
}
@media (min-width: 64rem) {
  .project-strip li:last-child:nth-child(odd) { grid-column: auto; }
  .project-strip li:last-child:nth-child(3n + 1) { grid-column: 1 / -1; }
  /* And the case the homepage actually shows: five properties in three
     columns leaves the last cell of the second row empty, and an empty cell in
     this grid is not nothing — the hairlines between cells are the container's
     own background showing through a 1px gap, so a cell with no item in it
     paints as a solid block of rule colour. It read as a grey box sitting
     after the fifth property. The last item takes the room instead. */
  .project-strip li:last-child:nth-child(3n + 2) { grid-column: span 2; }
}
/* Stacked into one column the side padding indents every row away from the
   section heading, which reads as a mistake rather than as a card. */
.project-strip li { background: var(--shell); padding: 1.25rem 0; }
@media (min-width: 40rem) { .project-strip li { padding-inline: 1.5rem; } }
.project-strip li { padding: 0; }
.project-strip a {
  display: grid;
  grid-template-columns: 6rem 1fr;
  grid-template-rows: auto auto;
  column-gap: 1rem;
  align-items: center;
  padding: 1.25rem 0;
  text-decoration: none;
  color: inherit;
  min-height: 44px;
}
@media (min-width: 40rem) { .project-strip a { padding-inline: 1.5rem; } }
.project-strip a:hover { text-decoration: none; }

/* The mark spans both rows so the name and the meta line stack beside it.

   A logo plate, the same everywhere a property's logo appears: white, a
   hairline, landscape. It used to be a sand-filled square, and both halves of
   that were wrong for real logos. The fill put a logo supplied on a white
   ground — a JPG, which is most of what clients send — inside a white square
   inside a beige box. The square shape shrank a wide wordmark, which is what
   most hotel logos are, to an unreadable sliver. White makes a white-ground
   logo vanish into the plate; landscape gives a wordmark its width and leaves
   a square mark centred with room either side. Every plate is the same size,
   so the names beside them still line up row to row. */
.project-mark {
  grid-row: 1 / 3;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 6rem;
  height: 4rem;
  border-radius: var(--radius);
  background: var(--shell);
  border: 1px solid var(--rule);
  overflow: hidden;
}
.project-mark img { width: 100%; height: 100%; object-fit: contain; padding: 0.35rem 0.45rem; }
.project-monogram {
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 24;
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: 0.02em;
  color: var(--purple);
}
.project-name { font-family: var(--font-display); font-weight: 600; font-size: 1.05rem; align-self: end; }
.project-strip .project-meta { font-size: 0.88rem; color: var(--ink-soft); margin-top: 0.15rem; align-self: start; }

@media (prefers-reduced-motion: no-preference) and (hover: hover) {
  .project-mark { transition: border-color var(--motion-fast) var(--ease); }
  .project-name { transition: color var(--motion-fast) var(--ease); }
  /* The border, not the ground: a purple fill swallowed every purple logo
     and blacked out the white plate around every other one. */
  .project-strip a:hover .project-mark { border-color: var(--purple); }
  .project-strip a:hover .project-name { color: var(--purple); }
}

/* The logo on the projects page sits beside the property's name, on the same
   plate as everywhere else, a size down. */
.project-card-logo {
  width: 4.5rem;
  height: 3rem;
  object-fit: contain;
  flex: none;
  align-self: center;
  background: var(--shell);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.3rem 0.4rem;
}

/* --- Small list utilities ------------------------------------------------ */

.tag-list { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 1.5rem; }
.tag-list li {
  background: var(--shell);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0.4rem 0.9rem;
  font-size: 0.9rem;
  color: var(--ink-soft);
}
.band-shell .tag-list li { background: var(--sand); }

.tick-list { list-style: none; padding: 0; }
.tick-list li { padding-left: 1.5rem; position: relative; }
.tick-list li + li { margin-top: 0.5rem; }
.tick-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.45em;
  width: 0.5rem;
  height: 0.85rem;
  border: solid var(--purple);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

.no-fees-inline {
  border-left: 3px solid var(--gold);
  padding-left: 0.9rem;
  font-size: 0.95rem;
}

.note { color: var(--ink-faint); font-size: 0.9rem; max-width: 62ch; }

/* --- Careers band on the homepage ---------------------------------------- */

.careers-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 0.75rem; }
.careers-count {
  font-family: var(--font-display);
  font-size: 1.1rem;
  padding-bottom: 0.75rem;
  border-bottom: 2px solid var(--gold);
  margin-bottom: 0.25rem;
}

/* --- Action list ----------------------------------------------------------
   A route with a label and a line saying who it is for. Used where a column
   would otherwise hold two or three bare buttons and most of its width would
   go unused. The whole row is the target, and it is a real list so a screen
   reader is told how many routes there are. */

.action-list { display: grid; gap: 0.6rem; margin-top: 1.25rem; }
.action-list a {
  display: flex;
  align-items: flex-start;
  gap: 0.9rem;
  padding: 1rem 1.1rem;
  background: var(--shell);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  text-decoration: none;
  color: inherit;
  min-height: 44px;
}
/* Gold on hover and on keyboard focus, and the border is where it goes.
   
   Not the label: --gold is 1.37:1 on white, and a word nobody can read is not
   an accent. A 1px border turning gold reads across the whole row without
   asking any text to be legible in it. The hover half is repeated inside the
   pointer-and-motion query below, where the transition lives. */
.action-list a:hover,
.action-list a:focus-visible {
  text-decoration: none;
  border-color: var(--gold-deep);
}
/* The icon, unboxed.
   
   It used to sit in a 2.4rem tinted square, which made a 20px glyph read as
   small — the box took the eye and the mark inside it was two thirds of the
   width. Out of the box it draws at 24px, and it is the one gold thing in a
   column that is otherwise entirely purple.
   
   --gold-deep, never --gold. #FFDA09 measures 1.37:1 on the card's white and
   1.16:1 on the rail's sand, which is a smudge, not a mark; #7F6900 is 5.35:1
   and still unmistakably gold. Same rule as the list markers and the WhatsApp
   glyph. */
.action-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem; height: 1.5rem;
  flex: none;
  /* The row aligns to flex-start, and a 24px square against a 25px line of
     600-weight text sits a hair high without this. */
  margin-top: 0.05rem;
  color: var(--gold-deep);
}

.action-text { display: block; }
.action-title {
  display: block;
  font-weight: 600;
  color: var(--purple);
}
/* The arrow rides with the title rather than sitting in its own column, so a
   long note cannot push it out of line with the label it belongs to. */
.action-title::after { content: " →"; white-space: pre; }
/* A row that leaves the site says so with the arrow it is already drawing,
   turned to point out of it. The note under the label names the destination,
   so the arrow is a confirmation rather than the only warning. */
.action-list a.is-external .action-title::after { content: " ↗"; }
/* An .action-list outside a rail. One column is the only sensible shape in an
   18rem rail; across the full 70rem measure it puts a two-word label alone on
   a 70rem row, which reads as a banner rather than as a choice between two
   routes. Scoped to the modifier so nothing in a rail can pick it up. */
@media (min-width: 48rem) {
  .action-list-pair { grid-template-columns: repeat(2, 1fr); }
  /* Both rows the height of the taller, so the pair reads as one choice. */
  .action-list-pair > li > a { height: 100%; }
}
.action-note {
  display: block;
  font-size: 0.88rem;
  color: var(--ink-soft);
  margin-top: 0.15rem;
}

@media (prefers-reduced-motion: no-preference) and (hover: hover) {
  .action-list a {
    transition: border-color var(--motion-fast) var(--ease),
                background-color var(--motion-fast) var(--ease);
  }
  .action-icon { transition: color var(--motion-fast) var(--ease); }
  .action-title::after {
    display: inline-block;
    transition: transform var(--motion-fast) var(--ease-out);
  }
  /* Gold, not purple. The row is purple type on white inside a purple rail,
     and the hover had been purple too — nothing moved except an arrow. The
     border is the one part of the row that can carry a colour without a word
     having to stay legible in it.
     
     The icon deliberately does NOT change on hover any more. It used to
     invert into a filled purple square, which only made sense while there
     was a square. */
  .action-list a:hover { border-color: var(--gold-deep); }
  .action-list a:hover .action-title::after { transform: translateX(4px); }
}

/* --- Office gallery -------------------------------------------------------
   Two up at every width. The photographs are 3:2 and stay 3:2 — cropping a
   wide courtyard shot square throws away the part that shows it is a
   courtyard. */

.office-gallery {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.6rem;
  margin-top: 2rem;
}
.office-gallery img {
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--radius);
  background: var(--sand-deep);
}

/* --- Contact and consultation -------------------------------------------- */

.hours-list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.35rem 1.25rem;
  font-size: 0.95rem;
  margin-block: 0.75rem 1rem;
}
.hours-list dt { color: var(--ink-soft); }
.hours-list dd { margin: 0; font-weight: 500; }

/* The contact list ends on a hairline, which ran straight into the next
   heading with nothing between them. */
.contact-details h2 + address { margin-top: 0.75rem; }
.contact-details .contact-list + h2,
.contact-details .hours-list + h2 { margin-top: 2rem; }

.contact-callout {
  background: var(--sand-deep);
  border-radius: var(--radius-lg);
  padding: 1.5rem;
  margin-top: 2rem;
}
.contact-callout h2 { font-size: 1.15rem; margin-bottom: 0.5rem; }
.contact-callout p { font-size: 0.95rem; color: var(--ink-soft); margin-bottom: 1.25rem; }

.form-intro { color: var(--ink-soft); font-size: 0.95rem; margin-bottom: 1.5rem; }

/* Form first on mobile, form and rail side by side on desktop. The rail
   carries what a visitor needs before committing to a long form — on the
   consultation page, and on the employers request form, where the form used
   to sit at 47% of the container with nothing beside it. */
/* The block always follows a heading and a lede, and had no space above it:
   on a phone "Tell us what you need" sat directly on top of "Property or
   company name", with the sentence and the first field label reading as one
   run of text. */
.form-with-rail { display: grid; gap: 2.5rem; align-items: start; }
/* Not on the consultation page, where .form-with-rail is itself the section
   and already carries the section's own padding. */
.form-with-rail:not(.section) { margin-top: 1.75rem; }
@media (min-width: 62rem) {
  .form-with-rail { grid-template-columns: minmax(0, 1fr) 20rem; gap: 3.5rem; }
}
.form-with-rail .stacked-form { max-width: none; }

.form-rail { display: grid; gap: 1.25rem; }
.rail-card {
  background: var(--shell);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: 1.5rem;
}
.rail-card-quiet { background: var(--sand-deep); border-color: var(--rule-strong); }
.rail-card h2, .rail-card h3 { font-size: 1.05rem; margin-bottom: 0.75rem; }

.rail-facts { font-size: 0.92rem; }
.rail-facts dt {
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.rail-facts dd { margin: 0.15rem 0 0.85rem; color: var(--ink-soft); }
.rail-facts dd:last-child { margin-bottom: 0; }
.rail-card .contact-list { font-size: 0.92rem; }
.rail-card .hours-list { font-size: 0.88rem; margin-bottom: 0; }

.rail-steps { counter-reset: rail; list-style: none; padding: 0; }
.rail-steps li {
  counter-increment: rail;
  position: relative;
  padding-left: 1.85rem;
  font-size: 0.92rem;
  color: var(--ink-soft);
}
.rail-steps li + li { margin-top: 0.85rem; }
.rail-steps li::before {
  content: counter(rail);
  position: absolute;
  left: 0;
  top: -0.1em;
  width: 1.35rem;
  height: 1.35rem;
  border: 1px solid var(--purple);
  border-radius: 50%;
  color: var(--purple);
  font-size: 0.72rem;
  display: grid;
  place-items: center;
}

.rail-list { list-style: none; padding: 0; font-size: 0.92rem; color: var(--ink-soft); }
.rail-list li { padding-left: 0.9rem; position: relative; }
.rail-list li + li { margin-top: 0.4rem; }
.rail-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  /* The same gold mark the prose lists carry, one size down for the smaller
     type it sits beside. One marker on the site, not two. */
  width: 5px;
  height: 5px;
  border-radius: 1px;
  background: var(--gold-deep);
  transform: rotate(45deg);
}

/* --- Footer additions ----------------------------------------------------- */

.footer-tagline {
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--gold);
  /* The same measured curve as every other instance. It was a flat 1.05rem,
     which wrapped to two lines at every width from 320 to 1920. */
  font-size: clamp(0.75rem, 3.8vw, 1.28rem);
  letter-spacing: -0.02em;
  line-height: 1.35;
  margin-bottom: 0.75rem;
}

.footer-promise {
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  padding-block: 1.5rem;
}
.footer-promise p {
  font-family: var(--font-display);
  font-size: clamp(1rem, 0.95rem + 0.35vw, 1.2rem);
  line-height: 1.5;
  color: #EDE6F2;
  /* Display face across the full 70rem band; 58ch left a third of the row
     empty. Larger type eats width faster, so this stops short of the ceiling. */
  max-width: 74ch;
  margin: 0;
}

/* ===========================================================================
   23. Motion, continued — the consultancy front door

   Same rules as section 22: short, small, one easing curve, nothing moving
   while someone is reading. Everything here is inside a reduced-motion guard
   or collapses under the blanket rule at the end.
   =========================================================================== */

/*
 * The hero arrives rather than appearing.
 *
 * Pure CSS rather than the IntersectionObserver the rest of the page uses.
 * The observer's starting state is applied by a class the deferred script
 * adds, which is fine below the fold but risks a flash of visible-then-hidden
 * content for the first thing on the page. A keyframe with fill-mode both
 * holds the opening frame through its delay and needs no script at all: if
 * the stylesheet fails, the content is simply visible.
 */
@media (prefers-reduced-motion: no-preference) {
  /* The stagger runs over the text column's own children and then the proof
     column as a whole, so the sequence still reads top-to-bottom once the
     hero became two columns. Targeting .hero-inner > * directly animated the
     two columns and nothing inside them. */
  .hero-body > *,
  .hero-proof {
    animation: rise-in 820ms var(--ease-out) both;
  }
  .hero-body > *:nth-child(1) { animation-delay: 80ms; }
  .hero-body > *:nth-child(2) { animation-delay: 150ms; }
  .hero-body > *:nth-child(3) { animation-delay: 240ms; }
  .hero-body > *:nth-child(4) { animation-delay: 330ms; }
  .hero-body > *:nth-child(5) { animation-delay: 400ms; }
  .hero-proof { animation-delay: 470ms; }

  /* Inner pages open on a page head rather than a hero. Two elements, quicker,
     because there is less to establish. */
  .page-head > * { animation: rise-in 620ms var(--ease-out) both; }
  .page-head > *:nth-child(1) { animation-delay: 60ms; }
  .page-head > *:nth-child(2) { animation-delay: 130ms; }
}

@keyframes rise-in {
  from { opacity: 0; transform: translateY(16px) scale(0.99); }
  to   { opacity: 1; transform: none; }
}

/* --- Hover states on the new cards ---------------------------------------
   Only what is clickable or scannable moves, and none of it shifts layout.
   Cards sit in grids six at a time; anything that lifts or scales turns a
   quiet page into a nervous one. */

.service-card,
.project-card {
  transition: background-color var(--motion-fast) var(--ease),
              border-color var(--motion-fast) var(--ease),
              box-shadow var(--motion-fast) var(--ease);
}

@media (hover: hover) {
  /* The service grid has no borders — separation comes from 1px gaps — so the
     ground warms instead. */
  .service-card:hover { background: var(--sand); }
  .service-card-cta:hover { background: var(--sand-deep); }
  /* A hover a card can be felt by: the icon takes the brand colour and lifts
     the arrow with it. The card itself does not translate — in a grid built
     from hairline separators, a moving cell tears the lines. */
  .service-card:hover .service-icon {
    background: var(--purple);
    color: #fff;
  }
  .service-card:hover .link-arrow::after { transform: translateX(4px); }
  .service-card:hover .link-arrow-label { background-size: 100% 2px; }
  .service-card:hover h3 a { color: var(--purple); }
  .service-icon {
    transition: background-color var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                transform var(--motion) var(--ease-out);
  }
  /* The icon turns a quarter and settles. It is the only thing in the grid
     that moves, which is what keeps six of them from reading as noise. */
  .service-card:hover .service-icon { transform: rotate(-6deg); }

  .project-card:hover {
    border-color: var(--rule-strong);
    box-shadow: var(--lift);
  }

  .project-strip li { transition: background-color var(--motion-fast) var(--ease); }
  .project-strip li:hover { background: var(--sand); }

  .brand { transition: opacity var(--motion-fast) var(--ease); }
  .brand:hover { opacity: 0.82; }
}

/* --- Navigation underline -------------------------------------------------
   The current page already carries a gold rule. On hover the same rule grows
   from the left rather than switching on, so the two read as one system. */
@media (min-width: 60rem) {
  .site-nav a:not(.nav-cta) { position: relative; }
  .site-nav a:not(.nav-cta)::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -0.35rem;
    height: 2px;
    background: var(--gold);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--motion-fast) var(--ease);
  }
  @media (hover: hover) {
    .site-nav a:not(.nav-cta):hover::after { transform: scaleX(1); }
  }
  .site-nav a[aria-current="page"]::after { transform: scaleX(1); }
}

/* --- Press ---------------------------------------------------------------
   A button that gives under the finger feels like a control rather than a
   picture of one. One pixel is enough. */
.button:active:not(:disabled) { transform: translateY(1px); }
.button { transition: background-color var(--motion-fast) var(--ease),
                      color var(--motion-fast) var(--ease),
                      border-color var(--motion-fast) var(--ease),
                      transform 80ms var(--ease); }

/* --- In-page anchors ------------------------------------------------------
   The services index and the "Request staff" jump both move a long way down
   the page. Landing there abruptly loses the reader's place. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

@media (prefers-reduced-motion: reduce) {
  .hero-body > *,
  .hero-proof,
  .page-head > * { animation: none !important; }
  .button:active:not(:disabled) { transform: none; }
  .site-nav a:not(.nav-cta)::after { transition: none; }
}

/* --- Founder portrait ------------------------------------------------------
   Both layouts collapse to a single column when no photograph is set, so the
   pages read correctly either way rather than reserving an empty frame. */
.founder-layout, .founder-block { display: grid; gap: 2rem; }
@media (min-width: 56rem) {
  .founder-layout.has-portrait,
  .founder-block.has-portrait {
    grid-template-columns: 18rem minmax(0, 1fr);
    gap: 3rem;
    align-items: start;
  }
}

.founder-portrait {
  max-width: 18rem;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--sand-deep);
}
.founder-portrait img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
}

/* ===========================================================================
   24. Two-factor enrolment

   These pages sit outside the admin — they have to, since the admin will not
   open until a device exists — so they use the public site's chrome and need
   to look like part of it rather than a bolted-on utility.
   =========================================================================== */

.setup-steps { margin-bottom: 2rem; }

/* The QR is inline SVG, so it inherits colour from here rather than being a
   fixed-colour image. White ground regardless of theme: a scanner needs the
   contrast, and the sand ground is close enough to off-white to reduce it. */
.qr-frame {
  display: inline-block;
  background: #fff;
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: 1rem;
  margin-bottom: 1.5rem;
  line-height: 0;
}
.qr-frame svg { display: block; width: 208px; height: 208px; }

.setup-manual { margin-bottom: 2rem; }
.setup-manual summary {
  cursor: pointer;
  font-weight: 500;
  color: var(--purple);
  padding-block: 0.6rem;
  min-height: 44px;
  display: flex;
  align-items: center;
}
.setup-manual p { color: var(--ink-soft); font-size: 0.95rem; }

/* The shared secret, in a face where the ambiguous characters are
   distinguishable — someone is going to type this by hand. */
.setup-secret {
  font-family: ui-monospace, "SFMono-Regular", "Cascadia Mono", Menlo, monospace;
  font-size: 1.05rem;
  letter-spacing: 0.08em;
  word-break: break-all;
  background: var(--sand-deep);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
  color: var(--ink);
}

.recovery-codes {
  list-style: none;
  padding: 1.25rem;
  margin-block: 1.5rem;
  display: grid;
  gap: 0.5rem 1.5rem;
  background: var(--sand-deep);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-lg);
  font-family: ui-monospace, "SFMono-Regular", "Cascadia Mono", Menlo, monospace;
  font-size: 1.05rem;
  letter-spacing: 0.06em;
}
@media (min-width: 34rem) { .recovery-codes { grid-template-columns: 1fr 1fr; } }

.rail-note { font-size: 0.92rem; color: var(--ink-soft); margin: 0; }

/* These pages print — someone will print the recovery codes. */
@media print {
  .site-header, .site-footer, .skip-link, .button { display: none !important; }
  .recovery-codes { border-color: #000; }
}

/* Six digits do not need the full width of a form built for addresses, and a
   field far wider than its content invites the wrong kind of input. */
.stacked-form input[name="token"] {
  max-width: 14rem;
  font-family: ui-monospace, "SFMono-Regular", "Cascadia Mono", Menlo, monospace;
  font-size: 1.25rem;
  letter-spacing: 0.28em;
}

/* ===========================================================================
   25. Sticky action bar — phones only

   Below 60rem the header's call to action is inside the collapsed menu, so
   without this most pages open with nothing to act on. Above 60rem the header
   button is visible and this would be redundant, so it is not rendered at all.
   =========================================================================== */

.sticky-cta { display: none; }

@media (max-width: 59.99rem) {
  .sticky-cta {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 40;
    display: flex;
    gap: 0.5rem;
    padding: 0.6rem 1rem;
    /* Clears the home indicator on an iPhone and the gesture bar on Android. */
    padding-bottom: calc(0.6rem + env(safe-area-inset-bottom, 0px));
    background: rgba(250, 247, 242, 0.94);
    backdrop-filter: blur(8px);
    border-top: 1px solid var(--rule);
  }
  .sticky-cta .button {
    flex: 1;
    min-height: 48px;
    padding-inline: 0.75rem;
  }


  /* Reserve the bar's height so it never covers the last thing on the page —
     the footer's legal links, usually. */
  body { padding-bottom: calc(4.25rem + env(safe-area-inset-bottom, 0px)); }
}

/* Printing a page should not print a floating button. */
@media print { .sticky-cta { display: none !important; } }

/* ===========================================================================
   26. Icons

   Inline SVG on a 24-unit grid, inheriting currentColor so an icon is always
   the colour of the text it sits beside. Every one is decorative and carries
   aria-hidden — each sits next to its own visible label.
   =========================================================================== */

.icon {
  flex: none;
  display: inline-block;
  vertical-align: -0.18em;
}

/* Contact lists become icon + label rows rather than a bare list. */
.contact-list li,
.footer-contact li {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
}
.contact-list .icon,
.footer-contact .icon { margin-top: 0.1rem; }

/*
 * Platform colours.
 *
 * WhatsApp has two values on purpose. Its signature green #25D366 measures
 * 1.86:1 against the page ground — a pale smudge rather than a logo — so
 * light backgrounds use WhatsApp's own teal #128C7E (3.87:1) and the dark
 * footer uses the signature green (8.85:1). Both are the platform's colours;
 * neither is invented. The floor for a meaningful graphic is 3:1.
 */
:root {
  --brand-whatsapp: #128C7E;
  --brand-whatsapp-on-dark: #25D366;
  --brand-linkedin: #0A66C2;
  --brand-facebook: #1877F2;
  --brand-instagram: #C13584;
}

.icon-brand { color: var(--purple); }
.icon-whatsapp.icon-brand { color: var(--brand-whatsapp); }
.icon-linkedin.icon-brand { color: var(--brand-linkedin); }
.icon-facebook.icon-brand { color: var(--brand-facebook); }
.icon-instagram.icon-brand { color: var(--brand-instagram); }

.on-dark .icon-brand,
.site-footer .icon-brand { color: var(--gold); }
.site-footer .icon-whatsapp.icon-brand { color: var(--brand-whatsapp-on-dark); }

/* An icon-only control still needs a real target. */
.sticky-cta-call { flex: 0 0 3.75rem; padding-inline: 0; }

h2.with-icon {
  display: flex;
  align-items: center;
  gap: 0.55rem;
}
h2.with-icon .icon { color: var(--gold-deep); }

/* The sticky bar's call button: icon and word on one line. */
.sticky-cta-call {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
}

/* ===========================================================================
   27. Questions, collapsed

   <details> rather than a scripted accordion — keyboard-operable, exposed to
   assistive technology, findable by the browser's own in-page search, and it
   works with the script absent. The motion is an enhancement on top.
   =========================================================================== */

/* --- FAQ layout -----------------------------------------------------------
   Questions left at a readable measure, a way through on the right. Before
   this the block ran to 704px inside a 1120px container on six pages and
   left the rest empty. */

.faq-layout { display: grid; gap: 2rem; }
@media (min-width: 62rem) {
  .faq-layout {
    grid-template-columns: minmax(0, 1fr) 19rem;
    gap: 3rem;
    align-items: start;
  }
  /* The rail follows the questions rather than scrolling away from them —
     the moment someone gives up on the list is when they need it. */
  .faq-aside { position: sticky; top: 92px; }
}

.faq-aside h3 { font-size: 1.05rem; margin: 0; }
.faq-aside > p { color: var(--ink-soft); font-size: 0.92rem; margin: 0.4rem 0 0; }
.faq-aside .action-list { margin-top: 1rem; }
.faq-aside .action-title { font-size: 0.95rem; }

.faq-list { list-style: none; padding: 0; max-width: var(--wrap-narrow); }
.faq-item { border-bottom: 1px solid var(--rule); }
.faq-list > li:first-child .faq-item { border-top: 1px solid var(--rule); }

.faq-item > summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  padding-block: 1.1rem;
  cursor: pointer;
  list-style: none;
  transition: color var(--motion-fast) var(--ease);
}
.faq-item > summary::-webkit-details-marker { display: none; }
.faq-item > summary:hover { color: var(--purple); }
.faq-item > summary:focus-visible {
  outline: 2px solid var(--purple);
  outline-offset: 3px;
  border-radius: var(--radius);
}

.faq-question {
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 24;
  font-size: 1.075rem;
  font-weight: 600;
  line-height: 1.35;
}

/* A plus that becomes a minus. Two rules rather than a glyph, so it is drawn
   at any size and rotates rather than swapping. */
.faq-marker {
  flex: none;
  position: relative;
  width: 15px;
  height: 15px;
  margin-top: 0.35rem;
}
.faq-marker::before,
.faq-marker::after {
  content: "";
  position: absolute;
  inset: 50% 0 auto 0;
  height: 1.5px;
  background: var(--purple);
  transition: transform var(--motion) var(--ease-out), opacity var(--motion-fast) var(--ease);
}
.faq-marker::after { transform: rotate(90deg); }
.faq-item[open] .faq-marker::after { transform: rotate(0deg); opacity: 0; }

.faq-answer { padding-bottom: 1.3rem; color: var(--ink-soft); }
.faq-answer p { max-width: 62ch; margin: 0; }

/* Opening animates everywhere; the height interpolation below is a bonus
   where the browser supports it. */
.faq-item[open] .faq-answer { animation: faq-open 380ms var(--ease-out) both; }
@keyframes faq-open {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

/* True height animation, in browsers that can interpolate to a keyword. */
@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }
  .faq-item::details-content {
    height: 0;
    overflow: hidden;
    transition: height var(--motion) var(--ease-out), content-visibility var(--motion) allow-discrete;
  }
  .faq-item[open]::details-content { height: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .faq-item[open] .faq-answer { animation: none; }
  .faq-marker::before, .faq-marker::after { transition: none; }
  @supports (interpolate-size: allow-keywords) {
    .faq-item::details-content { transition: none; }
  }
}

/* ===========================================================================
   28. Motion, third pass — the things that separate considered from adequate

   Every rule here is inside a reduced-motion guard or collapses under the
   blanket rule. Nothing moves while someone is reading, nothing shifts layout,
   and nothing is longer than three quarters of a second.
   =========================================================================== */

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

  /* --- Between pages ------------------------------------------------------
     A multi-page Django site flashes white between navigations. The View
     Transition API cross-fades instead, natively, with no script and no
     framework. Browsers without it simply navigate as before. */
  @view-transition { navigation: auto; }

  ::view-transition-old(root) { animation: vt-out 140ms var(--ease) both; }
  ::view-transition-new(root) { animation: vt-in 320ms var(--ease-out) both; }

  @keyframes vt-out { to { opacity: 0; } }
  @keyframes vt-in { from { opacity: 0; transform: translateY(8px); } }

  /* The header and footer persist across a navigation rather than fading with
     the page, so the chrome stays put and only the content changes. */
  .site-header { view-transition-name: site-header; }
  .site-footer { view-transition-name: site-footer; }

  /* --- Buttons ------------------------------------------------------------
     A primary button gains a shadow on hover and loses it on press, so it
     reads as lifting toward the cursor and then being pushed down. */
  .button-primary {
    box-shadow: 0 1px 2px rgba(36, 16, 51, 0.12);
    transition:
      background-color var(--motion-fast) var(--ease),
      color var(--motion-fast) var(--ease),
      border-color var(--motion-fast) var(--ease),
      box-shadow var(--motion) var(--ease-out),
      transform 90ms var(--ease);
  }
  @media (hover: hover) {
    .button-primary:hover {
      box-shadow: 0 2px 4px rgba(36, 16, 51, 0.14), 0 8px 20px rgba(36, 16, 51, 0.16);
    }
  }
  .button-primary:active:not(:disabled) {
    box-shadow: 0 1px 1px rgba(36, 16, 51, 0.16);
  }

  /* --- Links in running text ---------------------------------------------
     The underline grows from the left rather than switching colour, which is
     the same gesture the navigation uses. One idea, applied twice. */
  .prose a:not(.button):not(.link-arrow),
  .faq-answer a:not(.button) {
    text-decoration: none;
    background-image: linear-gradient(var(--purple), var(--purple));
    background-repeat: no-repeat;
    background-position: 0 100%;
    background-size: 100% 1px;
    padding-bottom: 1px;
    transition: background-size var(--motion) var(--ease-out), color var(--motion-fast) var(--ease);
  }
  @media (hover: hover) {
    .prose a:not(.button):not(.link-arrow):hover,
    .faq-answer a:not(.button):hover { background-size: 100% 2px; }
  }

  /* --- Cards --------------------------------------------------------------
     Only the ones that lead somewhere lift. A grid of six cards all rising
     under the cursor is restless; a single clickable card rising is feedback. */
  @media (hover: hover) {
    .project-card { transition: border-color var(--motion) var(--ease-out),
                                box-shadow var(--motion) var(--ease-out),
                                transform var(--motion) var(--ease-out); }
    .project-card:hover { transform: translateY(-2px); }
  }

  /* --- The gold rule under a section heading ------------------------------
     Draws itself in as the heading arrives, left to right. A two-line rule
     that turns a static underline into the page assembling itself. */
  .reveal-ready [data-reveal] .ruled { background-size: 0 2px; }
  .reveal-ready [data-reveal].is-revealed .ruled {
    background-size: 100% 2px;
    transition: background-size var(--motion-slow) var(--ease-out) 140ms;
  }
}

/* The blanket rule already collapses transitions and animations; view
   transitions need naming separately because they are neither. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) { animation: none !important; }
}

/* ===========================================================================
   29. The coverage map

   A static SVG of Kenya, one dot per county, and a list beside it that does
   the naming. Everything here is either the drawing or the two-column frame
   around it; there is no state, no hover-only information and no JavaScript.
   =========================================================================== */

.coverage-lede { margin-bottom: 2rem; }

.coverage-layout { display: grid; gap: 2rem; }

/* The map takes the narrower column. It is a portrait shape — Kenya is taller
   than it is wide — and giving it the larger half would push the list, which
   is the part people actually read, into a column too narrow for a property
   name to sit on one line. */
/* The map takes the larger share now that the counties collapse: five closed
   rows need about 20rem, and every pixel the map gains is a pixel more room
   for a label to fit without colliding with the next one. */
@media (min-width: 52rem) {
  .coverage-layout {
    grid-template-columns: minmax(0, 1fr) minmax(0, 20rem);
    gap: 3rem;
    align-items: start;
  }
}

/* min-width on the FIGURE, which is the grid item. Putting it on the frame
   inside did nothing: a grid item's default min-width is min-content, and
   Leaflet's controls give the live map a min-content width of 416px, which
   pushed the document to 436px wide inside a 390px phone. */
.coverage-map { margin: 0; min-width: 0; }

/* The frame holds the static drawing until Leaflet takes it over, so both
   states occupy the same box and the page does not jump when the upgrade
   lands. Leaflet needs a height before it will initialise at all; the aspect
   ratio is Kenya's own, 870 by 1010. */
.coverage-map-frame {
  /* Near square, which is also the drawing's own ratio now that it reaches
     into Uganda: 1070 units by 1010. A portrait frame would waste the width
     the labels need. */
  aspect-ratio: 1 / 1;
  max-width: 28rem;
  /* A grid item's default min-width is min-content, and Leaflet's controls
     and attribution give the live map a min-content width wider than a phone.
     Without this the frame measured 416px inside a 390px viewport. */
  min-width: 0;
  margin-inline: auto;
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.coverage-map-frame.coverage-map-live {
  background: var(--sand-deep);
  border: 1px solid var(--rule);
}
.coverage-svg {
  display: block;
  width: 100%;
  /* Capped so the drawing does not grow into a poster on a wide screen, and
     so the markers keep a sensible physical size: the viewBox is 1070 wide,
     so at this width a dot of 18.5 units renders about 15px across. The
     marker sizes in apps/core/geo.py were raised when the frame widened for
     Uganda, so that number is the same as it was at 870. */
  max-width: 28rem;
  height: auto;
  margin-inline: auto;
}

.coverage-land {
  fill: var(--sand-deep);
  /* Outlined in the brand purple rather than in a rule colour, which is what
     the live map has always done — the two pictures are now the same picture
     at different resolutions, and the country the work is in should be the one
     drawn in the firm's own colour. */
  stroke: var(--purple);
  stroke-opacity: 0.55;
  /* In viewBox units, where 100 units is a degree — so this is about three
     kilometres wide, which holds its weight as the drawing scales. Heavier
     than it was, because the drawing now has more than one country in it and
     the border is the only thing telling them apart. */
  stroke-width: 3;
  stroke-linejoin: round;
}

/* The neighbours are context, not the subject: a paler ground and a neutral
   outline, so Kenya reads as the filled shape and Uganda as the country beside
   it rather than as a competing one.

   The fill is not optional, and the first version of this drawing proved it by
   leaving the neighbours the same white as the band. The border lines were
   there, faintly — and the Kampala dot still read as a dot floating on blank
   paper, which is the exact thing this section was widened to stop. */
.coverage-land-near {
  fill: var(--sand);
  stroke: var(--rule-strong);
  stroke-width: 2.5;
  stroke-linejoin: round;
}

/* The halo is drawn first and painted in the band's own ground, so two dots
   that nearly touch — Nairobi and Machakos are sixty kilometres apart — still
   read as two rather than as one peanut. */
.coverage-halo {
  fill: none;
  stroke: var(--shell);
  stroke-width: 8.5;
}
.coverage-dot {
  fill: var(--purple);
  fill-opacity: 0.88;
}

/* The live sentence appears only once the script has upgraded the frame. */
.coverage-caption-live { display: none; }
.coverage-map-live + .coverage-caption .coverage-caption-still { display: none; }
.coverage-map-live + .coverage-caption .coverage-caption-live { display: inline; }

.coverage-caption kbd {
  font-family: var(--font-body);
  font-size: 0.95em;
  font-weight: 600;
  background: var(--sand-deep);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0 0.3em;
}

.coverage-caption {
  margin-top: 0.9rem;
  font-size: 0.8rem;
  color: var(--ink-faint);
  text-align: center;
}
@media (min-width: 52rem) {
  .coverage-caption { text-align: left; }
}

/* --- The legend, which is the part that gets read ------------------------ */

/* One column, not two and not CSS columns. Each county is a <details>, and a
   disclosure that grows inside a multi-column flow shoves every county after
   it into a different column — the list reshuffles under the reader's finger
   on the click that was meant to open one row. A single column cannot do
   that, and with the counties collapsed the list is five rows anyway. */
.coverage-areas {
  list-style: none;
  padding: 0;
  margin: 0;
}
.coverage-areas > li + li { margin-top: 0.4rem; }

.coverage-area > summary {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  /* A row, so it is a real target for a finger rather than a line of text. */
  min-height: 44px;
  padding-block: 0.35rem;
  border-bottom: 1px solid var(--rule);
  cursor: pointer;
  list-style: none;
}
.coverage-area > summary::-webkit-details-marker { display: none; }
.coverage-area > summary:focus-visible {
  outline: 3px solid var(--purple);
  outline-offset: 2px;
  border-radius: var(--radius);
}

/* The same chevron as the footer's careers tree and the services menu. One
   disclosure gesture across the site, not three. */
.coverage-area-marker {
  flex: none;
  width: 0.45rem;
  height: 0.45rem;
  border-right: 1.5px solid var(--purple);
  border-bottom: 1.5px solid var(--purple);
  transform: rotate(-45deg);
}
.coverage-area[open] > summary .coverage-area-marker { transform: rotate(45deg); }
@media (prefers-reduced-motion: no-preference) {
  .coverage-area-marker { transition: transform var(--motion-fast) var(--ease); }
}

.coverage-area-name {
  flex: 1;
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 24;
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--ink);
}
.coverage-area-tally {
  flex: none;
  font-size: 0.78rem;
  color: var(--ink-faint);
}
@media (prefers-reduced-motion: no-preference) and (hover: hover) {
  .coverage-area-name { transition: color var(--motion-fast) var(--ease); }
}
.coverage-area > summary:hover .coverage-area-name { color: var(--purple); }

.coverage-properties {
  list-style: none;
  margin: 0.85rem 0 1.1rem;
  padding-left: 1.2rem;
  border-left: 1px solid var(--rule);
}
.coverage-properties li + li { margin-top: 0.7rem; }
.coverage-properties a {
  display: block;
  font-weight: 500;
  font-size: 0.95rem;
}
.coverage-place {
  display: block;
  font-size: 0.8rem;
  color: var(--ink-faint);
  margin-top: 0.1rem;
}

.coverage-note { margin-top: 1.75rem; }

@media (prefers-reduced-motion: no-preference) and (hover: hover) {
  .coverage-properties a { transition: color var(--motion-fast) var(--ease); }
}


/* ===========================================================================
   30. The live map

   Leaflet's own stylesheet does the mechanics — panes, transforms, controls.
   Everything here is paint: the house palette applied to its parts, and the
   two behaviours its defaults get wrong for this site.
   =========================================================================== */

/* NO width/height here. Leaflet adds .leaflet-container to the frame itself,
   so `height: 100%` resolved against the figure's auto height and collapsed
   the figure to exactly the map's height — which threw the caption out of its
   parent's box and let the county list underneath draw straight over it. The
   frame's own aspect-ratio is what gives Leaflet the height it needs.

   Desktop never showed it, because the legend is in the other column there.
   The phone sweep found it on the first screenshot; all nine audits were
   green. */
.leaflet-container {
  background: var(--sand-deep);
  font-family: var(--font-body);
  font-size: 0.85rem;
}

/* The borders, drawn over the tiles from our own GeoJSON. Painted here rather
   than in the script because a brand colour belongs in the stylesheet and
   because the Content-Security-Policy refuses inline styles.

   Heavier than it was. The tiles do draw borders of their own, but faintly and
   in grey, and once a pin appeared in Kampala "which country is that in" became
   a question the map had to answer at a glance rather than on inspection. */
.coverage-border {
  fill: var(--purple);
  fill-opacity: 0.04;
  stroke: var(--purple);
  stroke-width: 2.2;
  stroke-opacity: 0.7;
  stroke-linejoin: round;
}

/* The neighbours: outline only, and lighter. No wash, so the tinted country is
   Kenya and only Kenya — which is the fastest way to say where the border runs
   without labelling anything. */
.coverage-border-near {
  fill-opacity: 0;
  stroke-width: 1.4;
  stroke-opacity: 0.33;
}

/* The marker is a div, not an SVG circle — see the note in coverage-map.js.
   Leaflet sizes and positions it through the CSSOM, which the CSP permits;
   everything you can see is here. */
.coverage-pin {
  background: var(--purple);
  border: 2px solid #fff;
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(31, 26, 34, 0.35);
}
.leaflet-container .coverage-pin:focus-visible {
  outline: 3px solid var(--purple);
  outline-offset: 3px;
}
/* The hover state is a ring, NOT a transform. Leaflet positions every marker
   with `transform: translate3d(...)` written straight onto the element, so a
   CSS `transform: scale()` here does not add to it — it replaces it, and the
   marker teleports to the corner of the map the moment the pointer touches
   it. A box-shadow changes no geometry and cannot collide with anything. */
@media (prefers-reduced-motion: no-preference) and (hover: hover) {
  .coverage-pin { transition: box-shadow var(--motion-fast) var(--ease); }
}
.coverage-pin:hover {
  box-shadow: 0 0 0 5px rgba(90, 41, 130, 0.22), 0 1px 3px rgba(31, 26, 34, 0.35);
}

/* --- Controls ------------------------------------------------------------ */

/* 44px, because the zoom buttons are the map's only real tap targets and
   Leaflet ships them at 26 — or 30 under .leaflet-touch, which is the rule
   that actually applies on a phone. Matching that selector's specificity is
   the point: loading this stylesheet after Leaflet's is not enough, because
   `.leaflet-touch .leaflet-bar a` is two classes and `.leaflet-bar a` is one.
   Measured at 30x30 twice before this was written properly. */
.leaflet-bar a,
.leaflet-bar a:hover,
.leaflet-touch .leaflet-bar a,
.leaflet-touch .leaflet-bar a:hover {
  width: 44px;
  height: 44px;
  line-height: 44px;
  color: var(--ink);
  background: var(--shell);
  border-bottom-color: var(--rule);
  font-size: 1.1rem;
}
.leaflet-bar a:hover,
.leaflet-touch .leaflet-bar a:hover { background: var(--sand-deep); }
.leaflet-bar a:focus-visible { outline: 3px solid var(--purple); outline-offset: -3px; }
.leaflet-bar { border: 1px solid var(--rule); box-shadow: var(--lift); }
.leaflet-touch .leaflet-bar a:first-child { border-radius: var(--radius) var(--radius) 0 0; }
.leaflet-touch .leaflet-bar a:last-child { border-radius: 0 0 var(--radius) var(--radius); }

/* Attribution is a condition of using the tiles, not decoration. It stays
   legible: Leaflet's default is 11px, which is under this project's floor. */
.leaflet-control-attribution {
  background: rgba(255, 255, 255, 0.88);
  color: var(--ink-soft);
  /* 12px is this project's floor and Leaflet ships 11. */
  font-size: 0.75rem;
  line-height: 1.6;
  padding: 0.1rem 0.5rem;
}
/* The two links here stay inline text at their natural 15px, and are NOT
   padded out to this project's 44px target rule.

   `line-height` was tried and does nothing — an inline element's hit box comes
   from its content box, not its line box, so it measured 15px either way.
   Getting to 44px, or even to WCAG 2.5.8's 24px, means inline-block plus
   vertical padding, which puts a thick white bar across a map that is only
   500px tall on a phone.

   They are left as they are because WCAG 2.5.8 exempts targets that are
   "inline" in a sentence, and this is one sentence of credit with two links
   in it. The exemption in responsive-audit.mjs says the same thing. The
   attribution itself is not optional — it is a condition of the tile licence. */
.leaflet-control-attribution a { color: var(--purple); }

/* --- Popups -------------------------------------------------------------- */

.leaflet-popup-content-wrapper {
  border-radius: var(--radius-lg);
  box-shadow: var(--lift);
  border: 1px solid var(--rule);
}
.leaflet-popup-content { margin: 0.9rem 1.1rem; line-height: 1.5; }
.leaflet-popup-content p { margin: 0; }
.coverage-popup-name {
  font-family: var(--font-display);
  font-variation-settings: 'opsz' 24;
  font-size: 1rem;
  font-weight: 600;
}
.coverage-popup-type { font-size: 0.82rem; color: var(--ink-soft); margin-top: 0.3rem; }
.coverage-popup-place { font-size: 0.78rem; color: var(--ink-faint); margin-top: 0.2rem; }
.leaflet-container a.leaflet-popup-close-button {
  width: 30px;
  height: 30px;
  font-size: 1.1rem;
  color: var(--ink-faint);
  padding: 4px 4px 0 0;
}

/* A callout: the name in a box with a tail pointing at its own pin.
   
   The tail is the part doing the work. With eleven properties a reader can
   usually guess which name belongs to which dot; at twenty-plus they cannot,
   and a name floating near two pins is worse than no name. Leaflet draws the
   tail itself for all four directions — it was switched off in the first
   version, when the gap was wide enough that it read as a speech bubble
   rather than a pointer.

   HTML, not SVG text, so it holds 12px whatever the map is scaled to. That is
   the reason the still fallback carries no labels at all. */
.leaflet-tooltip.coverage-label {
  font-size: 0.75rem;
  font-weight: 500;
  padding: 0.2rem 0.5rem;
  border-radius: var(--radius);
  /* No border. The tail Leaflet draws is a solid triangle of one colour, and
     a bordered box with an unbordered tail reads as a mistake. The shadow
     does the separating instead. */
  border: 0;
  background: #fff;
  color: var(--ink);
  box-shadow: 0 1px 3px rgba(31, 26, 34, 0.28), 0 0 0 1px rgba(31, 26, 34, 0.06);
  white-space: nowrap;
}
/* Leaflet colours the tail from these, one per direction. */
.leaflet-tooltip.coverage-label.leaflet-tooltip-left::before { border-left-color: #fff; }
.leaflet-tooltip.coverage-label.leaflet-tooltip-right::before { border-right-color: #fff; }
.leaflet-tooltip.coverage-label.leaflet-tooltip-top::before { border-top-color: #fff; }
.leaflet-tooltip.coverage-label.leaflet-tooltip-bottom::before { border-bottom-color: #fff; }

/* Hidden because another label got there first — see layoutLabels() in
   coverage-map.js. visibility, not display, so the element keeps a box and
   the next pass can measure it without a reflow storm. */
.leaflet-tooltip.coverage-label.is-crowded { visibility: hidden; }

@media (prefers-reduced-motion: reduce) {
  .leaflet-fade-anim .leaflet-tile,
  .leaflet-fade-anim .leaflet-popup { transition: none; }
  .leaflet-zoom-anim .leaflet-zoom-animated { transition: none; }
}


/* ===========================================================================
   31. Profiles in the footer
   =========================================================================== */

.footer-social { margin-top: 1.4rem; }
.footer-social ul {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  list-style: none;
  padding: 0;
  margin: 0;
}
/* A pill rather than a bare icon: one link labelled "LinkedIn" reads as a
   deliberate line, where a lone 20px glyph reads as debris. It is also a real
   tap target without having to pad an icon out to 44px. */
.footer-social a {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
  padding: 0 0.9rem;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 500;
}
.footer-social .icon { flex: none; }
@media (prefers-reduced-motion: no-preference) and (hover: hover) {
  .footer-social a {
    transition: background-color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease);
  }
}
.footer-social a:hover,
.footer-social a:focus-visible {
  background: rgba(255, 255, 255, 0.08);
  border-color: var(--gold);
}

/* ===========================================================================
   32. A property's own page
   =========================================================================== */

/* The page borrows the category hub's layout — .hub-layout and .hub-rail —
   rather than introducing one for fourteen pages that each hold a few
   paragraphs. Only the parts the hub has no equivalent for are here.

   The logo sits beside the h1 rather than above it, so a property with a logo
   and one without open at the same height. It is decorative: the name is
   right there in text, so the alt is empty and the height is fixed in the
   markup to stop the heading jumping when the image arrives. */
.project-page-head {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}
.project-page-logo {
  width: 8.5rem;
  height: 4.5rem;
  object-fit: contain;
  flex: none;
  background: var(--shell);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.5rem 0.7rem;
}
.project-page-heading {
  font-size: 1.15rem;
  margin: 2rem 0 0.75rem;
}

/* The facts, above the routes out in the rail. A definition list would be the
   semantic choice and is the wrong one here: a <dl> split across a label and a
   value that must sit in a column each is a fight with the default styling on
   every browser, and the pairs are read in order either way. */
.project-facts {
  list-style: none;
  padding: 0;
  margin: 0 0 1.5rem;
  display: grid;
  gap: 0.9rem;
}
.project-facts li {
  display: grid;
  gap: 0.15rem;
}
.project-fact-label {
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.project-fact-value {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  font-size: 0.95rem;
  color: var(--ink);
}
/* The pin sits on the text's baseline rather than the flex line's, which puts
   it under the cap height of a two-line address instead of halfway down it. */
.project-fact-value .icon { color: var(--purple); flex: none; transform: translateY(2px); }

/* The property's name on a card is now a link to its own page. It keeps the
   heading's colour at rest — a grid of fourteen cards with fourteen coloured
   headings reads as a link farm — and takes the site's underline on hover and
   focus like every other text link. */
.project-card h3 a { color: inherit; text-decoration: none; }
@media (prefers-reduced-motion: no-preference) and (hover: hover) {
  .project-card h3 a { transition: color var(--motion-fast) var(--ease); }
}
.project-card h3 a:hover,
.project-card h3 a:focus-visible { color: var(--purple); text-decoration: underline; text-underline-offset: 0.18em; }

/* --- At a Glance ----------------------------------------------------------
   The facts a reader takes in without reading, above the write-up rather than
   below it. Two columns from 44rem, which is where a single column of five
   short lines starts to look like a list of links rather than a summary.

   Hairlines under each item rather than bullets on a coloured panel: the page
   already carries a panel (the rail) and a second one beside the first
   paragraph turns a property page into a dashboard. */
.project-highlights {
  list-style: none;
  padding: 0;
  margin: 0.9rem 0 0;
}
/* Columns rather than a grid, and that is the whole reason.

   Five facts in a two-column grid leave the fifth alone on a row of its own —
   a grid orphan, which this project's layout audit counts and which reads as
   a list that ran out rather than one that finished. Columns balance instead:
   three and two, top to bottom, which is also the order somebody reads a list
   in. `break-inside` keeps a two-line fact from splitting across the fold. */
@media (min-width: 44rem) {
  .project-highlights { column-count: 2; column-gap: 2.5rem; }
  .project-highlights li { break-inside: avoid; }
}
.project-highlights li {
  position: relative;
  padding: 0.62rem 0 0.62rem 1.05rem;
  border-bottom: 1px solid var(--rule);
  font-size: 0.97rem;
}
.project-highlights li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 1.08em;
  /* The same gold mark the scope list and the prose lists carry. One marker
     on this site, not three. */
  width: 5px;
  height: 5px;
  border-radius: 1px;
  background: var(--gold-deep);
  transform: rotate(45deg);
}

/* --- A property's photographs ---------------------------------------------
   Outside the reading measure, because a photograph held to 42 characters of
   width is a thumbnail. One up on a phone, two from 40rem, three from 64rem.

   .office-gallery is deliberately not reused: it is two up at every width,
   which at 390px gives two 180px-wide pictures of a hotel. A property's own
   page is the one place on this site where the photograph is the point.

   3:2 and cropped to it. The alternative is a grid whose rows are as tall as
   whichever picture is tallest, and a gallery that jumps as each one loads —
   width and height are on every <img> for the same reason. */
.project-gallery {
  list-style: none;
  padding: 0;
  margin: 0.9rem 0 0;
  display: grid;
  gap: 0.9rem;
}
@media (min-width: 40rem) { .project-gallery { grid-template-columns: repeat(2, 1fr); } }
/* Two columns and no third, so an odd photograph is the only case to handle
   rather than two. A last photograph with nothing beside it runs the full
   width as a wide shot, which reads as a decision; the same picture left
   sitting in half a row reads as a gap. A single photograph gets the same
   treatment, which is the right answer for one picture anyway. */
@media (min-width: 40rem) {
  .project-gallery li:last-child:nth-child(odd) { grid-column: 1 / -1; }
  .project-gallery li:last-child:nth-child(odd) img { aspect-ratio: 21 / 9; }
}
.project-gallery figure { margin: 0; }
.project-gallery img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--radius);
  background: var(--sand-deep);
}
.project-gallery figcaption {
  font-size: 0.85rem;
  color: var(--ink-soft);
  margin-top: 0.45rem;
  /* A caption is a caption, not a paragraph. */
  max-width: 46ch;
}

/* --- The back control -----------------------------------------------------
   On a detail page — one property, one job — above the name and ahead of
   everything else.

   The breadcrumb above it is a location, not a route: 0.85rem, --ink-faint,
   and on a phone it reads as decoration. A reader who landed here from a
   search result has no history to go back through, and the nearest route out
   should not be the footer.

   It is a link and it looks like one, with a border so it is findable and no
   fill so it never competes with the page's real call to action. 44px tall,
   because it is a tap target on the page every visitor from search lands on. */
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  /* 0.7rem of padding rather than a bare min-height, which measures as
     43.9999px on a fractional layout offset and is then a failing tap target
     by a ten-thousandth of a pixel. Letting the content set the height puts
     it at 46 and out of the argument. */
  min-height: 44px;
  padding: 0.7rem 0.9rem 0.7rem 0.75rem;
  margin-bottom: 1.25rem;
  border: 1px solid var(--rule-strong);
  border-radius: 999px;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--purple);
  text-decoration: none;
  background: var(--shell);
}
.back-link-arrow { font-size: 1.05em; line-height: 1; }
@media (prefers-reduced-motion: no-preference) and (hover: hover) {
  .back-link { transition: border-color var(--motion-fast) var(--ease),
                           background-color var(--motion-fast) var(--ease); }
  .back-link-arrow { display: inline-block;
                     transition: transform var(--motion-fast) var(--ease-out); }
  .back-link:hover .back-link-arrow { transform: translateX(-3px); }
}
.back-link:hover,
.back-link:focus-visible { border-color: var(--purple); background: var(--sand); text-decoration: none; }
