/* =============================================================================
   SDEIRA SUB-PAGE SYSTEM
   -----------------------------------------------------------------------------
   The stylesheet for every sub-page of the group site. Currently: the news
   index (news.html) and the news article pages (news-*.html).

   WHY THIS FILE EXISTS AND WHY IT CANNOT BREAK THE HOMEPAGE
   sdeira-v2.html carries its own ~12,000 line inline <style> block, split into
   @layer legacy plus an unlayered SYSTEM LAYER. That structure is internal to
   that page. This file is never linked by sdeira-v2.html, and sdeira-v2.html's
   block is never linked by a sub-page, so the two cascades never meet. There
   is therefore no need for a layer, a defensive prefix or !important here, and
   none of the three is used.

   RULES THIS FILE OBEYS
   1. Link assets/css/sdeira-tokens.css BEFORE this file. Link the Phosphor
      icon CSS before it too.
   2. This file declares NO token custom property. It only consumes them. The
      four properties it does define (--page-gutter, --hd-pad, --logo-h,
      --cta-h) do not exist in the token file.
   3. No @layer. No !important. No box-shadow. No one-sided border used as a
      visual treatment. No colour outside the eight brand colours.
   4. Every selector is defined ONCE. Breakpoint work lives in the RESPONSIVE
      section at the end of each half of the file, with a note on what it
      changes. This is the discipline the homepage lost.
   5. Decorative geometry uses PHYSICAL coordinates (top/left), not logical
      ones. docs/quality/design-qa.md records that logical properties pushed
      this kind of decoration on top of the text on an RTL page. The English
      mirror mirrors the values by hand; the slant angle is never mirrored,
      because the Sdeira mark has one physical slant.

   FILE MAP
   PART A, page chrome, flattened from sdeira-v2.html:
     1. Reset and document base
     2. Layout, section and text roles
     3. Header and nav
     4. Mobile drawer
     5. Footer
     6. Motion
     7. Responsive (chrome)
   PART B, sub-page components, authored here:
     8. Utilities
     9. Sub-page hero
    10. Breadcrumb
    11. News index
    12. Article
    13. Pending
    14. Reveal
    15. Responsive (components)
   ========================================================================== */

/* =============================================================================
   1. RESET AND DOCUMENT BASE
   ========================================================================== */

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* Form controls do not inherit the document face by default. Without this,
   every <button> and <input> falls back to the UA default. */
button,
input,
select,
textarea {
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  color: inherit;
}

/* Removing an author border from a <button> does not leave it borderless: the
   UA default takes over. */
button {
  background: none;
  border: 0;
}

html {
  scroll-behavior: smooth;
  overflow-x: hidden;
}

body {
  font-family: var(--font-brand);
  background: var(--offwhite);
  color: var(--ink);
  line-height: 1.75;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

/* Heading face and shape. The font-size values come from the SYSTEM LAYER
   type roles, which beat every per-section size in the legacy layer. */
h1,
h2,
h3,
.display {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0;
}

h1 {
  font-size: var(--type-display);
  line-height: var(--leading-display);
}

h2 {
  font-size: var(--type-section);
  line-height: var(--leading-section);
}

/* h3 and h4 carry no page-level size override on the homepage: they take the
   token file's own heading rules. Left unset here for the same reason. */

h1,
h2 {
  text-wrap: balance;
}

h1,
h2,
h3,
p,
a,
span {
  overflow-wrap: break-word;
}

::selection {
  background: var(--accent-fill);
  color: #fff;
}

a {
  color: inherit;
  text-decoration: none;
}

/* Body copy role, from the SYSTEM LAYER. */
main p {
  font-size: var(--type-body);
  line-height: var(--leading-body);
  text-wrap: balance;
}

/* Phosphor glyphs are filled paths delivered as an icon font, so they are
   sized with font-size and coloured with `color`. */
i.ph {
  display: inline-block;
  line-height: 1;
  color: inherit;
  flex: 0 0 auto;
  font-size: var(--ic-md);
}

/* The header is fixed, so an in-page anchor would otherwise land with the
   section heading hidden underneath it. 72px is the compressed bar height. */
section[id] {
  scroll-margin-top: 72px;
}

/* =============================================================================
   2. LAYOUT
   ========================================================================== */

/* Page gutter. NOT a token: this name does not exist in sdeira-tokens.css. */
:root {
  --page-gutter: 32px;
}

.wrap {
  max-width: var(--max);
  margin: 0 auto;
  padding: 0 32px;
}

.pad {
  padding: var(--sec-pad) 0;
  background: var(--surface-base);
}

/* About's figures are part of the Who We Are composition, matching the
   homepage's single About parent and its established values spacing. */
.about-who-we-are > .wrap {
  display: flex;
  flex-direction: column;
  gap: clamp(64px, 8vw, 112px);
}

.skip-link {
  position: fixed;
  top: 14px;
  right: 24px;
  z-index: 2000;
  padding: 10px 16px;
  background: var(--charcoal);
  color: #fff;
  border-radius: var(--r);
  transform: translateY(-140%);
  transition: transform var(--dur-base) var(--ease-out);
}

.skip-link:focus {
  transform: translateY(0);
}

/* The skip link sits on a dark surface, so it takes the dark-surface hover
   role rather than the light one. */
.skip-link:is(:hover, :focus-visible) {
  color: var(--link-hover-on-dark);
}

/* =============================================================================
   3. SECTION HEADINGS AND KICKERS
   ========================================================================== */

.sec-head {
  margin-bottom: var(--sec-head-gap);
  max-width: 680px;
}

.sec-head h2 {
  /* The local clamp(2rem, 4vw, 3.1rem) loses to the SYSTEM LAYER h2 role. */
  color: var(--charcoal);
  margin-bottom: 18px;
}

.sec-head p {
  /* The SYSTEM LAYER lead role wins over the local 1.12rem. */
  font-size: var(--type-lead);
  line-height: var(--leading-lead);
  color: var(--muted);
  font-weight: 400;
}

.kicker {
  /* .82rem locally, and var(--type-meta) from the SYSTEM LAYER resolves to
     the same value. The role is used so a token change carries. */
  font-size: var(--type-meta);
  font-weight: 700;
  letter-spacing: 0;
  color: var(--label);
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 20px;
}

/* A kicker inside page content shrink-wraps and carries the Sdeira mark.
   The homepage scopes this to `main section:not(.hero)`; the :not() is kept
   so a page that does have a hero behaves identically. */
main section:not(.hero) .kicker {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  width: auto;
}

main section:not(.hero) .kicker::before {
  content: "";
  width: 20px;
  height: 20px;
  flex: 0 0 20px;
  background: url("../images/brand/sdeira-mark.png") center / contain no-repeat;
}

/* =============================================================================
   4. BUTTONS AND ACTION LINKS
   ========================================================================== */

/* Height comes from a control height, not from block padding, and that is the
   whole fix here. This was `padding: 15px 30px` with no height, and because the
   action role also gives it a 26.6px body line-height it computed to 59px -
   against 46px for the nav CTA, the search field and both dropdowns, every one
   of which is sized by a height variable. It was the only control on the site
   sized by padding, so it was the only one that read as a card rather than a
   button, 13px taller than its neighbours for no stated reason.
   46px matches them and is also the floor worth holding: WCAG's AAA target
   size is 44px, so this should not be taken lower for a touch control.
   The inline padding is untouched, so the width does not move.
   padding-block stays non-zero only so a label that wraps on a narrow screen
   still clears the edges; on one line the min-block-size wins and the padding
   is inert. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  min-block-size: 46px;
  padding: 8px 30px;
  /* Local 1rem loses to the SYSTEM LAYER action role. */
  font-size: var(--type-action);
  font-weight: 500;
  border-radius: var(--r);
  transition: background var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
  cursor: pointer;
}

/* Unified primary action. The later unconditional rule replaced the original
   --accent fill and removed the box-shadow the first pass carried, which is
   what keeps this in step with the no-shadow rule of the design system. */
.btn-primary,
.nav-cta {
  /* White labels require the accessible fill role. */
  background: var(--accent-fill);
  color: var(--brand-white);
  border-color: var(--accent-fill);
  font-weight: 800;
}

/* The directional wipe shared by the primary and ghost buttons. The angle is
   logical through the token: RTL and LTR begin at each reader's start edge. */
.btn-primary,
.btn-ghost,
.nav-cta {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.btn-primary::before,
.btn-ghost::before,
.nav-cta::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  --wipe-p: 0%;
  background-image: linear-gradient(
    var(--motion-wipe-gradient-angle),
    color-mix(in srgb, var(--brand-black) 30%, transparent) 0 var(--wipe-p),
    transparent var(--wipe-p) 100%
  );
  transition: --wipe-p var(--dur-slow) var(--ease-out);
  pointer-events: none;
}

.btn-primary:is(:hover, :focus-visible),
.btn-ghost:is(:hover, :focus-visible),
.nav-cta:is(:hover, :focus-visible) {
  background: var(--accent-fill);
  color: var(--brand-white);
  border-color: var(--accent-fill);
}


.btn-ghost:is(:hover, :focus-visible)::before,
.nav-cta:is(:hover, :focus-visible)::before {
  --wipe-p: 100%;
}

/* The secondary button. This is the sanctioned border exception: it has no
   fill, so the outline is the only thing that makes its hit area visible. */
.btn-ghost {
  /* Full shorthand, not just border-color: the base .btn carries no border,
     so a colour-only declaration would resolve to border-style:none. */
  border: 1px solid var(--brand-white);
  background: transparent;
  color: var(--brand-white);
  font-weight: 800;
}

/* Text and outline stay white in both states, since the wipe never becomes
   light enough to need a dark label. */
.btn.btn-ghost:is(:hover, :focus-visible) {
  color: var(--brand-white);
  border-color: var(--brand-white);
}

/* Links signal themselves with an underline that grows from the reading edge
   instead of a sliding arrow glyph. */
.more {
  position: relative;
  display: inline-block;
  font-size: var(--type-action);
}

.more::after {
  content: "";
  position: absolute;
  inset-block-end: -3px;
  inset-inline-start: 0;
  inline-size: 100%;
  block-size: 1px;
  background: currentColor;
  opacity: .32;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform var(--dur-base) var(--ease-out), opacity var(--dur-base) var(--ease-out);
}

a:hover .more::after,
.more:hover::after {
  transform: scaleX(1);
  opacity: 1;
}

/* The contextual action link placed over imagery. */
.action-link {
  position: relative;
  z-index: 0;
  display: inline-flex;
  align-items: center;
  gap: .48rem;
  min-height: 0;
  padding: 0 0 4px;
  overflow: visible;
  isolation: auto;
  background: transparent;
  color: var(--brand-white);
  font-size: var(--type-action);
  font-weight: 800;
  line-height: 1.3;
  transition:
    opacity var(--dur-base) var(--ease-out),
    visibility var(--dur-base) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.action-link::before {
  content: none;
}

.action-link::after {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block-end: 0;
  inline-size: 100%;
  block-size: 2px;
  background: currentColor;
  transform: scaleX(.42);
  transform-origin: right center;
  transition: transform var(--dur-base) var(--ease-out);
}

.action-link:is(:hover, :focus-visible)::after {
  transform: scaleX(1);
}

.action-link > i.ph {
  flex: 0 0 auto;
  transform: rotate(45deg);
  transition: transform var(--dur-base) var(--ease-out);
}

.action-link:is(:hover, :focus-visible) > i.ph {
  transform: rotate(0deg);
}

/* One hover colour role for the whole page: the word and any icon inside the
   link move to the warm accent together. */
:is(.action-link, .more):is(:hover, :focus-visible) {
  color: var(--link-hover);
}

:is(.action-link, .more):is(:hover, :focus-visible) > i.ph {
  color: inherit;
}

/* One keyboard focus ring for every control. */
:is(.btn, .nav-cta, .cta-action, .action-link, .more, .sector-card-action, .section-view-all):focus-visible {
  outline: 3px solid var(--brand-razeen);
  outline-offset: 3px;
}

/* =============================================================================
   5. HEADER
   -----------------------------------------------------------------------------
   The pill (gap, padding, CTA) is ONE size in both states - see the comment
   below for why that stays fixed. The logo is the one deliberate exception,
   matching the group site: bigger at rest, shrinking to the compact size the
   moment .scrolled lands. Only that one property carries two values now.
   ========================================================================== */

#hd {
  position: fixed;
  top: 0;
  right: 0;
  left: 0;
  z-index: 1000;

  /* ONE size, both states, for everything except the logo below, and the
     values are the group site's own. This file used to carry a second smaller
     set of these under .scrolled plus two more pairs inside media blocks, so
     scrolling resized the logo, the CTA and the padding at once and the bar
     read as "morphing" rather than arriving. The homepage deleted exactly
     that ladder; the news pages kept it, which is a large part of why they
     read as a different system. They also kept an uncapped scale: at 1600 the
     bar stood 160px tall with a 108px logo against the homepage's 100px and
     38px.

     Two distances, two jobs, deliberately separate:
       --hd-gap  viewport edge -> the pill (the float gap)
       --hd-pad  pill edge -> the nav content (equal on all four sides)
     clamp() carries both across the whole range, which is what lets the
     per-breakpoint override blocks go away entirely. */
  --hd-gap: clamp(6px, .6vw, 8px);
  --hd-pad: clamp(6px, .6vw, 8px);
  --cta-h: clamp(34px, 2.6vw, 40px);

  /* Single shared logo height across rest and scrolled states and widths
     (V3 change request): --header-logo-height lives in the token file. */
  --logo-h: var(--header-logo-height);

  /* All four sides: the inline padding is what holds the pill off the screen
     edge on a phone, where the pill is narrower than --max and would otherwise
     run edge to edge. */
  padding: var(--hd-gap);
}

/* The one property this class still changes here, kept in step with the
   group site's own header.scrolled rule. */
#hd.scrolled {
  --logo-h: var(--header-logo-height);
}

/* Readability scrim for the fixed header while it sits over a hero photo
   (:not(.scrolled) state): without it, the header's white nav text had no
   contrast floor against a bright image and depended entirely on the hero's
   own scrim, which is deliberately near-transparent at the very top so the
   photo reads. This used to be a dead layer (an unconditional opacity: 0
   on :not(.scrolled) killed it in both states) - re-enabled below. */
#hd::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  block-size: 230px;
  z-index: -1;
  pointer-events: none;
  /* Held nearly flat across the top 40%, where the nav text actually sits,
     then dropped away fast. Against a fully white photo region .58 composites
     to rgb(107), which is 5.3:1 for the white nav text - clear of the 4.5:1
     body-text floor with room, and the header is the only thing this layer
     serves.
     It used to open at .72 and still be at .48 halfway down, which made it
     the darkest thing over the photograph now that the hero's own scrim
     starts nearly clear. The nav's contrast comes from the first 40% of this
     band; the rest of it was just weight on the image. */
  background: linear-gradient(180deg,
    rgba(0, 0, 0, .62) 0%,
    rgba(0, 0, 0, .58) 40%,
    rgba(0, 0, 0, .3) 72%,
    transparent 100%);
  transition: opacity var(--dur-base) var(--ease-out);
}

#hd.scrolled::before {
  opacity: 0;
}

/* .nav IS the pill box. .wrap already gives it max-width: var(--max) and
   margin-inline: auto, so it is the page's own content column, centred, with
   no width arithmetic of its own - that is the whole reason the glass lands
   exactly on the content width. The padding shorthand overrides .wrap's
   `0 32px`, so the distance from the pill edge to the nav content is
   --hd-pad on all four sides.

   isolation, not z-index: .nav sits inside #hd's stacking context next to
   #hd::before (the scrim, z-index: -1). Making .nav its own stacking context
   is what guarantees .nav::before below lands above that scrim and below the
   logo and links. It creates a stacking context WITHOUT creating a containing
   block for fixed-position descendants, which matters here - see .nav::before. */
.nav {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  padding: var(--hd-pad);
}

/* The floating glass pill.
   ---------------------------------------------------------------------------
   WHY A PSEUDO-ELEMENT AND NOT #hd ITSELF. backdrop-filter makes an element a
   containing block for every position:fixed descendant, and the mobile drawer
   (.menu / #mobileNav) is position:fixed AND a child of .nav. This file used
   to put the backdrop-filter on #hd.scrolled, so the drawer was already being
   measured against the header instead of the viewport once the visitor had
   scrolled. A pseudo-element has no descendants, so its containing block traps
   nothing.

   WHY inset: 0 IS EXACT. .nav's padding box is the pill, because .nav's
   padding is --hd-pad on all four sides. So the pill needs no width, no gutter
   arithmetic and no per-breakpoint correction: it is simply its parent's
   padding box.

   WHY NOTHING GEOMETRIC IS IN THE TRANSITION. The pill is fully shaped at rest
   - same box, same border-radius, same 1px border - and is invisible only
   because its paint is transparent. So the corner never animates from square
   to round, the box never grows, and no nav element shifts: on scroll the
   surface simply arrives, already the right shape. Only colour and blur move.

   blur(0px), not none: `none` -> `blur(18px)` is a discrete jump that pops
   mid-fade, whereas 0px interpolates with the rest of the paint.

   No box-shadow: elevation on this site is tonal-step only. The hairline
   border in --line is what separates the translucent panel from the page
   behind it. */
.nav::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border: 1px solid transparent;
  border-radius: var(--r);
  background: rgba(255, 255, 255, 0);
  backdrop-filter: blur(0px) saturate(100%);
  -webkit-backdrop-filter: blur(0px) saturate(100%);
  transition:
    left var(--dur-base) var(--ease-out),
    right var(--dur-base) var(--ease-out),
    margin-inline var(--dur-base) var(--ease-out),
    top var(--dur-base) var(--ease-out),
    bottom var(--dur-base) var(--ease-out),
    border-radius var(--dur-base) var(--ease-out),
    background-color var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out),
    backdrop-filter var(--dur-base) var(--ease-out);
}

/* Liwa migration F1 - ported from the reference sdeira-subpage.css: the bar
   at rest is one full-width white strip from first paint, not a transparent
   pill that only appears on scroll. .nav is .wrap, centred, so left:50%/
   right:50% with margin -50vw lands the strip exactly on header's outer edge
   at any width. */
#hd:not(.scrolled) .nav::before {
  left: 50%;
  right: 50%;
  margin-left: -50vw;
  margin-right: -50vw;
  top: calc(-1 * var(--hd-gap));
  bottom: calc(-1 * var(--hd-gap));
  border-radius: 0;
  background: var(--brand-white);
  border-color: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

#hd.scrolled .nav::before {
  background: rgba(255, 255, 255, .82);
  border-color: var(--line);
  backdrop-filter: blur(var(--glass-blur)) saturate(140%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(140%);
}

.logo {
  font-family: var(--font-display);
  font-weight: 700;
  /* scale-exempt: the text fallback behind the logo <img>. It is a wordmark at
     a lockup proportion, not copy, and it is only ever seen if the image fails
     to load. No text role governs the size of a logo. */
  font-size: 1.6rem; /* scale-exempt: wordmark fallback */
  letter-spacing: 0;
  /* Liwa migration F2: the bar's background is always white now (see
     .nav::before above), so the ink is held dark at every scroll position
     instead of white-at-rest/dark-on-scroll. */
  color: var(--charcoal);
  transition: color var(--dur-base);
  /* Never shrinks. The nav absorbs the width instead. */
  flex: 0 0 auto;
}

.logo img,
#navLogo {
  display: block;
  width: auto;
  max-width: 440px;
  height: var(--logo-h);
  object-fit: contain;
  transition: height var(--dur-base) var(--ease-out);
}

#hd.scrolled .logo {
  color: var(--charcoal);
}

/* =============================================================================
   6. NAV MENU (desktop)
   ========================================================================== */

.menu {
  display: flex;
  /* Fluid gap: a fixed gap overflowed the row the moment the fallback font
     measured wider than the brand face, which wrapped the menu to two lines.

     THE CEILING IS NOT DECORATIVE. .wrap (this row's own container) stops
     growing at var(--max) = 1280px, but a raw vw ceiling keeps reading the
     true viewport - so past 1280px wide the row's available box is fixed
     while this gap kept demanding more, eventually pushing "About Sdeira"
     under the logo and "News & Media" under the search icon with no error
     and no scrollbar (measured on /en/companies/: clean at 1400, broken at
     1600 - the EN nav's longer labels reach this before the Arabic one
     does). 20px is what 1.55vw already equals at 1280px, i.e. the value
     already verified clean by the comment on the drawer breakpoint below -
     this stops it from ever asking for more than that again. */
  gap: clamp(12px, 1.55vw, 20px);
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
  justify-content: center;
}

.menu a {
  /* The local clamp(.86rem, .95vw, .95rem) loses to the SYSTEM LAYER control
     role, which applies to `.menu > a` and to `.menu .nav-submenu a`. */
  font-size: var(--type-control);
  font-weight: 500;
  /* Liwa migration F2: bar ink held dark at every scroll position - see
     .nav::before above. */
  color: var(--ink);
  position: relative;
  /* A nav label must never wrap: a two-line link changes the bar height. */
  white-space: nowrap;
  transition: color var(--dur-base);
}

.menu a::after {
  content: "";
  position: absolute;
  bottom: -6px;
  right: 0;
  width: 0;
  height: 1.5px;
  background: var(--accent);
  transition: width var(--dur-base);
}

.menu a:hover::after,
.menu a.active::after {
  width: 100%;
}

.menu-mobile-actions a::after {
  display: none;
}

#hd.scrolled .menu a {
  color: var(--ink);
}

/* Liwa migration F2: the current item is ink (marked by the accent underline
   below), never white - white would vanish on the always-white bar. */
.menu a.active {
  color: var(--ink);
}

#hd.scrolled .menu a.active {
  color: var(--accent-text);
}

/* The drawer head and the mobile action block exist in the markup at every
   width and are revealed only inside the drawer breakpoint. */
.menu-head {
  display: none;
}

.menu-mobile-actions {
  display: none;
}

.menu-lang,
.menu-contact {
  display: none;
}

.nav-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 0 0 auto;
}

.nav-cta {
  /* Height is driven by the header state so the CTA compresses with the bar
     instead of pinning it open. */
  min-height: var(--cta-h);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 clamp(18px, 1.8vw, 28px);
  border-radius: var(--r);
  /* Local .82rem loses to the SYSTEM LAYER control role, same value. */
  font-size: var(--type-control);
  letter-spacing: 0;
  white-space: nowrap;
}

/* The CTA keeps the accessible fill in both header states. */
#hd.scrolled .nav-cta,
#hd.scrolled .nav-cta:is(:hover, :focus-visible) {
  background: var(--accent-fill);
  color: var(--brand-white);
  border-color: var(--accent-fill);
  font-weight: 800;
}

/* Language switch. Shared shape with its drawer twin. */
.lang,
.menu-lang {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-height: 44px;
  padding-inline: 8px;
  color: rgba(255, 255, 255, .84);
  /* Local .82rem loses to the SYSTEM LAYER control role, same value. */
  font-size: var(--type-control);
  font-weight: 600;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}

.lang i.ph,
.menu-lang i.ph {
  font-size: var(--ic-sm);
}

.lang {
  /* Liwa migration F2 - ported from the reference sdeira-subpage.css: the bar
     ink is held dark at every scroll position, so the language control rests
     on the light --muted tone, never white. */
  color: var(--muted);
  cursor: pointer;
}

.lang:hover,
.lang:focus-visible {
  /* --accent-strong, not --accent-text: this role is small text on the pale
     --accent-wash tint, and --accent-text is calibrated for a solid fill
     instead (white on brands like Khadamat, invisible on a light wash). */
  color: var(--accent-strong);
  background: var(--accent-wash);
}

/* =============================================================================
   7. NAV DISCLOSURES AND SUBMENUS
   -----------------------------------------------------------------------------
   `is-animating` and `is-closing` are set by the script, which holds a
   <details> open for the whole exit so its content stays rendered.
   ========================================================================== */

.menu > .nav-disclosure {
  position: relative;
  flex: 0 0 auto;
}

/* Chromium wraps a details element's non-summary children in
   ::details-content. Because the desktop panel is absolutely positioned, that
   anonymous wrapper has zero block-size and its default overflow clips the
   otherwise-open panel. Keep the native disclosure semantics while allowing
   the positioned panel to paint beyond that wrapper. */
.nav-disclosure[open]::details-content {
  overflow: visible;
}

/* Newer Chromium versions give ::details-content its own layout box. At the
   desktop breakpoint, make that box the positioned dropdown host and return
   the panel to flow inside it; this avoids a zero-height anonymous wrapper.
   Browsers without the pseudo-element keep the established absolute panel. */
@supports selector(.nav-disclosure::details-content) {
  @media (min-width: 1281px) {
    .nav-disclosure[open]::details-content {
      position: absolute;
      z-index: 4;
      inset-block-start: 100%;
      inset-inline-start: 0;
    }

    .nav-disclosure[open] > .nav-submenu {
      position: relative;
      z-index: auto;
      inset-block-start: 10px;
      inset-inline-start: auto;
    }
  }
}

.nav-disclosure > summary {
  min-block-size: 44px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding-inline: 6px;
  /* Liwa migration F2: bar ink held dark at every scroll position. */
  color: var(--ink);
  /* The local clamp and the drawer's 1.05rem both lose to the SYSTEM LAYER
     control role, which is unlayered and therefore unconditional. */
  font-size: var(--type-control);
  font-weight: 500;
  white-space: nowrap;
  list-style: none;
  cursor: pointer;
  /* The open trigger takes the Sweihan fill. That is a colour change, so it
     runs on --dur-fast, the step the token file reserves for colour. */
  transition:
    background var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.nav-disclosure > summary::-webkit-details-marker {
  display: none;
}

/* The label is a direct link inside the summary. Its generic nav-link hover
   underline would duplicate the summary's open/current state line. */
.nav-disclosure > summary > a::after {
  display: none;
}

.nav-disclosure > summary::after {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block-end: 2px;
  inline-size: 0;
  block-size: 1.5px;
  background: var(--accent);
  transition: inline-size var(--dur-base) var(--ease-out);
}

.nav-disclosure > summary i.ph {
  font-size: var(--ic-xs);
  transition: transform var(--dur-fast) var(--ease-out);
}

/* THE CURRENT SECTION IS A SECOND REASON TO DRAW THIS UNDERLINE, and it is the
   only one of the two that is true before the visitor touches anything. The
   other - open - is a reaction; this one states where the page already is,
   which is what a bar with no marked item could never say. It is the same
   underline deliberately: a nav item has one state vocabulary, and a second
   treatment for "you are here" would be a second language in one bar.

   summary.active comes from {{NAV_BRANCH:...}} in the partial, and covers what
   :has(.nav-submenu a.active) beside it cannot - a page inside the section with
   no entry of its own in the menu. Both are kept: the :has() branch also lights
   the summary during the closing animation of a menu whose child is current,
   and dropping it would flicker the underline off mid-close. */
.nav-disclosure:is([open], :has(.nav-submenu a.active)):not(.is-closing) > summary::after,
.nav-disclosure > summary.active::after {
  inline-size: 100%;
}

.nav-disclosure[open]:not(.is-closing) > summary i.ph {
  transform: rotate(180deg);
}

/* The open trigger carries NO fill. The underline under it is already the
   open signal - it is the same underline every other nav item uses for its
   own state, so a filled block here made the dropdown trigger the one item in
   the bar with a second, louder vocabulary. The chevron rotation is the
   second half of the signal. The drawer keeps its fill: at that width the
   underline is display:none, so the fill is the only signal left. */
.nav-disclosure[open]:not(.is-closing) > summary {
  background: transparent;
}

.nav-disclosure > summary:focus-visible {
  outline: 3px solid var(--brand-razeen);
  outline-offset: 3px;
}

#hd.scrolled .nav-disclosure > summary {
  color: var(--ink);
}

/* One rule for every dropdown, both hierarchies and both viewports: a white
   panel, rows separated by space, and the fill as the only state signal
   WITHIN the panel. This is the same decision already documented for the
   tab lists, so the navigation stops being the one place that also draws
   divider lines between rows.
   The panel's own edge is a separate question: a #fff panel drawn on a
   --surface-raised (also #fff) section - a scrolled header over a white
   "pad--raised" block, for one - has no edge at all. No box-shadow per the
   token file; the site's elevation is tonal-step, so this is the same 1px
   --line hairline every raised card and form field already uses for the
   same separation. Mirrors sdeira-v2.css / sdeira-v2-en.css. */
.nav-submenu {
  position: absolute;
  z-index: 4;
  inset-block-start: calc(100% + 10px);
  inset-inline-start: 0;
  inline-size: max-content;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--r);
  min-inline-size: var(--nav-submenu-min);
}

.menu .nav-submenu a {
  min-block-size: 42px;
  padding-inline: 12px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  color: var(--ink);
  /* Local .88rem and the drawer's .92rem both lose to the control role. */
  font-size: var(--type-control);
  white-space: normal;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
  gap:12px;
}

.menu .nav-submenu a::after {
  display: none;
}

/* Fill, not a text colour: on the Sweihan fill an orange label measures 1.00
   and disappears, so these links are excluded from the link-hover role.

   The id is here to win a cascade race, not for style. A submenu link is also
   `.menu a`, so once the header gains .scrolled, `#hd.scrolled .menu a.active`
   (1,3,1) out-specified the plain form of this rule (0,3,1) and repainted the
   label in --accent-text, brand Black, on the solid --accent-fill - correct
   for the top-level nav item that rule targets, never meant to reach inside
   the dropdown. With the id this rule ties that one at (1,3,1) and wins on
   source order, since it sits ~200 lines further down. This is the same fix
   the two homepages make from their system layer; a flat file has no layer to
   win from. */
#hd .menu .nav-submenu a:is(:hover, :focus-visible, .active) {
  background: var(--accent-fill);
  color: #fff;
}

.menu .nav-submenu a:focus-visible {
  outline: 3px solid var(--brand-razeen);
  outline-offset: -3px;
}

.nav-submenu-hover-icon {
  flex: 0 0 1em;
  color: inherit;
  font-size: 1em;
  line-height: 1;
  opacity: 0;
  pointer-events: none;
  transform: translateX(4px);
  transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-base) var(--ease-out);
}

/* The homepage needs an important flag here only because the legacy i.ph
   font-size rule sits in the same layer. Stating the size on the element
   selector is enough in a flat file. */
.menu .nav-submenu i.nav-submenu-hover-icon {
  font-size: 1em;
}

.menu .nav-submenu a:is(:hover, :focus-visible) .nav-submenu-hover-icon {
  opacity: 1;
  transform: translateX(0);
}

.nav-submenu--companies .company-nav-link {
  justify-content: flex-start;
  gap: 12px;
}

.company-nav-link .nav-submenu-hover-icon {
  margin-inline-start: auto;
}

/* On the filled row the subsidiary marks reverse to white, the same answer the
   company cards on the homepage give their marks on the same fill - see the
   long note at the matching block in sdeira-v2.css. Without it, Khadamat green
   and Your Laundry blue sit on Sweihan, which is the clash that treatment
   exists to prevent, and the row disagrees with itself: white label, brand
   mark. Measured alpha decides the mechanism per source: the transparent marks
   take brightness(0) invert(1), Eskan takes it plus its lockup text (no filter
   reaches live text), and Zoad - a dark mark on a solid white ground, 100%
   opaque - inverts and then screens, which drops its now-black ground into the
   fill and leaves the mark white on it. */
.company-nav-mark :is(img, svg) {
  transition: filter var(--dur-fast) var(--ease-out);
}

.company-nav-mark--eskan :is(b, small) {
  transition: color var(--dur-fast) var(--ease-out);
}

.company-nav-link:is(:hover, :focus-visible, .active) .company-nav-mark :is(img, svg) {
  filter: brightness(0) invert(1);
}

.company-nav-link:is(:hover, :focus-visible, .active) .company-nav-mark--eskan :is(b, small) {
  color: var(--brand-white);
}

/* THE ZOAD NAV EXCEPTIONS ARE GONE, matching the reference: the nav now
   carries the WORDMARK (zoad-logo-wordmark.webp), navy ink on transparent,
   so it takes the same brightness(0) invert(1) hover filter every other
   company mark takes. The old two-crop screen treatment was for the emblem,
   white letterforms knocked out of a solid navy block, which is no longer
   what the nav shows. */

.company-nav-mark {
  /* V3 geometry: a dedicated 96px logo column, 32px-high container,
     reading-start aligned. */
  inline-size: 96px;
  block-size: 32px;
  display: grid;
  place-items: center start;
  flex: 0 0 96px;
  overflow: hidden;
}

.company-nav-mark svg {
  inline-size: 31px;
  block-size: 26px;
}

.company-nav-mark img {
  /* Height-led sizing so every mark keeps its own aspect ratio inside the
     32px container; width follows height and is capped by the 96px column
     (V3 per-brand heights below). */
  inline-size: auto;
  block-size: 30px;
  max-inline-size: 100%;
  object-fit: contain;
  object-position: left center;
}

/* V3 per-brand logo heights. Arabic/RTL is the default; the English/LTR
   overrides follow the token file's :root[dir] pattern. */
.company-nav-mark--eskan img { block-size: 28px; }
:root[dir="ltr"] .company-nav-mark--eskan img { block-size: 32px; }
.company-nav-mark--khadamat img { block-size: 30px; }
.company-nav-mark--zoad img { block-size: 28px; }
.company-nav-mark--laundry img { block-size: 17px; }
:root[dir="ltr"] .company-nav-mark--laundry img { block-size: 20px; }

.company-nav-mark--eskan {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 3px;
}

.company-nav-mark--eskan svg {
  inline-size: 20px;
  block-size: 25px;
  flex: 0 0 20px;
}

/* scale-exempt: these are proportions inside a wordmark lockup, not text
   sizes, which is why they are excluded from the type roles. The block is
   aria-hidden and __typecheck.js skips it for the same reason. */
.company-nav-mark--eskan b,
.company-nav-mark--eskan small {
  display: block;
  color: var(--brand-black);
  font-family: var(--font-display);
  line-height: 1;
}

.company-nav-mark--eskan b {
  /* scale-exempt: wordmark lockup proportion, not a text size. */
  font-size: .72rem;
}

.company-nav-mark--eskan small {
  margin-top: 2px;
  /* scale-exempt: wordmark lockup proportion, not a text size. */
  font-size: .54rem;
}

.company-nav-mark--khadamat {
  color: var(--brand-black);
  font-family: var(--font-display);
  /* scale-exempt: wordmark lockup proportion, not a text size. */
  font-size: .72rem;
  font-weight: 600;
  line-height: 1;
  text-align: center;
}

.company-nav-mark--khadamat small {
  display: block;
  margin-top: 2px;
  /* scale-exempt: wordmark lockup proportion, not a text size. */
  font-size: .58rem;
}

.company-nav-mark--laundry {
  color: var(--brand-black);
  font-family: var(--font-display);
  /* scale-exempt: wordmark lockup proportion, not a text size. */
  font-size: .7rem;
  font-weight: 600;
  line-height: 1;
  text-align: center;
}

/* =============================================================================
   8. DRAWER CONTROLS (burger, backdrop, scroll-top)
   ========================================================================== */

/* Only the two things this file still decides: whether the burger is shown at
   all (its breakpoint) and where it sits in the bar. The 44px target, the
   radius, the glyph and every colour are the shared family's - see
   assets/css/sdeira-nav-controls.css. */
.burger {
  display: none;
  margin-inline-end: -9px;
}


.menu-backdrop {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: none;
  border: 0;
  background: rgba(var(--image-overlay-rgb), .38);
  backdrop-filter: blur(8px);
  cursor: pointer;
}

body.menu-open .menu-backdrop {
  display: block;
}

/* The drawer is a child of the fixed header, so its z-index cannot escape the
   header's stacking context. Hide the resting chrome while it is open; this
   lets the backdrop cover the page without leaving the light logo or glass
   pill floating above it, while the drawer's own `.menu-logo` remains visible. */
body.menu-open #hd > .nav > .logo,
body.menu-open #hd > .nav > .nav-actions,
body.menu-open #hd::before,
body.menu-open #hd .nav::before {
  visibility: hidden;
  opacity: 0;
}

/* Low-priority utility, not a primary action: smaller footprint, no border
   or blur, and it rests at partial opacity even while "visible". */
.scroll-top {
  position: fixed;
  left: 24px;
  bottom: 24px;
  z-index: 999;
  width: 38px;
  height: 38px;
  border-radius: var(--r);
  background: rgba(65, 64, 66, .55);
  border: none;
  color: #fff;
  /* scale-exempt: the glyph inside the 38px scroll-top tile. It is sized to
     the tile, not to any copy beside it, so no text role applies - the icon
     roles are for glyphs set with text. */
  font-size: .95rem; /* scale-exempt: tile glyph */
  line-height: 1;
  display: grid;
  place-items: center;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transform: translateY(12px);
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out);
}

.scroll-top.visible {
  opacity: .55;
  pointer-events: auto;
  transform: none;
}

/* The tile state is a fill, which is why this control is excluded from the
   link-hover colour role. Hover was later moved from Sweihan to Razeen; the
   focus half of the original pair was never updated, so it still lifts to the
   accent. Both are reproduced exactly as they resolve on the homepage. */
.scroll-top:hover {
  background: var(--brand-razeen);
  border-color: var(--brand-razeen);
  color: var(--brand-black);
  opacity: 1;
}

.scroll-top:focus-visible {
  background: var(--accent);
  opacity: 1;
}

/* =============================================================================
   9. FOOTER
   ========================================================================== */

footer {
  /* Dark glass rather than an opaque band, but the tone is the brand Black
     from the token file. The alpha and the composited contrast figures live
     with the token. */
  background: var(--scrim-footer);
  backdrop-filter: blur(16px) saturate(125%);
  -webkit-backdrop-filter: blur(16px) saturate(125%);
  /* The floor lives here so descendants can step down without falling
     through it. */
  color: rgba(255, 255, 255, .96);
  font-weight: 600;
  padding: 80px 0 36px;
  /* Extra bottom clearance keeps the socials row from sitting under the fixed
     .scroll-top button once the page is scrolled all the way down. */
  padding-bottom: max(36px, calc(96px + env(safe-area-inset-bottom)));
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

footer > .wrap {
  position: relative;
  z-index: 1;
}

.foot-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  gap: 40px;
  padding-bottom: 56px;
}

.foot-brand {
  min-width: 0;
}

.foot-brand .logo {
  display: block;
  color: #fff;
  /* scale-exempt: the same wordmark fallback, at the footer's lockup size. */
  font-size: 1.9rem;
  inline-size: min(220px, 100%);
  max-inline-size: 100%;
  aspect-ratio: 360 / 92;
  margin-bottom: 20px;
}

.foot-brand .logo img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  max-inline-size: 100%;
  object-fit: contain;
  object-position: right center;
}

.foot-brand p {
  font-weight: 400;
  /* The local .98rem loses to the SYSTEM LAYER body role. */
  font-size: var(--type-body);
  line-height: var(--leading-body);
  max-width: 300px;
  text-wrap: balance;
}

/* :is(h3, h4) because the footer column headings are moving from h4 to h3.
   They sat at h4 with the page's own h2 above them and nothing in between,
   which is a level skip on every page that loads this file. The pair is
   deliberate rather than transitional: the homepage's footer is styled from
   sdeira-v2.css and cannot be changed here, so accepting both keeps one
   definition working across the transition instead of forking it. */
.foot-col :is(h3, h4) {
  font-family: var(--font-display);
  color: #fff;
  /* --type-label, not the 1.05rem this carried. That literal rendered 15.8px
     and was the last off-scale size in the shared chrome, so the footer column
     headings were a different size from every other supporting heading on the
     site - on all 37 sub-pages and both homepages at once. label is the
     heading role directly below card and the token file names this exact job:
     supporting heading metadata. */
  font-size: var(--type-label);
  line-height: var(--leading-label);
  margin-bottom: 22px;
  font-weight: 700;
}

.foot-col a,
.foot-col .foot-pending {
  display: block;
  margin-bottom: 13px;
  /* The local .95rem and the SYSTEM LAYER action role agree. */
  font-size: var(--type-action);
  font-weight: 400;
  transition: color var(--dur-base);
}

.foot-col a {
  color: #fff;
}

/* An entry with no destination yet. It reads as a label, so the footer never
   offers a link that goes nowhere. Distinguished by tone, not by opacity. */
.foot-col .foot-pending {
  color: var(--muted-on-dark);
  cursor: default;
}

/* The footer background composites dark but is translucent, so these links
   take the dark-surface hover role, not the light one. */
:is(.foot-col a, .foot-contact):is(:hover, :focus-visible) {
  color: var(--link-hover-on-dark);
}

:is(.foot-col a, .foot-contact):is(:hover, :focus-visible) > i.ph {
  color: inherit;
}

.foot-contact bdi {
  direction: ltr;
  unicode-bidi: isolate;
}

.foot-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-top: 30px;
  flex-wrap: wrap;
  gap: 16px;
}

.copy {
  /* Local .85rem loses to the SYSTEM LAYER caption role. */
  font-size: var(--type-caption);
}

.foot-socials {
  display: flex;
  align-items: center;
  gap: 16px;
}

.foot-socials a {
  inline-size: 40px;
  block-size: 40px;
  display: grid;
  place-items: center;
  /* No outline on the tile. The system separates surfaces by tone, not by a
     line, and on the footer's near-black a .3 white hairline was the most
     visible thing in the row. The .08 fill is the tone step. */
  border: 0;
  border-radius: var(--r);
  color: var(--icon-on-dark);
  background: rgba(255, 255, 255, .08);
  transition: background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.foot-socials a:is(:hover, :focus-visible) {
  background-color: var(--brand-razeen);
  color: var(--brand-black);
  transform: scale(1.06);
}

.foot-socials i.ph {
  font-size: var(--ic-sm);
  line-height: 1;
}

/* =============================================================================
   10. MOTION: SCROLL REVEAL
   -----------------------------------------------------------------------------
   Content is visible by default. The script adds `.armed` to opt an element
   in to animating, then `.in` when it enters the viewport. If the script never
   runs, nothing is hidden.
   ========================================================================== */

.reveal.armed {
  opacity: 0;
  visibility: hidden;
  transform: translate3d(0, 24px, 0);
  transition:
    opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out),
    visibility 0s linear var(--dur-slow);
  will-change: opacity, transform;
}

.reveal.armed.in {
  opacity: 1;
  visibility: visible;
  transform: translate3d(0, 0, 0);
  transition-delay: 0s;
  will-change: auto;
}

.reveal.armed[data-d="1"] { transition-delay: 80ms; }
.reveal.armed[data-d="2"] { transition-delay: 160ms; }
.reveal.armed[data-d="3"] { transition-delay: 240ms; }

.reveal.armed.in[data-d="1"] { transition-delay: 80ms; }
.reveal.armed.in[data-d="2"] { transition-delay: 160ms; }
.reveal.armed.in[data-d="3"] { transition-delay: 240ms; }

/* =============================================================================
   RESPONSIVE
   -----------------------------------------------------------------------------
   Only genuinely breakpoint-specific overrides live below. Ordered widest to
   narrowest so a narrower block always wins where two match.
   ========================================================================== */

/* --- Desktop, 981 and up, NO upper bound: the complete six-item sitemap
       needs the compact header values already used below 1180. Type never
       shrinks; the logo, gaps and CTA padding step to their established
       compact values.

       THIS USED TO CUT OFF AT max-width: 1500px, on the assumption that past
       1500 there was "enough room" for the roomier rest-state values (a
       211px logo, a 124px CTA button, a 22px-30px menu gap) to fit again.
       There isn't, at any width: .wrap is capped at var(--max) = 1280px, so
       the box available to .logo + .menu + .nav-actions stops growing at
       1280px viewport and never grows again - it does not care how much
       further the true viewport goes. Reverting to the roomier values past
       1500 put "About Sdeira" back under the logo and "News & Media" back
       under the search icon (measured on /en/companies/, clean at 1400,
       broken at 1600 - EN's longer labels hit this before Arabic's do). The
       compact values are correct for every desktop width above 981px, not
       just a middle band, because the constraint that demands them (--max)
       never goes away. */
@media (min-width: 981px) {
  .wrap {
    width: min(calc(100% - 84px), var(--max));
  }

  .logo img,
  #navLogo {
    max-width: 220px;
  }

  .menu {
    gap: 22px;
  }

  .nav-actions {
    gap: 8px;
  }

  .nav-cta {
    padding-inline: 20px;
  }
}

/* --- Desktop only: a hit-area bridge under the dropdown trigger, so
       travelling into the panel is never read as leaving the trigger, and a
       panel on its way out stops answering the pointer. --------------------- */
@media (min-width: 981px) {
  .nav-disclosure.is-closing > .nav-submenu {
    pointer-events: none;
  }

  .nav-submenu::before {
    content: "";
    position: absolute;
    inset-inline: 0;
    inset-block-end: 100%;
    block-size: 10px;
  }
}

/* --- Below 1180: the nav row tightens before it has to break. ------------- */
@media (max-width: 1180px) {
  .menu {
    gap: 22px;
  }

  .nav-actions {
    gap: 8px;
  }

  .nav-cta {
    padding: 0 20px;
  }
}

/* --- Below 1280: the desktop menu becomes a right-edge drawer. -----------
   1280, not the 980 this was. The number was never measured against the nav
   it gates, and the nav outgrew it: the bar stops fitting between the logo
   and the icon controls well above 980, and because .menu carries
   overflow: visible nothing clipped - the links simply slid UNDER the logo on
   one side and under the search icon on the other, with no error and no
   scrollbar.

   Measured overlap at the moment it was found, per family:

     /en/* (32 pages)  81px at 1000, 42px at 1100, 10px at 1180, 1px at 1200,
                       first clean at 1240
     /news/* (5 pages)  8px at 1000, 2px of slack at 1024

   English is far worse because its labels are longer ("Projects &
   Communities", "Group Companies"), but Arabic overlapped too - this was not
   a one-language bug, it was one wrong number shared by both.

   1280 is where every page family on the site measures clean, with room to
   spare rather than to the pixel, so a label that grows by a word does not
   put it straight back. */
@media (max-width: 1280px) {
  body.menu-open {
    overflow: hidden;
    overscroll-behavior: none;
  }

  .nav {
    gap: 18px;
  }

  .logo img,
  #navLogo {
    max-width: 180px;
    /* A fixed height here, not var(--logo-h): the drawer breakpoint stops the
       logo compressing with the bar. */
    height: 42px;
  }

  .nav-cta {
    display: none;
  }

  .nav-actions > .lang {
    display: none;
  }

  .burger {
    display: flex;
    position: relative;
    margin-inline-end: 0;
    border: 0;
    background: transparent;
  }


  body.menu-open .burger {
    opacity: 0;
    pointer-events: none;
  }

  /* The drawer panel itself. It is always rendered and always positioned; the
     open class only flips visibility, opacity and the slide. */
  .menu {
    display: flex;
    position: fixed;
    z-index: 1101;
    top: 0;
    right: 0;
    bottom: 0;
    left: auto;
    inline-size: min(88vw, 380px);
    block-size: 100dvh;
    padding: 16px var(--page-gutter) max(24px, env(safe-area-inset-bottom));
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--surface-raised);
    color: var(--ink);
    visibility: hidden;
    pointer-events: none;
    opacity: .98;
    transform: translateX(100%);
    transition:
      transform var(--dur-base) var(--ease-out),
      opacity var(--dur-base) var(--ease-out),
      visibility 0s linear var(--dur-base);
  }

  .menu::after {
    content: "";
    position: absolute;
    z-index: -1;
    inset-inline-start: -34px;
    inset-block-end: 54px;
    inline-size: 160px;
    aspect-ratio: 1;
    //background: url("../images/brand/sdeira-mark.png") center / contain no-repeat;
    opacity: .055;
    pointer-events: none;
  }

  /* Two passes wrote this state. The open panel's own z-index and background
     are more specific than the .menu rule above, so they win: 1001, and the
     cream page surface rather than white. Both reproduced as they resolve. */
  .menu.open {
    display: flex;
    position: fixed;
    z-index: 1001;
    top: 0;
    right: 0;
    bottom: 0;
    left: auto;
    flex-direction: column;
    align-items: stretch;
    background: var(--offwhite);
    padding: 16px var(--page-gutter) max(24px, env(safe-area-inset-bottom));
    gap: 0;
    visibility: visible;
    pointer-events: auto;
    opacity: 1;
    transform: translateX(0);
    transition-delay: 0s;
  }

  .menu.open a {
    min-height: 44px;
    display: flex;
    align-items: center;
    color: var(--ink);
  }

  .menu-head {
    min-block-size: 64px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    border-block-end: 1px solid var(--line);
  }

  .menu-logo {
    min-block-size: 44px;
    display: flex;
    align-items: center;
  }

  .menu-logo img {
    display: block;
    inline-size: min(152px, 52vw);
    block-size: auto;
  }

  /* The desktop underline has no job inside the drawer. */
  .menu-logo::after,
  .menu > a:not(.menu-logo)::after {
    display: none;
  }

  /* The close control is the shared icon family now - geometry, radius, ink and
     hover all live in assets/css/sdeira-nav-controls.css, and the 1px --line
     box and the two rotated <span> bars that used to be here are gone with the
     markup that fed them. */

  /* Drawer rows. NOTE: the local font-size: 1.05rem here is overridden by the
     unlayered control role, so these render at var(--type-control). The
     declaration is therefore omitted rather than copied as dead weight. */
  .menu > a:not(.menu-logo) {
    position: relative;
    min-block-size: 54px;
    padding-inline: 4px 12px;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    border-block-end: 1px solid var(--line);
    color: var(--ink);
    font-family: var(--font-display);
    font-weight: 600;
    opacity: 0;
    transform: translateX(10px);
    transition:
      color var(--dur-fast) var(--ease-out),
      background var(--dur-fast) var(--ease-out),
      opacity var(--dur-base) var(--ease-out),
      transform var(--dur-base) var(--ease-out);
  }

  .menu.open > a:not(.menu-logo) {
    opacity: 1;
    transform: translateX(0);
  }

  .menu.open > a:nth-of-type(2) { transition-delay: 45ms; }
  .menu.open > a:nth-of-type(3) { transition-delay: 70ms; }
  .menu.open > a:nth-of-type(4) { transition-delay: 95ms; }
  .menu.open > a:nth-of-type(5) { transition-delay: 120ms; }
  .menu.open > a:nth-of-type(6) { transition-delay: 145ms; }
  .menu.open > a:nth-of-type(7) { transition-delay: 170ms; }
  .menu.open > a:nth-of-type(8) { transition-delay: 195ms; }
  .menu.open > a:nth-of-type(9) { transition-delay: 220ms; }

  /* Fill is the state for drawer rows, not a text colour.

     THE SUMMARY IS IN THIS LIST TOO, and has to be: at this width the
     underline it uses in the bar is display:none, so a current section with a
     dropdown would be the one nav item in the drawer with no state at all -
     which is exactly what Land Lease showed, where the section is marked and
     no child entry is. Same fill, same rule, so the two shapes of nav item
     cannot say "you are here" two different ways. */
  .menu > a:not(.menu-logo):hover,
  .menu > a:not(.menu-logo):focus-visible,
  .menu > a:not(.menu-logo).active,
  .nav-disclosure > summary.active {
    background: var(--surface-sunken);
    color: var(--accent-text);
  }

  .menu > a:not(.menu-logo).active::before {
    content: "";
    position: absolute;
    inset-inline-start: 0;
    inline-size: 3px;
    block-size: 24px;
    background: var(--accent);
  }

  /* Disclosures inside the drawer: full-width rows that push their siblings. */
  .menu > .nav-disclosure {
    inline-size: 100%;
    border-block-end: 1px solid var(--line);
    opacity: 0;
    transform: translateX(10px);
    transition:
      opacity var(--dur-base) var(--ease-out),
      transform var(--dur-base) var(--ease-out);
  }

  .menu.open > .nav-disclosure {
    opacity: 1;
    transform: translateX(0);
  }

  .nav-disclosure > summary {
    min-block-size: 54px;
    padding-inline: 4px 12px;
    justify-content: space-between;
    color: var(--ink);
    font-family: var(--font-display);
    font-weight: 600;
  }

  .nav-disclosure > summary::after {
    display: none;
  }

  .nav-disclosure[open]:not(.is-closing) > summary {
    background: var(--accent-fill);
    color: #fff;
  }

  /* The visible label is the <a> nested inside the summary (site-alignment
     instructions, 13 Aug 2026: every dropdown's label is itself a real link),
     and `.menu.open a` above sets every drawer link to --ink at a higher
     specificity than the white this rule sets on the summary. Colour does not
     inherit past that explicit declaration, so the label stayed --ink (near-
     black) on the accent-fill background it now sits on - unreadable, since
     that pairing is the white-text-only fill the token file documents.
     Reported 15 Aug 2026. */
  .nav-disclosure[open]:not(.is-closing) > summary a {
    color: inherit;
  }

  .nav-submenu {
    position: static;
    inline-size: 100%;
    padding: 4px 14px 12px;
    background: #fff;
    border-block-start: 0;
    border-inline-start: 0;
    border-radius: var(--r);
  }

  /* The drawer panel is in flow and has to push the rows under it, so it
     animates its measured height. Clipping is what makes that height readable
     as a reveal; the movement itself is driven from the script. */
  .menu .nav-submenu {
    overflow: hidden;
  }

  .menu .nav-submenu a {
    min-block-size: 44px;
    padding-inline: 10px;
  }

  /* The mobile action block at the foot of the drawer. */
  .menu.open .menu-mobile-actions {
    width: 100%;
    display: flex;
    flex-direction: column;
    margin-block-start: auto;
    padding-block-start: 14px;
    gap: 10px;
    border-block-start: 1px solid var(--line);
  }

  .menu.open .menu-lang {
    display: flex;
    width: 100%;
    min-height: 48px;
    align-items: center;
    justify-content: flex-start;
    padding-inline: 4px;
    gap: 10px;
    color: var(--charcoal);
    font-weight: 700;
  }

  .menu.open .menu-contact {
    width: 100%;
    min-height: 52px;
    display: flex;
    justify-content: center;
    background: var(--accent-deep);
    color: #fff;
    border-radius: var(--r);
    font-weight: 800;
  }

  /* Tile-style state, matching the other icon tiles rather than the link
     hover role. */
  .menu.open .menu-contact:hover {
    background: var(--brand-razeen);
    border-color: var(--brand-razeen);
    color: var(--brand-black);
  }

  .menu-backdrop {
    display: block;
    z-index: 999;
    visibility: hidden;
    pointer-events: none;
    opacity: 0;
    background: rgba(var(--image-overlay-rgb), .44);
    backdrop-filter: blur(4px);
    transition:
      opacity var(--dur-base) var(--ease-out),
      visibility 0s linear var(--dur-base);
  }

  body.menu-open .menu-backdrop {
    display: block;
    visibility: visible;
    pointer-events: auto;
    opacity: 1;
    transition-delay: 0s;
  }

  body.menu-open .scroll-top {
    z-index: 997;
  }

}

/* The footer's column count is NOT part of the drawer, and was only ever in
   that block because 980 used to be both answers. Lifted out when the drawer's
   trigger moved to 1280 - a two-column footer at 1280 was never the design. */
@media (max-width: 980px) {
  .foot-grid {
    grid-template-columns: 1fr 1fr;
  }
}

/* --- Below 720: the page gutter narrows and section wraps follow it. ------ */
@media (max-width: 720px) {
  :root {
    --page-gutter: clamp(14px, 4.6vw, 18px);
  }

  main > section > .wrap,
  footer > .wrap {
    padding-inline: var(--page-gutter);
  }

  .menu-lang {
    gap: 9px;
  }
}

/* --- Below 560: the page gutter tightens and the footer stacks. The bar's own
       height is handled by the clamps on #hd, not here. ------------------- */
@media (max-width: 560px) {
  .wrap {
    padding: 0 20px;
  }

  .nav-actions {
    gap: 6px;
  }

  /* The logo tracks the bar state again below the drawer breakpoint. */
  .logo img,
  #navLogo {
    max-width: min(42vw, 146px);
    height: var(--logo-h);
  }

  /* The 40x40 this used to shrink to at the narrowest width is gone: it was
     below the 44px target on the one control that opens navigation, on the
     viewport where that matters most. The family holds 44 at every width. */

  .scroll-top {
    left: 16px;
    bottom: max(16px, env(safe-area-inset-bottom));
    width: 34px;
    height: 34px;
    /* scale-exempt: the same tile glyph, stepped with the tile. */
    font-size: .9rem;
  }

  .foot-grid {
    grid-template-columns: 1fr;
  }

  .foot-brand .logo {
    inline-size: min(190px, 100%);
  }
}

/* --- Below 420: the language switch is dropped and the row closes up. ----- */
@media (max-width: 420px) {
  .nav {
    gap: 10px;
  }

  .logo img,
  #navLogo {
    max-width: min(39vw, 146px);
  }

  .lang {
    display: none;
  }

  .nav-actions {
    gap: 4px;
  }
}

/* --- Reduced motion ------------------------------------------------------
   The homepage uses a blanket `*` reset carrying important flags. A universal
   selector has zero specificity, so without those flags it would lose to
   every rule above. Instead each affected selector is repeated here at its
   own specificity, later in source order, which needs no flag at all. Add a
   line here whenever a new transition is added above. -------------------- */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  /* Reduced motion must never mean reduced content. */
  .reveal.armed {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: none;
  }

  #hd,
  #hd::before,
  .logo,
  .logo img,
  #navLogo,
  .menu,
  .menu a,
  .menu a::after,
  .menu-backdrop,
  .menu > a:not(.menu-logo),
  .lang,
  .menu-lang,
  .nav-disclosure,
  .nav-disclosure > summary,
  .nav-disclosure > summary::after,
  .nav-disclosure > summary i.ph,
  .nav-submenu-hover-icon,
  .menu .nav-submenu a,
  .btn,
  .btn-primary,
  .btn-ghost,
  .nav-cta,
  .btn-primary::before,
  .btn-ghost::before,
  .nav-cta::before,
  .more::after,
  .action-link,
  .action-link::after,
  .action-link > i.ph,
  .skip-link,
  .scroll-top,
  .foot-col a,
  .foot-socials a {
    transition: none;
  }

  .foot-socials a:is(:hover, :focus-visible) {
    transform: none;
  }
}


/* =============================================================================
   8. UTILITIES
   ========================================================================== */

/* Available to screen readers, removed from the visual flow. Used for section
   headings that the layout does not need to show but the document outline
   does. */
.visually-hidden {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ==========================================================================
   9. SUBPAGE HERO
   A short identity band, not a full-viewport hero. Photograph under a scrim at
   or above the .86 floor documented in section 11 of the token file, with the
   Sdeira mark's parallel bars laid over it as the page's identity signature.
   ========================================================================== */

/* Plain block flow, deliberately. The hero used to be a flex container so the
   content could sit at the bottom of the panel, and that is what threw the
   heading out of alignment with every other section: .wrap carries
   `margin: 0 auto`, and auto inline margins opt an item out of stretch
   alignment in BOTH flex and grid, so it sized to its content and the auto
   margins then centred that shrunken box.
   Three fixes were measured before this one. `inline-size: 100%` fixed narrow
   viewports and broke wide ones, where .wrap is width: min(100% - 84px, --max)
   and the hero then ran 84px wider than the rest of the page. A single-column
   grid did not stretch it, for the auto-margin reason above. Column flex did
   not either, for the same reason.
   In normal flow .wrap is just a block, so its width, max-width and auto
   margins resolve exactly as they do in the breadcrumb and the card grid, and
   the heading lines up at every breakpoint with no special case. The generous
   block-start padding is what seats the content low in the panel; that is the
   only thing the flex container was buying. */
.subpage-hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  /* Taller than it needs to be for its copy, deliberately, and this is what
     buys the scrim its shape. The floor for text over photography is fixed
     at .86 (section 11 of the token file), so the only way to spend the ramp
     across the whole photograph instead of dumping it just above the text is
     to seat the text lower: the copy now starts around 60% down, which
     leaves the top 60% for a gradient that is still climbing. Shrink this
     padding and the ramp has to finish sooner, which is exactly the flat
     black band this was changed to get rid of. */
  min-block-size: clamp(360px, 46vh, 460px);
  display: block;
  padding-block: clamp(200px, 27vh, 290px) clamp(34px, 4vw, 52px);
  background: var(--surface-inverse);
  color: var(--brand-white);
}

.subpage-hero-media {
  position: absolute;
  inset: 0;
  z-index: -2;
}

/* Slow zoom on load, the same treatment the homepage hero gives its active
   slide: scale(1.08) resting, easing back to 1 over 7s. Same duration and
   same easing as `slowZoom` in sdeira-v2.css - one motion, written twice only
   because the two cascades never meet (see "The sub-page system" in
   CLAUDE.md). `forwards` so the frame it settles on is the one the visitor
   keeps looking at. .subpage-hero already clips, so the overscan never
   escapes the panel. */
.subpage-hero-media img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  object-position: center 62%;
  transform: scale(1.08);
  animation: subpage-hero-zoom 7s var(--ease-out) forwards;
}

@keyframes subpage-hero-zoom {
  to {
    transform: scale(1);
  }
}

/* Scrim: black, running the full height of the photograph.

   ONE CONTINUOUS CURVE. NO PLATEAU, NO CORNER.
   The previous version was three straight segments (.02 -> .12 -> .38 -> .86)
   ending in a dead-flat hold from 60% down. Two things about that shape were
   visible on the page and neither was a rendering artefact: the slope tripled
   between the 45% and 60% stops, which the eye reads as an edge, and then it
   dropped to zero at 60%, which the eye reads as the bottom of a band. A
   piecewise-linear ramp always shows its joints, because human vision responds
   to the RATE of change, not the value. So the joints are gone: the stops
   below are sampled from a smoothstep (3x^2 - 2x^3), whose slope starts at
   zero, peaks in the middle and returns to zero. There is no interior point
   where the rate jumps, so there is nothing to see the edge of.

   IT IS ALSO MUCH LIGHTER, AND THAT WAS MEASURED RATHER THAN JUDGED.
   The old comment here asserted a .86 floor from section 11 of the token file
   and called it non-negotiable. That was a misreading: section 11 governs FLAT
   layers built on brand Black, rgba(65,64,66,a), where .86 composites to a
   known grey. This scrim is pure black (--image-overlay is #000000), which is
   a different material - at the same alpha it composites far darker - and it
   is a gradient, so no single alpha describes it. The old ramp did not even
   obey its own stated floor: the breadcrumb, the smallest text on the panel,
   starts at 51% of the height, where that gradient was still only at .57.

   What actually binds is the brightest pixel of the photograph behind each
   line of text. Sampled off news/index.html's own hero at 5% intervals through
   the whole text band, the worst pixel needs alpha .46-.49 for white text to
   clear 4.5:1, and .32-.36 to clear 3:1. The curve below sits at .55 where the
   breadcrumb starts and climbs to .72 at the foot - between 6 and 25 points of
   headroom over the measured requirement at every line, on the brightest
   pixel, not the average one.

   HOW TO RE-DERIVE THIS FOR A NEW HERO IMAGE, rather than guessing: draw the
   image to a canvas, walk the text band, take the maximum relative luminance
   per row, and solve for the alpha at which white text clears 4.5:1. If that
   number exceeds the curve at that height, raise the whole curve - do not add
   a stop, which is what put the corner here in the first place. */
.subpage-hero-media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to bottom,
    rgba(var(--image-overlay-rgb), .03) 0%,
    rgba(var(--image-overlay-rgb), .06) 6%,
    rgba(var(--image-overlay-rgb), .12) 13%,
    rgba(var(--image-overlay-rgb), .20) 19%,
    rgba(var(--image-overlay-rgb), .30) 25%,
    rgba(var(--image-overlay-rgb), .39) 31%,
    rgba(var(--image-overlay-rgb), .47) 38%,
    rgba(var(--image-overlay-rgb), .53) 44%,
    rgba(var(--image-overlay-rgb), .55) 50%,
    rgba(var(--image-overlay-rgb), .56) 56%,
    rgba(var(--image-overlay-rgb), .58) 63%,
    rgba(var(--image-overlay-rgb), .63) 75%,
    rgba(var(--image-overlay-rgb), .69) 88%,
    rgba(var(--image-overlay-rgb), .72) 100%);
}

/* The identity bands, the same mark the homepage hero and the closing CTA
   carry: three skewed outlines in Sweihan sitting in the empty photo space
   past the copy column, never crossing the text. Geometry comes from the
   shared --identity-band-* tokens, so this band group and the homepage's are
   one set of measurements, not two.
   z-index 0 puts them over the scrim (the media block is at -2) and under
   .wrap, which is at 1. */
.subpage-hero-glass {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

/* Geometry from --identity-band-*, tone from --identity-band-*-on-photo.
   That split is the rule now, not a local choice: see "ONE TONE FOR A BAND ON
   A PHOTOGRAPH" in the token file. This panel is what made it explicit - at
   the flat-surface strength (solid Sweihan outline, 12% fill) the three bands
   read as five bright orange diagonals over the photograph and pulled
   attention off the heading. */
.subpage-hero-glass span {
  position: absolute;
  top: var(--identity-band-top);
  height: var(--identity-band-height);
  width: var(--identity-band-width);
  border: var(--identity-band-border) solid
    color-mix(in srgb, var(--brand-sweihan) var(--identity-band-line-on-photo), transparent);
  border-radius: var(--r);
  background: color-mix(in srgb, var(--brand-sweihan) var(--identity-band-fill-on-photo), transparent);
  transform: skewX(-24deg);
}

.subpage-hero-glass .r1 { inset-inline-start: 68%; }
.subpage-hero-glass .r2 { inset-inline-start: 78%; }

/* Third band steps back so the group reads as depth, not three equals - the
   same relationship the homepage hero has between its own three. Half the
   line strength the other two carry, derived from the token rather than
   written as a second number, so it follows if the on-photo tone is ever
   retuned. */
.subpage-hero-glass .r3 {
  inset-inline-start: 88%;
  border-color: color-mix(in srgb,
    var(--brand-sweihan) calc(var(--identity-band-line-on-photo) / 2), transparent);
}

/* Below the drawer breakpoint the copy column fills the panel, so there is no
   empty photo space left for the bands to sit in without crossing the text. */
@media (max-width: 920px) {
  .subpage-hero-glass { display: none; }
}

/* =============================================================================
   THE PLAIN SUB-PAGE HERO'S OWN TREATMENT (subpage-hero--photo).
   A flat scrim covers the whole photograph, so the panel no longer competes
   with the picture as a separate shape. .wrap carries no background of its
   own; the section's own `color: var(--brand-white)` already reaches the
   heading and breadcrumb, scrim or not.

   LAYOUT: centred content in a compact hero. This hero does not carry a
   hugging copy panel any more, so it no longer needs the vertical room the
   old taller padding bought. The top padding is the header-clearance number
   from .subpage-hero's own flat-ground padding-block-start, the value already
   proven to clear the fixed header.
   ========================================================================== */
.subpage-hero--photo {
  /* display: grid; */
  grid-template-columns: minmax(0, 1fr);
  align-content: center;
  min-block-size: clamp(240px, 24vw, 340px);
  padding-block: clamp(118px, 16vh, 170px) clamp(24px, 3vw, 36px);
}

/* 981px and up: align the photo-hero breadcrumb + title with the content
   column so it starts where the page's sections start (25 Sept change
   request; between ~981px and 1360px the old offset was ~42px). */
@media (min-width: 981px) {
  .subpage-hero--photo > .wrap {
    width: min(calc(100% - 84px), var(--max));
  }
}

/* Site owner: asked for the photograph itself to carry some blur under the
   flat scrim, for a softer read than a sharp photo. Reuses --glass-blur
   (18px) rather than inventing a second blur number. */
.subpage-hero--photo .subpage-hero-media img {
  filter: blur(var(--glass-blur));
}

/* The flat scrim, painted on the media's own ::after so it stays inside that
   element's stacking context, directly over the image it darkens and nothing
   else. --scrim-glass (.74) is calibrated for this blurred surface - the same
   blur the token file's worked example uses to justify the lower floor. This
   replaces the base hero's tall gradient scrim for the photo variant. */
.subpage-hero--photo .subpage-hero-media::after {
  background: var(--scrim-glass);
}

/* The p.43 corner marks, in place of the identity bands for the photo hero.
   Same pair, same files, same ratio the closing CTA band carries
   (--photo-mark-p43-ratio, lines.svg / lines-rotated-180.svg). z-index 0 sits
   above the scrim (the media block is at -2) and below .wrap (1).

   :not(.subpage-hero--error) - the error pages carry their own photo ground
   and their own header rule: no stripe or watermark decoration there. */
.subpage-hero--photo:not(.subpage-hero--error)::before,
.subpage-hero--photo:not(.subpage-hero--error)::after {
  content: "";
  position: absolute;
  z-index: 0;
  block-size: 62%;
  aspect-ratio: var(--photo-mark-p43-ratio);
  pointer-events: none;
}

.subpage-hero--photo:not(.subpage-hero--error)::before {
  inset: 0 auto auto 0;
  background: url("../images/patterns/lines-rotated-180.svg") no-repeat left top / 100% 100%;
}

.subpage-hero--photo:not(.subpage-hero--error)::after {
  inset: auto 0 0 auto;
  background: url("../images/patterns/lines.svg") no-repeat right bottom / 100% 100%;
}

/* Staggered entrance for the hero copy, so a sub-page opens the way the
   homepage does instead of appearing fully formed.
   A CSS animation, not the .sweep/.reveal classes the rest of the page uses:
   both of those are script-driven and arm on scroll, and the hero is already
   in view on load. The homepage hit exactly this and reached the same answer
   (see the `hero-reveal` comment in sdeira-v2.css) - a transition needs a
   painted "armed" frame to move from and can silently lose that race on a
   warm cache, an animation does not.
   Longhand, not the `animation` shorthand: the shorthand resets
   animation-delay, which would collapse the stagger below. */
@keyframes subpage-hero-reveal {
  from {
    opacity: 0;
    transform: translateY(24px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.subpage-hero .breadcrumb--hero,
.subpage-hero .kicker,
.subpage-hero h1,
.subpage-hero p {
  animation-name: subpage-hero-reveal;
  animation-duration: var(--dur-base);
  animation-timing-function: var(--ease-out);
  animation-fill-mode: both;
}

.subpage-hero .kicker,
.subpage-hero h1 {
  animation-delay: calc(var(--dur-fast) * 1);
}

html[dir="rtl"] .subpage-hero h1 {
    line-height: 1.5;
}

.subpage-hero p {
  animation-delay: calc(var(--dur-fast) * 2);
}

/* Motion only. The scrim, the bands and the layout are unaffected, so the
   panel still reads exactly the same standing still. */
@media (prefers-reduced-motion: reduce) {

  .subpage-hero-media img {
    transform: none;
    animation: none;
  }

  .subpage-hero .breadcrumb--hero,
  .subpage-hero .kicker,
  .subpage-hero h1,
  .subpage-hero p {
    animation: none;
  }
}

/* The wrap is a flex item of .subpage-hero, so without an explicit width it
   sizes to its content and `margin: 0 auto` then centres that shrunken box.
   That is what pulled the heading inward, out of line with the breadcrumb and
   the card grid below it. It fills the section, so the heading starts exactly
   where every other section on the page starts. */
.subpage-hero .wrap {
  position: relative;
  z-index: 1;
}

/* Tighter than the default kicker spacing: the label and the heading are one
   unit here, not two stacked blocks.
   White, not Razeen. Section 11 of the token file allows Razeen over a scrim
   for large text only, never for body size, and this label is meta size.
   Measured against the article heroes it comes out at 3.64:1 and 4.34:1
   where meta text needs 4.5:1, so the rule is right and this used to break
   it. Accent orange is worse still here, 2.15:1, which is why the badges on
   this panel stay white while the ones on the cards are orange. */
.subpage-hero .kicker {
  color: var(--brand-white);
  margin-block-end: 4px;
}

.subpage-hero h1 {
  font-size: var(--type-display);
  line-height: var(--leading-display);
  margin: 0;
  max-inline-size: 22ch;
  color: var(--brand-white);
}

.subpage-hero p {
  margin: 14px 0 0;
  max-inline-size: 62ch;
  font-size: var(--type-lead);
  line-height: var(--leading-lead);
  color: var(--brand-white);
}

/* THE HERO'S ACTION ROW IS THE CLOSING BAND'S ACTION ROW. Two pages open with
   a button - the investment enquiry and the land enquiry - and rather than a
   hero-only wrapper this reuses .cta-actions, which already carries the row's
   geometry and its gap. Only one thing differs on a hero: the band centres its
   actions under centred copy, and hero copy starts at the reading edge, so its
   actions start there too. A hero with no actions renders no such element. */
.subpage-hero .cta-actions {
  justify-content: start;
  margin-block-start: 22px;
}

/* An article headline needs more room than a section title. It is no longer
   "the one place a story's own photograph carries the page identity" - that
   comment described the previous version of this panel. Every article hero
   now shares one unified graphic instead of four different photo crops each
   competing with the heading for attention; the story's own photo moved into
   the body as a normal <figure>, where it belongs to the reading flow instead
   of decorating the top of the page. */
/* CENTRED ON ONE AXIS ONLY: the block axis, not the inline one. The panel
   holds its copy in the middle of its own height, which is what makes it read
   as a title card; the title itself starts at the page's reading edge, in
   line with the breadcrumb above it, the meta band below it and every section
   further down. It was centred on both axes for one session and that broke
   that line - the headline floated in the middle of the panel with nothing on
   the page agreeing with its edge.

   align-content is what does the block centring, and .wrap stays a normal
   block so its max-width and auto margins resolve exactly as they do in every
   other section (the note on .subpage-hero .wrap above is the history). The
   block-start padding still outweighs the block-end one - the fixed header
   floats over the top of this panel, so equal padding would look bottom-heavy
   even though it measures even. */
/* Article heroes are the shared .subpage-hero--photo treatment (reference:
   scripts/components.py HERO_VARIANTS + subpage.css, "GROUND FLIPPED TO
   photo", 28 Aug 2026). The flat deep ground, the tall clamp min-block-size,
   the oversized padding, the grid centering and the forced wrap width below
   belonged to the retired flat design and are removed so the shared photo
   hero's compact height, spacing and scrim govern here too. Only the h1
   sizing stays article-specific (see the News index section below). */

/* THE GRID ABOVE RE-OPENED THE BUG THE COMMENT ON .subpage-hero DESCRIBES, so
   read that one first. .wrap carries `margin: 0 auto`, and an auto inline
   margin opts an item out of stretch alignment in both flex AND grid: the wrap
   shrink-wrapped its own text and the auto margins then centred that shrunken
   box, which is why the headline and its breadcrumb sat about 200px inboard of
   the meta band, the article card and the header logo at 1500px wide. Nothing
   errors; the copy simply stops agreeing with the column it belongs to.

   `inline-size: 100%` is the fix, not removing the auto margins: the width
   still resolves to min(100%, --max) and the margins still centre that box,
   exactly as they do for a wrap in normal flow. The block centring the grid is
   here for is untouched. */

/* No line-clamp here, and this is not just the token file's general "reserve,
   do not clamp" rule - -webkit-line-clamp genuinely corrupted this Arabic
   title. -webkit-line-clamp requires display: -webkit-box, an old pre-flexbox
   layout mode never designed for complex-script shaping; on a long title it
   visually mangled letterforms mid-word (measured: "Ø§Ø³ØªØ±Ø§ØªÙŠØ¬ÙŠØ©" painted as
   "Ø§Ø³ØªØ±Ø§ØªÙŠØ­Ø©", a dropped ÙŠ and a reshaped Ø¬, on a title that DOM-inspects as
   correct - innerText matched the source exactly, so this was a paint bug,
   not corrupted content). max-inline-size still caps the line width so the
   hero does not run edge to edge; the panel simply grows to however many
   lines the real title needs, and .subpage-hero--article's min-block-size
   above is a minimum, not a fixed height, so it has room to do that. */
/* No auto inline margin: the cap limits the line length, it does not move the
   block. The heading sits at the wrap's start edge like everything else. */
/* THE SIZE, SEPARATE FROM EVERY OTHER PAGE HERO. Every other .subpage-hero h1
   is a short route name ("About Us", "Careers") at --type-display, so the
   generic role never had to hold a full press-release sentence. An article
   headline is 80-120 characters, and at --type-display + this panel's old
   26ch cap that ran to 3-5 lines (measured on all four live articles) -
   exactly the "4 lines, disfigures the panel" the site owner flagged. Smaller
   size, wider measure: at this pairing all four current headlines hold to 2
   lines at desktop width (measured up to the 115-character CERT title, the
   longest live one). --type-feature is an existing token role, not a new
   literal - the same move as .split--headline on the news index's pinned
   story, a long editorial sentence taking a scale one step down from the
   short-title role above it, in a size the system already has.
   THIS IS NOT A GUARANTEE AT EVERY WIDTH OR EVERY FUTURE HEADLINE. At a
   375px viewport the same four titles still run 4-5 lines - the panel above
   is explicit that clamping the box to force 2 lines is what corrupted
   Arabic letterforms, so there is no CSS floor under this that holds for an
   arbitrarily long sentence. A hard 2-line guarantee on mobile needs
   headlines written under roughly 45-50 characters; that is an editorial
   budget, not a mechanism this rule can enforce - the same discipline the
   token file already asks for section leads ("two lines, not four"). */
.subpage-hero--article h1 {
  font-size: var(--type-feature);
  line-height: var(--leading-feature);
  max-inline-size: 54ch;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 1;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The ground every article hero carries, in place of a photo.

   THE SURFACE IS --surface-brand-deep, FLAT. Deep Sweihan, not Black, on the
   site owner's direction (2026-08-04). Read the token's own comment before
   changing it: that value is not one of the eight brand colours, it is the
   darkened Sweihan --warn-text already carried, and the decision to use it as
   a surface is recorded there. White on it measures 5.76, so every size of
   text in this panel clears AA - which the palette's own orange (3.32) would
   not have done for the breadcrumb.

   The paragraph below describes the Black version this replaced and is kept
   because its argument still governs anything layered on top of this panel:

   THE SURFACE WAS --surface-inverse, FLAT. This panel used to be a
   Black-to-Sweihan gradient washed over with a repeating white stripe field
   and then darkened again by a .86 scrim, three layers deep. Everything that
   stack was trying to do is done by the system already, and the mix it landed
   on - a muddy brown that is neither of the two colours that made it - is the
   exact thing the palette rule exists to prevent: eight brand colours, and a
   gradient between two of them still has to READ as one of the eight. Black
   is the system's dark surface (docs/brand/black-usage-research.md is why it
   is #414042 and not a near-black), white on it measures 10.31, and it needs
   nothing on top to be legible.

   NOTHING ON TOP OF IT. A soft Sweihan radial glow sat here for a session,
   and it was wrong for a reason worth writing down: a radial that fades to
   transparent inside its own box does not read as light, it reads as a stain
   with a visible edge where the ramp runs out. Half the panel warm, half of
   it flat, and a soft boundary between the two that no element on the page
   explains. Black is the system's dark surface and it is finished as it is;
   depth here comes from the bands below, which are geometry, not from a
   gradient nobody can place the edge of.

   Both scrim and stripe field are gone with the photograph they were written
   for. There is no image here to veil and no busy pattern to hold text off. */
/* The flat ground is retired (see the note at the top of this variant): the
   media layer keeps the shared photo hero's background, and the ::after scrim
   is NOT removed, so the shared --photo scrim darkens the photograph for text
   contrast exactly as it does on every other photo hero. */

/* A FIELD OF BANDS ACROSS THE WHOLE PANEL, FADING TOWARD THE TEXT.

   This is a different job from the index hero's group. There, the bands sit
   past the end of the copy in the empty photo space and never cross it. Here
   they are the panel's texture - the ground has no photograph and nothing
   else in it - so they run the full width and the headline sits over them.

   TEN BANDS AT THE GROUP'S OWN SPACING, not three spread thin. Three across a
   full-width panel put a third of the width between them, which reads as
   three lone stripes rather than as a field; the spacing here is the 10% the
   group carried when it was clustered at one edge, repeated the whole way
   across. Band width is clamp(96px, 7.5vw, 144px) from the token file, so at
   a 10% pitch the band is most of its own slot and the result is a pattern
   with a narrow gap, which is what was asked for.

   The SKEW is never mirrored: the Sdeira mark has one physical slant and
   neither language nor layout flips it (token file, section 5). Placement is
   logical (`inset-inline-start` measures from the reading edge), so an LTR
   mirror of this page gets the same composition without touching a number.

   THE FADE IS WHAT KEEPS THE TEXT CLEAR, and it matters more at ten bands
   than it did at three, because now every line of the headline has one behind
   it. A band crossing a headline at one flat strength is a stripe over words;
   a run of them stepping down as they approach the reading edge reads as one
   graduated field, and the quietest are the ones under the first and heaviest
   part of the title. That is why the order runs strong at the far end to
   faint at the start, and not the other way round.

   The steps are derived, not invented. Each band carries --band-step, its
   share of the watermark pair the shared .subpage-hero-glass rule already
   sets, and the one tone rule below multiplies both tokens by it. Retuning
   the watermark tone in the token file moves all ten together and keeps every
   ratio. The ramp is linear from .3 at the reading edge to 1 at the far one.

   NONE OF THEM IS ZERO, and the floor is deliberate: at .3 the line still
   resolves to about a 13% Sweihan mix over Black, a visible warm edge rather
   than a band that fades out of existence and leaves the panel looking
   unfinished on one side.

   STILL A JUDGEMENT CALL THAT NEEDS SIGN-OFF. The token file binds a tone to
   each of three surfaces - flat light, photograph, ambient-behind-content -
   and a flat DARK surface with text on it is a fourth situation none of them
   was measured for. Ambient is the nearest by role, but its numbers were set
   on the two warm light surfaces and over Black resolve to roughly a four
   value step, which is not a band anyone would see. A dark-surface tone from
   the identity owner replaces all of this.

   The bands are unclassed <span>s and are positioned by :nth-child, because
   ten hand-named classes across four pages is four places to forget one. Each
   selector sets the position and the step; the single rule after them turns
   the step into the two colours. */
.subpage-hero--article .subpage-hero-glass span:nth-child(1)  { inset-inline-start: 6%;  --band-step: .30; }
.subpage-hero--article .subpage-hero-glass span:nth-child(2)  { inset-inline-start: 16%; --band-step: .38; }
.subpage-hero--article .subpage-hero-glass span:nth-child(3)  { inset-inline-start: 26%; --band-step: .46; }
.subpage-hero--article .subpage-hero-glass span:nth-child(4)  { inset-inline-start: 36%; --band-step: .53; }
.subpage-hero--article .subpage-hero-glass span:nth-child(5)  { inset-inline-start: 46%; --band-step: .61; }
.subpage-hero--article .subpage-hero-glass span:nth-child(6)  { inset-inline-start: 56%; --band-step: .69; }
.subpage-hero--article .subpage-hero-glass span:nth-child(7)  { inset-inline-start: 66%; --band-step: .77; }
.subpage-hero--article .subpage-hero-glass span:nth-child(8)  { inset-inline-start: 76%; --band-step: .84; }
.subpage-hero--article .subpage-hero-glass span:nth-child(9)  { inset-inline-start: 86%; --band-step: .92; }
.subpage-hero--article .subpage-hero-glass span:nth-child(10) { inset-inline-start: 96%; --band-step: 1; }

.subpage-hero--article .subpage-hero-glass span {
  border-color: color-mix(in srgb,
    var(--brand-sweihan) calc(var(--identity-band-line-on-photo) * var(--band-step)), transparent);
  background: color-mix(in srgb,
    var(--brand-sweihan) calc(var(--identity-band-fill-on-photo) * var(--band-step)), transparent);
}

/* ==========================================================================
   ARTICLE META BAR
   Lives inside .article-layout's own .wrap now, as the first row above the
   article shell, not as its own full-bleed strip above it. Two separate
   bands with two different edge widths was the actual complaint: this
   element's job (category, date, share control) is part of the article's
   content column, not a second layout of its own. Category and date still
   reuse .news-item-meta rather than a new badge style, because that is
   already the "date and category on a white surface" component the news
   cards use - this is the same fact in the same visual language, not a
   second implementation of it. */
/* A full-bleed white band directly under the hero, which is the same move the
   news index makes with its own band (.breadcrumb, section 10 below): white
   edge to edge, content held to the page .wrap. It was a boxed panel inside
   the article column for one session, which read as a card floating in the
   warm ground rather than as the page's own band, and it did not line up with
   the band the visitor had just seen on the index.

   No radius and no rule. The band runs to both edges so there is no corner to
   round, and separation from the warm ground below it is the tonal step
   itself - a one-sided border is banned as a visual treatment (AGENTS.md). */
.article-meta-bar {
  background: var(--surface-raised);
}

.article-meta-bar > .wrap {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-block: 14px;
}

/* ==========================================================================
   10. BREADCRUMB
   ========================================================================== */

.breadcrumb {
  background: var(--surface-raised);
  border-block-end: 1px solid var(--line);
}

/* The news index hero has no per-article breadcrumb trail worth repeating
   below it, so this bar carries the section kicker instead - same surface,
   same vertical rhythm as the breadcrumb ol it replaces. */
.breadcrumb--kicker-bar .kicker {
  margin-bottom: 0;
  padding-block: 14px;
}

.breadcrumb ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 14px 0;
  list-style: none;
  font-size: var(--type-meta);
  color: var(--label);
}

.breadcrumb li {
  display: flex;
  align-items: center;
  gap: 8px;
}

.breadcrumb li + li::before {
  content: "/";
  color: var(--line);
}

.breadcrumb a {
  color: var(--muted);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}

.breadcrumb-home {
  display: inline-flex;
  align-items: center;
  line-height: 1;
}

.breadcrumb a:is(:hover, :focus-visible) {
  color: var(--link-hover);
}

.breadcrumb [aria-current="page"] {
  color: var(--ink);
}

/* Every rule below must come after the plain .breadcrumb rules above: on a
   .breadcrumb.breadcrumb--hero element (the breadcrumb that opens an article
   or the news index hero, sitting directly on the photo instead of on its
   own white strip) each of these matches its plain-.breadcrumb counterpart
   at equal specificity, so source order alone decides the winner. Kept this
   whole variant here, together, instead of interleaved with the rules above,
   so nothing here can silently lose that fight again. Only the colours (and
   the background/border below) change - same markup, same on-dark token
   pairing throughout. */
.breadcrumb--hero {
  background: none;
  border-block-end: 0;
  margin-block-end: 10px;
}

.breadcrumb--hero ol {
  padding: 0;
  color: var(--muted-on-dark);
}

.breadcrumb--hero li + li::before {
  color: var(--line-on-dark);
}

.breadcrumb--hero a {
  color: var(--muted-on-dark);
}

.breadcrumb--hero a:is(:hover, :focus-visible) {
  color: var(--link-hover-on-dark);
}

.breadcrumb--hero [aria-current="page"] {
  color: var(--brand-white);
}

/* Article heroes are the shared photo treatment, so the breadcrumb uses the
   generic .breadcrumb--hero colours/hover that every other photo hero uses
   (the deep-Sweihan-only white/underline overrides below belonged to the
   retired flat article ground). */

/* ==========================================================================
   11. NEWS INDEX
   Cards keep straight rectangular edges. The diagonal lives on the media of
   the feature card only, so the row never reads as a set of clipped boxes.
   ========================================================================== */

.news-index {
  background: var(--surface-base);
}

/* Toolbar: two groups, not one undifferentiated row. The category tabs are one
   group; search plus the two filters are the other, wrapped together as
   .news-toolbar-filters so they move as a unit. justify-content: space-between
   is what puts visible air between the two groups on a wide screen without any
   control stretching to manufacture that gap - neither group grows, so the
   space left over after both take their natural width becomes the gap between
   them rather than being absorbed by the search field.
   Every control here filters real published items. The year list is built from
   the dates actually present in the DOM, so it can never offer a year with
   nothing behind it. */
.news-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 14px 10px;
  margin-block-end: clamp(26px, 3vw, 40px);
}

/* Shared control metrics. One height for the tab strip, the search field and
   both dropdowns, so the row reads as a single object.
   54px, not the 46px this used to be, and the number is derived rather than
   picked: the homepage's filter pill is 10px of block padding either side of
   one --type-action line box (about 46.6px), and its strip adds 4px of
   padding either side of that. Matching the pill is the point - see the note
   on .news-tab below - so the row it sits in has to be as tall as the strip
   around it, or the tabs and the search field stop lining up. */
.news-toolbar {
  --control-h: 54px;
}

/* Tab strip. Sized to its own buttons always, never stretched into a wide
   empty pill: flex-grow stays 0 in every layout below, on its own row or
   sharing one. On a phone it also keeps its single line scrolling sideways
   rather than wrapping to a second row, so the row height never changes as
   tabs are added. */
.news-tabs {
  display: flex;
  flex: 0 0 auto;
  flex-wrap: nowrap;
  align-items: center;
  gap: 4px;
  min-inline-size: 0;
  max-inline-size: 100%;
  block-size: var(--control-h);
  padding: 4px;
  background: var(--surface-raised);
  border-radius: var(--r);
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  scrollbar-width: none;
}

/* The search-plus-filters group. It does not grow to fill whatever the toolbar
   has left over - that would recreate the same oversized empty field this
   replaces - it sizes to its own three controls and space-between carries the
   rest. flex-wrap: nowrap is what keeps search and both dropdowns on one line
   together instead of each wrapping to its own row; .news-search is the one
   permitted to shrink inside it. */
.news-toolbar-filters {
  display: flex;
  flex: 0 1 auto;
  flex-wrap: nowrap;
  align-items: center;
  gap: 10px;
  min-inline-size: 0;
}

.news-tabs::-webkit-scrollbar {
  display: none;
}

/* THE SAME PILL AS THE HOMEPAGE'S .project-filter / .news-filter, value for
   value: --type-action at weight 700, Jais at rest on --surface-raised, 22px
   of inline padding, one radius token, and the accent fill on the active one.
   It is one component with two definitions only because the homepage's
   cascade and the sub-page cascade never meet (see "The sub-page system" in
   CLAUDE.md); that makes this parity a property to preserve, not a
   coincidence - change a value here and the homepage rule has to move with
   it, and the way to check is to measure both pages.

   THE EFFECT is what actually diverged and what the complaint was about.
   This tab used to fill with --surface-sunken on hover, so a hovered tab and
   the selected tab were two filled pills with different colours and the row
   read as though two things were chosen. The homepage answers hover with a
   colour step alone - Jais to Black, the surface unchanged - which is the
   approved interaction vocabulary and leaves the accent fill meaning exactly
   one thing: this is the selected tab.

   TWO DELIBERATE DIFFERENCES, both structural rather than visual:
   - The strip scrolls sideways instead of wrapping (see .news-tabs above),
     because this row has to hold its height on a phone. The pill inside it is
     unchanged.
   - The focus ring is inset. The strip is an overflow-x: auto scroller, and
     an outward ring on the first or last tab is clipped by it; the homepage
     strip has no scroller and can afford the outward offset. */
.news-tab {
  flex: none;
  border: 0;
  border-radius: var(--r);
  padding-inline: 22px;
  block-size: 100%;
  font: inherit;
  font-size: var(--type-action);
  font-weight: 700;
  color: var(--brand-jais);
  background: transparent;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.news-tab:hover {
  color: var(--brand-black);
}

.news-tab[aria-selected="true"] {
  color: var(--brand-white);
  background: var(--accent-fill);
}

.news-tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* flex-shrink is what it needs from the parent group, not flex-grow: the field
   has a natural width and stops there, so the empty space in it is gone. It is
   the only child of .news-toolbar-filters allowed to shrink below that width,
   because a dropdown truncating its own label is worse than the search field
   losing a few characters of placeholder. */
.news-search {
  position: relative;
  flex: 0 1 280px;
  min-inline-size: 120px;
  display: flex;
  align-items: center;
}

/* DIRECT child only. Unscoped, this also matched the <i class="ph ph-x"> inside
   .news-search-clear, and that is what threw the clear glyph out of its
   button: the button is itself positioned, so the icon absolutely positioned
   against IT, took inset-inline-start: 15px inside a 28px box, and had no
   block-axis offset at all - it could not land centred at any size. It also
   inherited pointer-events: none, so the visible glyph was not part of the
   button's hit area either. The magnifier is the only icon that belongs to
   the field itself; the one in the button belongs to the button. */
.news-search > i.ph {
  position: absolute;
  inset-block: 0;
  block-size: 1em;
  margin-block: auto;
  inset-inline-start: 15px;
  font-size: var(--icon-control);
  line-height: 1;
  color: var(--icon);
  pointer-events: none;
}

.news-search input {
  inline-size: 100%;
  block-size: var(--control-h);
  font: inherit;
  font-size: var(--type-action);
  color: var(--ink);
  background: var(--surface-raised);
  border: 0;
  border-radius: var(--r);
  /* Room for the search glyph on one side and the clear button on the other,
     regardless of which is actually showing at a given moment: the clear
     button toggles with [hidden], and reserving its space either way stops
     the text from shifting when it appears. */
  padding-inline: 44px 40px;
  transition: background var(--dur-fast) var(--ease-out);
}

.news-search input::placeholder {
  color: var(--label);
}

.news-search input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Chrome and Safari draw their own clear button inside a type="search" field.
   It is another piece of browser chrome the token file has no say over, so it
   is removed and replaced with the button below, styled like everything
   else on the page. */
.news-search input::-webkit-search-cancel-button {
  appearance: none;
}

/* Centred on the block axis by the offset/auto-margin pair, not left to the
   static position. An absolutely positioned element with no block-axis offset
   falls back to where it WOULD have sat in flow, and in a flex container that
   fallback is the container's content-box start edge - so the button sat high
   in the field and moved again whenever --control-h changed. inset-block: 0
   plus margin-block: auto against a fixed block-size centres it exactly, at
   any field height, with no transform to interact with anything else. */
.news-search-clear {
  position: absolute;
  inset-inline-end: 8px;
  inset-block: 0;
  margin-block: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 28px;
  block-size: 28px;
  border: 0;
  border-radius: var(--r);
  color: var(--icon);
  background: transparent;
  font-size: var(--icon-control);
  transition: background var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

/* The class rule above sets display unconditionally, and an author style
   beats the user-agent's [hidden] { display: none } regardless of
   specificity - so without this, the button ignored its own hidden
   attribute and stayed visible even on an empty field. */
.news-search-clear[hidden] {
  display: none;
}

/* The glyph is a font character: line-height: 1 and a block box keep the
   drawn mark on the button's own centre instead of on the icon font's
   default line box, which sits the X a couple of pixels high. */
.news-search-clear i.ph {
  display: block;
  font-size: inherit;
  line-height: 1;
}

.news-search-clear:is(:hover, :focus-visible) {
  color: var(--accent-text);
  background: var(--icon-wash);
}

/* ---- Dropdown -------------------------------------------------------------
   Button plus listbox, not <select>. A native select renders its list with the
   operating system widget, which ignores the token file entirely: its own
   font, its own blue highlight, its own radius, none of it reachable from CSS.
   This is the same control expressed in the design system. */
.news-dropdown {
  position: relative;
  flex: 0 1 auto;
}

.news-dropdown-toggle {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  inline-size: 100%;
  block-size: var(--control-h);
  border: 0;
  border-radius: var(--r);
  padding-inline: 16px;
  font: inherit;
  font-size: var(--type-action);
  color: var(--ink);
  background: var(--surface-sunken);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out);
}

/* Open takes the same tone as hover, and the same tone as the menu below it.
   Without this the open control is a white button under a warm list, which
   reads as two unrelated panels once the hairline between them is gone. */
.news-dropdown-toggle:hover,
.news-dropdown-toggle[aria-expanded="true"] {
  background: var(--surface-sunken);
}

.news-dropdown-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.news-dropdown-value {
  flex: 1;
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: start;
  overflow: hidden;
  text-overflow: ellipsis;
}

.news-dropdown-toggle i.ph {
  flex: none;
  font-size: var(--icon-control);
  line-height: 1;
  color: var(--icon);
  transition: transform var(--dur-base) var(--ease-out);
}

.news-dropdown-toggle[aria-expanded="true"] i.ph {
  transform: rotate(180deg);
}

/* THE STEP ALONE DOES NOT SAY WHERE A FLOATING LAYER ENDS, MEASURED. This used
   to read "the tonal step does that instead" and claim --surface-sunken reads
   against both grounds the menu can open over. It does not: sunken (#F6E9D6)
   against the page (--surface-base, #FAF4EC) is a luminance-ratio of 1.10, and
   against a news card (--surface-raised, #FFFFFF) it is 1.20 - both close
   enough to read as the same surface with no edge at all. No shadow is still
   the rule; a shadow was never the fix a step this close to its ground needed.

   What actually draws the edge is the hairline in --line, the token this
   system already uses for every other divider on the page. One pixel, all the
   way round, because this is a floating layer ending against an unknown
   ground beneath it - a photograph in the header case, a warm page here - not
   a border painted ON a static component. --line reads against the page, the
   card, and the fill inside it, at any of the grounds the menu can land on.
   The fill still does its job of matching the open toggle below it and giving
   the option rows something to step off of; the line is what makes that fill
   register as a panel instead of a stain. */
.news-dropdown-menu {
  position: absolute;
  z-index: 20;
  inset-inline-start: 0;
  inset-block-start: calc(100% + 6px);
  /* WIDTH FOLLOWS THE MENU'S OWN CONTENT, NOT THE SHRUNK TOGGLE. At a narrow
     viewport the toggle can fall to its 74px floor (see the responsive block
     below) while an option label like "Ù…Ù† Ø§Ù„Ø£Ø­Ø¯Ø« Ø¥Ù„Ù‰ Ø§Ù„Ø£Ù‚Ø¯Ù…" does not - the
     options keep white-space: nowrap by design, so a menu pinned to the
     toggle's width forced the browser into a same-axis scrollbar inside an
     open list, which is a worse failure than the ellipsis the closed toggle
     already accepts. max-content lets the menu take what its longest option
     needs; the clamp keeps that same menu off the edge of a phone screen,
     matching the .wrap gutter of 32px on each side. */
  inline-size: max-content;
  min-inline-size: 100%;
  max-inline-size: min(280px, calc(100vw - 64px));
  margin: 0;
  padding: 4px;
  list-style: none;
  /* RAISED, NOT SUNKEN, 1 Sep 2026: the toggle it drops from rests on
     --surface-sunken and .jobs-list's own cards sit on --surface-sunken too,
     so a sunken menu would have no edge left at all, hairline included. Raised
     is the one tone this menu can take that reads against every ground it
     might now open over. */
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: var(--r);
  /* A White panel with only a 1px --line edge reads as floating on nothing
     over a Light Grey card; --shadow-floating is what tells the eye this panel
     sits above the grid rather than being part of it. */
  box-shadow: var(--shadow-floating);
  max-block-size: 280px;
  overflow-y: auto;
}

.news-dropdown-menu[hidden] {
  display: none;
}

.news-dropdown-option {
  border-radius: var(--r);
  padding: 10px 14px;
  font-size: var(--type-action);
  color: var(--brand-jais);
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

/* Steps to sunken, not sunken-strong: the menu itself is raised (White) now,
   so the highlight steps DOWN to sunken the way every other raised-surface
   hover on the site does, rather than to sunken-strong (which in this theme
   resolves to White - the same tone as the menu, no highlight at all). */
.news-dropdown-option:hover,
.news-dropdown-option:focus-visible {
  color: var(--brand-black);
  background: var(--surface-sunken);
  outline: 0;
}

.news-dropdown-option[aria-selected="true"] {
  color: var(--brand-white);
  background: var(--accent-fill);
}

.news-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr));
  gap: clamp(18px, 2vw, 26px);
}

/* NO BOX, FIXED HEIGHT NOT A BOUNDING SQUARE - ported from the reference
   sdeira-subpage.css: the Light Grey tile is dropped entirely (no background
   once the logo sits directly on the card) and the logo is sized by its
   slot's HEIGHT, never a bounding box, so every mark reads at the same
   visual scale regardless of its own canvas shape. */
.job-card-logo { display: grid; place-items: start; block-size: 48px; margin-block-end: 18px; }
.job-card-logo img { inline-size: auto; block-size: 48px; max-inline-size: 100%; object-fit: contain; transition: filter var(--dur-slow) var(--ease-out); }

/* THE LOGO TURNS WHITE UNDER THE HOVER WIPE (never gains a background tile):
   the card hover fills the surface with the Liwa wipe and every line of copy
   already turns white to read against it - the logo follows, via the same
   brightness(0) invert(1) filter the rest of the site uses for a dark mark on
   a dark ground. */
.project-card-base:is(:hover, :focus-within) .job-card-logo img {
  filter: brightness(0) invert(1);
}

/* .jobs-search - written fresh, 15 Aug 2026, not reused from .news-search.
   .news-search (above, in the NEWS INDEX block) turned out to be dead CSS:
   grep across scripts/components.py shows nothing ever emits that class, so
   there was no working component to reuse in the first place - only leftover
   rules for a news-toolbar search that was never built. Borrowing its values
   anyway (44/40px input padding, 15px icon offset, 120px floor) because the
   reasoning behind them is sound, but as a real first implementation: also
   fixes the actual bug the site owner saw - the field was previously
   class="search-field jobs-search", and .search-field input's own
   `padding: 8px 0` (sdeira-search.css, loaded after this file) was winning
   the cascade at equal specificity over .news-search input's padding, so the
   icon sat directly on top of the placeholder text with no reserved room. */
.jobs-search {
  position: relative;
  flex: 0 1 280px;
  min-inline-size: 120px;
  display: flex;
  align-items: center;
}

.jobs-search > i.ph {
  position: absolute;
  inset-block: 0;
  block-size: 1em;
  margin-block: auto;
  inset-inline-start: 15px;
  line-height: 1;
  color: var(--icon);
  pointer-events: none;
}

.jobs-search input {
  inline-size: 100%;
  block-size: var(--control-h);
  font: inherit;
  font-size: var(--type-action);
  color: var(--ink);
  /* Light Grey, not White: fields take the sunken surface on a white section
     per the site's field convention (Light Gray at rest, never White), the
     same role the dropdown toggle beside it now also takes. */
  background: var(--surface-sunken);
  border: 0;
  border-radius: var(--r);
  padding-inline: 44px 16px;
  transition: background var(--dur-fast) var(--ease-out);
}

.jobs-search input::placeholder {
  color: var(--label);
}

.jobs-search input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.jobs-search input::-webkit-search-cancel-button {
  appearance: none;
}
.jobs-list .job-card[hidden] { display: none; }
.jobs-empty { margin-block-start: 26px; }

/* JOB TOOLBAR / FILTER LABELS - ported from the reference sdeira-subpage.css.
   Each filter dropdown carries a label above its toggle; the toolbar rows
   align on the controls' bottom edge (the base .news-toolbar centres). */
.jobs-filter-label {
  display: block;
  margin-block-end: 6px;
  color: var(--label);
  font-size: var(--type-meta);
  font-weight: 700;
}

.jobs-toolbar {
  align-items: flex-end;
}

/* One width for the three filter toggles so Category / Location / Employment
   type read as equal pills; long values truncate instead of stretching. */
.jobs-filter {
  inline-size: 180px;
}

.jobs-filter .news-dropdown-value {
  white-space: nowrap;
}

/* Jobs pagination (client-side, sdeira-jobs.js). Rendered only when the
   filtered job set spans more than one page; prev/next disable at the
   boundaries and the current page carries aria-current="page". */
.jobs-pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-block-start: 28px;
}

.jobs-pagination button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--control-h);
  block-size: var(--control-h);
  padding-inline: 8px;
  border: 0;
  border-radius: var(--r);
  background: var(--surface-raised);
  color: var(--ink);
  font: inherit;
  font-size: var(--type-action);
  cursor: pointer;
  transition:
    background var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.jobs-pagination button:is(:hover, :focus-visible) {
  background: var(--surface-sunken);
}

.jobs-pagination button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.jobs-pagination button[aria-current="page"] {
  background: var(--accent-strong);
  color: var(--brand-white);
}

.jobs-pagination button:disabled {
  opacity: .4;
  cursor: default;
}

/* Aryam's known-but-not-yet-published communities follow the project archive
   rhythm: two cards per row on larger screens, then the shared mobile collapse. */
.aryam-pending-projects {
  --grid-cols: 2;
}

.news-item {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface-raised);
  border-radius: var(--r);
  overflow: hidden;
}

/* Stretched link: the whole card is clickable, not just the media, the title
   and "read more" - the three separate <a> tags a card carries, all pointing
   at the same story. Hosted on the title link because it is the one real
   link with no overflow:hidden between it and .news-item: the media link
   sits inside .news-item-media, which clips at ~200px tall, so a pseudo aimed
   at the full card would be cut down to the photo's own box if it lived
   there instead. inset: 0 matches .news-item's own padding box exactly, so
   .news-item's own overflow: hidden never clips it either.

   No z-index games needed for the other two links: the state changes on
   hover/focus (the image zoom, the action underline) are keyed off
   .news-item:is(:hover, :focus-within), which is true the moment the pointer
   is anywhere over this pseudo too, since it is still a descendant of the
   card. Keyboard order and each link's own href are untouched - this only
   widens where a mouse click on the title's link is allowed to land. */
.news-item h3 a {
  position: static;
}

.news-item h3 a::after {
  content: "";
  position: absolute;
  inset: 0;
}

.news-item.is-hidden {
  display: none;
}

/* The card does NOT change tone on hover. It matches the homepage news card,
   where hovering activates the card's single action rather than repainting the
   surface: the photograph steps in, the action tints to the link colour and
   its underline grows to full width. A tonal step here read as a colour
   change on a card that is already the raised surface. */

/* 16:9, fixed, matching the ratio already used for a media field everywhere
   else on the site (.needs-prototype-row-media and its siblings). Was a
   height clamp independent of the card's own width, so the two-per-row rule
   below changed the card's width without changing this, and the effective
   crop ratio drifted at every width instead of holding still: at a 1920px
   viewport the card measured 595px wide against a height still capped at
   230px, a 2.59:1 crop where 16:9 is 1.78:1. aspect-ratio ties the two
   together so the crop the visitor sees is the same shape everywhere. */
.news-item-media {
  position: relative;
  display: block;
  aspect-ratio: var(--media-wide);
  block-size: auto;
  /* Without this, every regular card in a two-per-row row sat at a different
     height: .news-item is a column flex container, and a flex item's
     automatic minimum size falls back to its CONTENT size (here, the <img>'s
     own intrinsic ratio from its width/height attributes) rather than the
     size aspect-ratio alone would produce, unless overflow is anything but
     visible. Confirmed by measuring three cards at the same 546px width: 368,
     416 and 376px tall, tracking each photo's own intrinsic ratio instead of
     the shared 16:9 this rule declares. overflow: hidden forces the
     aspect-ratio value to win, and the object-fit: cover crop below never
     needed the clip anyway since it already fills 100%/100%. */
  overflow: hidden;
  background: var(--surface-sunken);
}

.news-item-media img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  filter: saturate(.94) contrast(1.04);
  transition: transform var(--dur-slow) var(--ease-out),
    filter var(--dur-base) var(--ease-out);
}

.news-item:is(:hover, :focus-within) .news-item-media img {
  transform: scale(var(--zoom));
  filter: saturate(1) contrast(1.03) brightness(1);
}

.news-item-body {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 22px 24px 26px;
  flex: 1;
  background: var(--surface-raised);
}

.news-item-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  font-size: var(--type-meta);
}

/* Both parts of the meta row are badges and take the badge role from the token
   file: --badge-text and --badge-weight, the same accent and the same 700 the
   homepage cards use. The category used to sit in --label grey with a gold
   diamond, which read as two different kinds of label in one row. */
.news-item-meta .date,
.news-item-meta .tag,
.news-item-meta .partner {
  color: var(--badge-text);
  /* Overrides the row's own --type-meta above. The badge role owns its size
     now (--badge-size, which resolves to --type-action) so a card's category
     and date match every other badge on the site instead of each context
     picking a size. */
  font-size: var(--badge-size);
  font-weight: var(--badge-weight);
}

.news-item-meta .date {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

/* Al Khatim, not the Sweihan accent: a deliberately different brand colour so
   the separator reads as its own mark rather than a third instance of the
   badge orange next to it on both sides. Decorative content only (no unique
   information; the date and category text already carry that), so it is
   exempt from the token file's "never as text" rule for Al Khatim, which
   governs legible text on a light surface. */
/* .news-item-meta .tag::before,
.news-item-meta .partner::before {
  content: "\25C8";
  margin-inline-end: 6px;
  color: var(--brand-al-khatim);
} */

.news-item-meta .date::before {
  content: "◷";
  color: currentColor;
  font-size: var(--icon-inline);
}

.news-item h3 {
  margin: 0;
  font-size: var(--type-label);
  line-height: var(--leading-label);
  /* RESERVE, DO NOT CLAMP. The token file forbids line-clamp here: truncating
     hides real copy from the visitor. Three lines of room, and a longer title
     is allowed to run on.
     The count is a variable because how much room to reserve depends on what
     the titles are, not on the component: three lines is right for news
     headlines and leaves a visible hole under a one-word sector name. The
     caller sets --card-title-lines on the list; nothing changes for a caller
     that does not, so every existing page keeps the 3 it had. */
  min-block-size: calc(var(--card-title-lines, 3) * 1lh);
}

.news-item h3 a {
  color: inherit;
  text-decoration: none;

  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  text-overflow: ellipsis;
}

.news-item p {
  margin: 0;
  color: var(--muted);
  /* Falls back to 0 (no reservation) for a caller that never sets
     --card-sub-lines, same as --card-title-lines above falls back to 3 -
     an unset custom property inside calc() is invalid and drops the whole
     declaration, which is what silently disabled this for every page until
     card_grid() started emitting the variable. */
  min-block-size: calc(var(--card-sub-lines, 0) * 1lh);
   display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* The action is always visible. .action-link is authored for use over
   photography and carries color: var(--brand-white), which on a raised-surface
   card rendered white on white: present in the DOM, invisible on screen. On a
   light card it takes the same colour the homepage news card gives it. */
.news-item .action-link {
  margin-block-start: auto;
  padding-block-start: 6px;
  align-self: flex-start;
  color: var(--accent-text);
  font-weight: 700;

  /* The homepage's news card gives its action link a 44px touch target and
     this one did not, which left the same shared component landing at 26px on
     a phone on the news pages only. The base .action-link zeroes min-height
     because it is authored for use over photography, where the link is one of
     several stacked overlay controls; inside a card it is the card's only
     action and gets the full target back. */
  min-height: 44px;
}

/* Underline at rest, grown on hover. Same vocabulary as the homepage: the
   action reads as live before the pointer arrives, and completing the line is
   what the hover state says. */
.news-item .action-link::after {
  background: currentColor;
  transform: scaleX(.42);
  transform-origin: right center;
}

.news-item:is(:hover, :focus-within) .action-link {
  color: var(--link-hover);
}

.news-item:is(:hover, :focus-within) .action-link::after {
  transform: scaleX(1);
}

/* F2, the audit finding: a card with no destination renders no .action-link
   at all (see _cards() in components.py, gated on `destination`), which
   left it exactly the action-link's reserved space (44px min-height + 6px
   padding + the body's 10px gap) shorter than every linked card beside it
   in the same grid - first measured on business/investments#projects-in-
   progress, where "Metal Park memorandum" carries no page of its own and
   sits next to two cards that do. Forcing a link to a page that does not
   exist would violate the no-fabricated-destination rule; reserving the
   space instead keeps every card in a grid the same height regardless of
   which ones are clickable, the same principle --card-sub-lines already
   applies to body-copy length. */
.news-item-body:not(:has(.action-link))::after {
  content: "";
  display: block;
  margin-block-start: auto;
  padding-block-start: 6px;
  block-size: 44px;
}

.news-item .action-link i.ph { transform: none; }

.news-item:is(:hover, :focus-within) .action-link i.ph {
  transform: translateX(4px);
}

[dir="rtl"] .news-item:is(:hover, :focus-within) .action-link i.ph {
  transform: translateX(-4px);
}

/* A project without its own page is an information card, not a muted link.
   It keeps the project card's geometry and media treatment, while the shared
   pending marker says why no action is offered. Do not let pointer hover make
   a non-destination look tappable. */
.news-item--pending:is(:hover, :focus-within) {
  background: var(--surface-raised);
}

.news-item--pending:is(:hover, :focus-within) .news-item-body::after {
  --wipe-p: 0%;
  --wipe-op: 72%;
}

.news-item--pending:is(:hover, :focus-within) .news-item-body > * {
  color: var(--ink);
}

.news-item--pending .is-pending { color: var(--pending); }

/* The newest story runs as a horizontal feature. This is the one place the
   diagonal cut appears in the list, and it falls on the photograph, never on
   the card, so the card keeps its straight edges. */
.news-item--feature {
  grid-column: 1 / -1;
  flex-direction: row;
  align-items: stretch;
}

/* No diagonal cut here. It was tried on the edge facing the text and read as
   an effect applied to the card rather than as the identity mark: the card is
   a rectangle of content, and slicing its photograph made the row look
   treated. The slant stays where it belongs on this page, in the hero strips
   and the quote mark. */
.news-item--feature .news-item-media {
  flex: 0 0 54%;
  block-size: auto;
  min-block-size: clamp(280px, 30vw, 360px);
}

.news-item--feature .news-item-body {
  flex: 1;
  justify-content: center;
  padding: clamp(28px, 3vw, 44px) clamp(26px, 3vw, 40px);
}

/* RESERVE, DO NOT CLAMP - same rule as the base .news-item h3 above, not an
   exception to it. This was min-block-size: 0, the one title on the page
   with no reserved room at all: every other title and every description
   (including this card's own p, two lines below) holds its space so a
   shorter or longer edit never reflows its siblings or jumps the card's
   height between edits. Three lines, matching the base card's own
   --card-title-lines default - this column is close enough in width to a
   regular two-per-row card's that the same line count is the right match
   even at the feature's larger type-card size. Today's headline already
   wraps to exactly three lines at this width, so this reservation changes
   nothing on screen; it exists for the next edit, not this one. */
.news-item--feature h3 {
  font-size: var(--type-card);
  line-height: var(--leading-card);
  min-block-size: calc(var(--card-title-lines, 3) * 1lh);
}

/* Reserved to two lines like every other card's description, not zero: a
   single short line here looked unfinished next to the feature card's extra
   width. The copy is written to actually reach two lines, not padded with a
   blank one. */
.news-item--feature p {
  font-size: var(--type-lead);
  line-height: var(--leading-lead);
  min-block-size: calc(var(--card-sub-lines, 2) * 1lh);
}

/* ==========================================================================
   12. ARTICLE
   ========================================================================== */

/* ONE SPACING SCALE FOR THE WHOLE ARTICLE.
   Every gap and every inner padding under this section used to be its own
   hand-picked number: 10px between the in-body figures, 14px between the aside
   ones, 26px between aside blocks, 32-64px between the columns, and three
   different card paddings (22/24/26 fixed, and a 22-30/22-28/24-32 clamp). None
   of them were wrong on their own and none of them were related to each other,
   which is what made the page read as assembled rather than composed. The four
   values below are the whole scale; nothing under .article-layout writes a
   spacing number that is not one of them.

   They are declared here rather than in the token file on purpose. The token
   file owns values the whole SITE shares; these are this section's proportions,
   and promoting them would imply every other section must adopt them too. */
.article-layout {
  background: var(--surface-base);
  /* Inside a small card (a figure), inside the reading card, between stacked
     blocks, and between the two columns. Each step is roughly 1.4x the one
     before it, so nesting reads as nesting. */
  /* The news card's own padding, not a larger one of this section's invention.
     .news-item-body has run 22/24/26 since it was built, and a figure card sits
     next to news cards on the related rail at the bottom of the same page - a
     figure card padded to 32 read as a different, roomier component. The top of
     the clamp is the news card's 24; the bottom lets it tighten on a phone,
     which the fixed value cannot. */
  --article-card-pad: clamp(18px, 1.5vw, 24px);
  --article-body-pad: clamp(24px, 3vw, 44px);
  --article-stack-gap: clamp(14px, 1.4vw, 20px);
  --article-shell-gap: clamp(24px, 2.6vw, 40px);
  /* The reading card's ceiling. It only binds where the track is wider than a
     comfortable measure - the full-width article, and every article once the
     shell collapses to one column. In the two-column layout the 7fr track is
     already narrower than this, so the card simply fills it. */
  --article-measure: 900px;
}

/* 70/30 rather than a fixed 280px aside. A fixed sidebar column left a large
   empty gap beside the text at wide viewports, because .article-body also
   capped itself at 68ch (about 640-680px) well short of the space the old
   minmax(0, 1fr) column actually had: at 1280px wrap width that column ran to
   roughly 850px, so 170-200px sat empty between the reading column and the
   aside. The fr/fr ratio below tracks the wrap's own width instead of a fixed
   number, and .article-body's cap is raised to use most of its share of it. */
.article-shell {
  /* display: grid; */
  grid-template-columns: minmax(0, 7fr) minmax(240px, 3fr);
  gap: var(--article-shell-gap);
  align-items: start;
}

/* THE READING COLUMN IS A CARD. Requested by the site owner: the figures beside
   it were already white cards on the warm ground, so the body - the one thing on
   the page the visitor actually came to read - was the only block left sitting
   directly on the section tone. It read as background rather than as content.

   White on --surface-base is the tonal step the system already uses everywhere
   else for exactly this (section 10 of the token file: a raised surface on a
   base one). No border, no shadow - there is no third mechanism in this system.

   The max-inline-size is the measure, and it lives on the CARD rather than on
   .article-body inside it. That is the whole reason this is one rule instead of
   two: a cap on the text would leave the card full width with a band of empty
   white on its end side, which is the void the aside was widened to remove in
   the first place. Capping the card means the text always fills the box it is
   in, at every width, in both shell shapes. */
.article-main {
  max-inline-size: min(100%, var(--article-measure));
  margin-inline: auto;
  padding: var(--article-body-pad);
  background: var(--surface-raised);
  border-radius: var(--r);
}

/* An article with no aside worth keeping (its facts folded into the meta
   bar above, nothing left that reads as a genuine stat or figure) gets the
   full wrap width instead of an empty second track sitting unused beside
   it. */
/* An article with no figures gets one track instead of an empty second one.

   WHY THIS ARTICLE HAS NONE, checked rather than assumed: the CERT story
   contains no numbers at all - not one figure anywhere in its body. There is
   nothing to pull out, and pulling out something that is not in an approved
   source is fabrication, which the content rules forbid outright.

   So the fallback the site owner specified applies: full width, starting at the
   page's own start edge. Both auto margins and the measure cap the two-column
   card carries are switched off here - centring a full-width card is a
   contradiction, and it was centring that put the text in the middle of the
   page instead of at the edge every other block on the page starts from. */
.article-shell--full {
  grid-template-columns: minmax(0, 1fr);
}

.article-shell--full .article-main {
  max-inline-size: 100%;
  margin-inline: 0;
}

/* The one thing NOT taken to the full width is the line itself. The card runs
   edge to edge as asked; the text inside it still stops at 95ch, because a line
   that runs the whole 1136px of a 1280 wrap is about 118 characters and the eye
   loses its place returning to the start of the next one. Start-aligned, so the
   first character still sits exactly where the card, the meta band above it and
   every other section on the page begin - the cap takes its space off the far
   end, never off the start.

   The cap is scoped to the text elements, not the whole card's content, for
   the same reason the card itself runs full width: an in-body figure is not a
   line the eye has to track, and this article has no aside to fill the space
   the cap used to leave beside it (site owner flagged, 15 Aug 2026 - a photo
   with no stats to sit next to it should use the card, not leave a blank
   column beside itself inside it too). */
.article-shell--full .article-body > :not(.article-figure) {
  max-inline-size: min(100%, 95ch);
}

/* The figure is the one child exempt from the cap above, so it already runs
   the card's full content width - up to .article-main's own padding, same as
   every other block in the card. It does NOT bleed through that padding: an
   earlier pass did (negative margin cancelling .article-main's padding so the
   photo touched the white card's raw edges), but the site owner asked for the
   opposite, 15 Aug 2026 - the card's margin exists on purpose, and the photo
   should sit inside it like everything else in the card, not against it. */

/* Running text is --type-body. It used to be --type-lead here, but that only
   ever applied to elements that INHERIT it: `main p` is a real rule and beats
   inheritance regardless of source order, so every paragraph rendered at
   --type-body while every list item rendered at --type-lead. Two reading
   sizes in one column, which is what the visitor reported. The lead
   paragraph, and only it, keeps the larger role. */
/* No measure cap here any more. It moved onto .article-main, which is the card
   the text now sits in - see the note there for why a cap on the text and a cap
   on the card are not interchangeable once there is a visible box. */
.article-body {
  max-inline-size: 100%;
  font-size: var(--type-body);
  line-height: var(--leading-body);
  color: var(--ink);
}

.article-body p {
  font-size: var(--type-body);
  line-height: var(--leading-body);
}

.article-body p.article-lead {
  font-size: var(--type-lead);
  line-height: var(--leading-lead);
}

.article-body > * + * {
  margin-block-start: 1.15em;
}

/* Heading scale inside the article, deliberately one step below the section
   headings outside it. --type-card is the CARD role, up to 2.25rem, and next
   to a 1.2rem body it read as a display heading dropped into running text:
   the same jump the visitor reported as inconsistent sizing. An in-body
   heading is a label-level role, and h3 sits one step under it. --type-action
   is not the next step down: at .95rem it is SMALLER than body text. */
.article-body h2 {
  font-size: var(--type-label);
  line-height: var(--leading-label);
  margin-block-start: 1.7em;
}

.article-body h3 {
  font-size: var(--type-lead);
  line-height: var(--leading-lead);
  font-weight: 700;
  margin-block-start: 1.5em;
}

.article-body ul {
  padding-inline-start: 1.2em;
  margin-inline-start: 0;
}

.article-body li + li {
  margin-block-start: .5em;
}

.article-body li::marker {
  color: var(--accent);
}

.article-body strong {
  color: var(--ink);
  font-weight: 700;
}

/* The pull quote (.article-quote) used to live here, a sunken panel carrying a
   slanted stripe watermark and an attribution footer. Removed on 2026-08-04 at
   the site owner's request: on an article that already runs a hero, a meta bar,
   a photograph and a figure set, a fifth boxed treatment was reading as visual
   complication rather than emphasis. Quoted material now stays in the running
   text. Do not reintroduce it as a per-page rule. */

/* Numbers pulled out of the body. Institutional value figures use the
   documented icon-value roles, not a new colour. */
/* Separation here is a real gap on the section tone, not a hairline drawn by a
   1px grid gap over a line-coloured container. That trick paints the container
   wherever a cell is missing, so a set of four in a three-column row showed an
   empty stripe. Tonal step is also what the system asks for: separation between
   surfaces is tone, never a shadow or a rule. */
/* Two per row, fixed, not auto-fit. Every article's figure set is an even
   count (4, 4, 2, and one article with none), so two columns is always
   complete rows. auto-fit fitted four across at wide viewports, which both
   squeezed each number against its own label and produced a row shape no
   other card on the site uses. */
.article-figures {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--article-stack-gap);
  /* Asymmetric on purpose. The block-start gap follows the sentence that
     introduces the numbers, so it is a short gap; the block-end gap separates
     the set from the next section, so it stays large. The old `margin-block`
     shorthand gave both the large value and left the numbers stranded far
     below their own heading. */
  margin-block-start: var(--article-stack-gap);
  margin-block-end: calc(var(--article-stack-gap) * 1.8);
}

/* Padding, radius and the icon chip are the news card's, so a figure and a
   card read as the same object in two sizes rather than two systems. */
.article-figures div {
  padding: var(--article-card-pad);
  background: var(--surface-sunken);
  border-radius: var(--r);
}

/* A figure set placed IN the body rather than in the aside is now a card inside
   a card, and white on white is not a separation - the system separates by tone
   and there is no other mechanism. It steps down to the sunken rung, which is
   the same relationship the raised card has to the base ground outside it.
   No article uses in-body figures today; this exists so the first one that does
   is correct on arrival rather than invisible. */
.article-main .article-figures div {
  background: var(--surface-sunken);
}

/* Number and icon share one row, number at the reading-start edge (right in
   RTL) since it is the figure that matters, icon pushed to the far end as a
   quiet label rather than a chip competing with it. space-between is what
   pushes the two apart regardless of how wide either one is. */
.article-figures-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

/* The figure is the loudest thing on the page after the H1, so it takes the
   section heading role rather than the card one it used to sit at. 700 is the
   ceiling: only the Regular weight of the brand font is licensed and the other
   @font-face blocks point at it deliberately to block browser-synthesized
   bold, so extra weight here has to come from size, not from a heavier
   number. */
/* line-height: 1, not --leading-section. The role's leading is built for a
   heading that wraps: it reserves half a line above and below every line so
   stacked lines breathe. A figure is always exactly one line and never wraps,
   so all that reservation did here was pad the card from the inside - about
   6px above the digits and 6px below, on top of the card's own padding, in a
   place no padding value could be found to explain it. Deleting it is most of
   why these cards are shorter now; the padding change is the rest. */
.article-figures b {
  font-family: var(--font-display);
  font-size: var(--type-section);
  line-height: 1;
  font-weight: 700;
  color: var(--figure);
}

/* Bare glyph, no chip: a background box competed with the number for
   attention, which is backwards for a mark whose only job is to label it.
   The size is the token scale's top step (--icon-display), not a local clamp:
   the icon sizes on this site come from section 9b of the token file, and a
   hand-written clamp here would have been a sixth scale nobody could find. */
.article-figures i.ph {
  flex-shrink: 0;
  font-size: var(--icon-display);
  color: var(--icon);
  opacity: .7;
}

.article-figures span {
  display: block;
  margin-block-start: 10px;
  font-size: var(--type-meta);
  color: var(--label);
}

/* The "as reported by the source" qualifier (.article-figures-note) used to
   sit under a figure set. Removed on 2026-08-04 at the site owner's request.
   The sourcing requirement it referred to is editorial and is checked when the
   content is written; the machine-readable record stays in each page's
   isBasedOn JSON-LD. It was never load-bearing as visible UI. */

/* Figures moved into the aside (in place of the old plain facts list) get a
   single column instead of the two-across grid the in-body version uses:
   the aside track is a fixed narrow width (240-360px), not the reading
   column, so two-across would squeeze each number against its label again -
   exactly what the two-column grid above was built to avoid in the first
   place. */
/* Now the only thing in the aside, so the cards get the room the fact panel
   used to take: more padding inside each card and a wider gap between them,
   because a single column of four is the whole column, not a block sitting
   under something else. */
.article-figures--aside {
  grid-template-columns: 1fr;
  gap: var(--article-stack-gap);
  margin-block-start: 0;
}

/* The three-value padding this carried (top, sides, a slightly larger bottom)
   was the only asymmetric box in the section and nothing depended on it: the
   number and its label are a single left-aligned stack, so an extra 2px under
   the label was not correcting anything an eye could find. It takes the scale's
   card step like every other card. */
.article-figures--aside > div {
  padding: var(--article-card-pad);
}

/* Same reason the aside drops to one column: the track is 240-360px wide, and
   the section role's 5rem ceiling would push a six-digit number (110,200) past
   it. The role still drives the size, capped at what the narrow track holds. */
.article-figures--aside b {
  font-size: min(var(--type-section), 2.875rem);
}

/* One step down the same icon scale, to stay in proportion with the smaller
   number beside it. */
.article-figures--aside i.ph {
  font-size: var(--icon-feature);
}

/* In-body photograph. The diagonal lives in the media edge, never on the
   container, which is the same rule the news card media follows. */
/* On the scale like everything else here: 1.8x the stack gap, which is enough
   air to read as a break in the column without becoming a gap the eye has to
   cross. The body's own paragraph rhythm (1.15em) is deliberately smaller. */
.article-figure {
  margin-block: calc(var(--article-stack-gap) * 1.8);
}

.article-figure img {
  display: block;
  inline-size: 100%;
  block-size: auto;
  border-radius: var(--r);
}

.article-figure figcaption {
  margin-block-start: 10px;
  font-size: var(--type-meta);
  line-height: var(--leading-body);
  color: var(--label);
}

/* Sidebar */
.article-aside {
  position: sticky;
  top: 104px;
  display: flex;
  flex-direction: column;
  gap: var(--article-stack-gap);
}

/* The "Ù…Ø¹Ù„ÙˆÙ…Ø§Øª Ø§Ù„Ø®Ø¨Ø±" panel (.article-aside section + .article-facts, a
   labelled date / category / partner / location list) was removed on
   2026-08-04 at the site owner's request, and with it the last thing in the
   aside that was not a figure. Every fact it carried is stated somewhere the
   visitor already reads: the date and category in the meta band under the
   hero, the partner and the location in the article's own sentences. It was a
   second copy of the page's own content in a smaller type size.

   The aside is now one thing on every article: the figure cards. That is the
   whole unification - one column of numbers on the reading-end side, the same
   component at the same size on every story. An article with no figures worth
   pulling out (cert-partnership-2026) carries no aside at all and runs
   .article-shell--full instead; the alternative would be inventing numbers to
   fill a column, which the content rules forbid. */

.article-share {
  display: flex;
  align-items: center;
  gap: 4px;
}

/* The row says what it is in words instead of leaving three unlabelled
   glyphs to be guessed at. Supporting text, so it takes --type-meta and the
   label colour rather than the badge role the date and category carry: the
   badge states a fact about the article, this names a control. */
.article-share-label {
  margin-inline-end: 8px;
  font-size: var(--type-meta);
  color: var(--label);
}

/* No chip. The beige fill these carried was a filled box behind a glyph on a
   band that is already its own surface, which is the same mistake the figure
   icons made - a background competing with the thing it labels. The glyph
   alone carries it, and the hover state is a colour step, which is the
   approved interaction vocabulary. The 40px box stays: it is the touch
   target, not a visual treatment. */
.article-share a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 40px;
  block-size: 40px;
  border-radius: var(--r);
  font-size: var(--icon-default);
  color: var(--icon);
  background: none;
  transition: color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

/* The hover state is the one the site already gives an icon-only control on a
   light surface: the Liwa colour step plus --icon-wash filling the 40px box.
   Written to match .news-search-clear rather than authored fresh - that is the
   same component doing the same job (a bare glyph in a touch target that has
   to answer a pointer), so it answers the same way.

   This does NOT reinstate the resting chip the note above removed. That was a
   permanent beige box sitting behind the glyph whether or not anyone was
   pointing at it, which is why it competed with the mark it labelled. The wash
   is neutral, it is Jais at 10% rather than a warm fill, and it exists only for
   as long as the control is being addressed - which is what makes it a state
   instead of a treatment. It is also the reason the 40px box is worth having:
   until now nothing ever drew it, so its size was an assertion in the comment
   above with nothing on screen to confirm it. */
.article-share a:is(:hover, :focus-visible) {
  color: var(--accent-text);
  background-color: var(--icon-wash);
}

/* Related stories, on the sunken rung.
   This reverses an earlier decision and the reversal is the site owner's:
   the section was put on --surface-base to match the article above it, on the
   argument that the white cards alone carried enough separation. In practice
   they did not - the rail ran straight on from the end of the article with no
   edge between them, so it did not read as a different section at all.
   --surface-sunken is the step the token file already prescribes for exactly
   this: section 10 says a card on a light section needs the section under it
   to be sunken, and these cards are --surface-raised. So the change buys the
   section boundary AND puts the cards on the ground they were designed for,
   with no new value and no border. */
.article-related {
  background: var(--surface-sunken);
}

/* AND THE SAND GROUND COMES WITH IT. Section 5b of the token file:
   --surface-sunken is not a flat fill, it is the lit half of a ground whose
   other half is --ground-sand, and a section takes both or neither. This was
   the last section on the site holding the colour without the pattern, which is
   the failure that rule exists to make impossible - it reads as sand on the
   generated pages and as a different, plainer sand here, on the one page family
   a visitor is most likely to arrive at from outside.

   The gradient is not restated here. It is the same token the sections file and
   both homepages draw, so the four measurements live in one place and this rail
   cannot drift away from the panels it is supposed to match. What IS restated is
   the three-part setup around it, because these pages do not load
   assets/css/sdeira-sections.css and so inherit none of its section machinery:
   a positioned host, a ::before at z-index 0, and the wrap lifted above it.

   A second sunken section in this sheet joins the :is() list rather than
   authoring its own copy of this. */
:is(.article-related) {
  position: relative;
  isolation: isolate;
}

:is(.article-related)::before {
  content: "";
  position: absolute;
  z-index: 0;
  inset: 0;
  background: var(--ground-sand);
  pointer-events: none;
}

:is(.article-related) > .wrap {
  position: relative;
  z-index: 1;
}

/* Two per row, fixed, on the related rail AND the news index - both by
   client request, so a wider card with a bigger image is the one shape a
   visitor sees for a story, whether they are reading the full list or "read
   next" under a finished article. A fixed track count is what guarantees the
   two, since the base .news-list rule above is auto-fill and would fit three
   across as soon as the viewport allows it. Explicit tracks also mean no
   phantom column is ever reserved, which is what auto-fill got wrong here.
   The feature story still spans 1 / -1 above this rule, so its first row stays
   full-width while every standard story remains in a two-column row. */
.article-related .news-list,
.news-index .news-list {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

/* =============================================================================
   13. EMPTY STATE
   Shown when a search or filter matches nothing. Neutral grey, not red: an
   empty result is not an error.
   ========================================================================== */

/* Enough block-size that the state reads as its own section, not a thin strip
   with the footer visible underneath it: a one-line message in 46px of
   padding left the footer showing through right below it. */
.news-empty {
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  min-block-size: clamp(280px, 32vw, 360px);
  padding: 46px 26px;
  text-align: center;
  color: var(--pending);
  background: var(--surface-raised);
  border-radius: var(--r);
}

.news-empty.is-shown {
  display: flex;
}

/* Top step of the token icon scale, not a local clamp of its own. It used to
   run larger than anything the scale defines, which made the empty state the
   only place on the site with a sixth icon size. */
.news-empty i.ph {
  font-size: var(--icon-display);
  color: var(--pending);
  opacity: .5;
}

.news-empty p {
  margin: 0;
  max-inline-size: 40ch;
}

/* ==========================================================================
   13b. PAGINATION
   Kept out of the tab row so it reads as a position in the archive rather
   than as another filter.
   ========================================================================== */

.news-pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-block-start: clamp(28px, 3vw, 40px);
}

/* A single page is not a choice, so the control is not rendered at all. */
.news-pagination:not(.is-shown) {
  display: none;
}

/* Server-side pagination and filter tabs render as links; the pill components
   were styled for buttons, so anchors inherit the same look without the
   default underline. */
a.news-page,
a.news-tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}

.news-page {
  min-inline-size: 42px;
  block-size: 42px;
  border: 0;
  border-radius: var(--r);
  padding-inline: 14px;
  font: inherit;
  font-size: var(--type-action);
  color: var(--brand-jais);
  background: var(--surface-raised);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.news-page:hover:not(:disabled) {
  color: var(--brand-black);
  background: var(--surface-sunken);
}

.news-page[aria-current="page"] {
  color: var(--brand-white);
  background: var(--accent-fill);
}

.news-page:disabled {
  color: var(--pending);
  cursor: default;
}

.news-page:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Arrow buttons carry an icon from the site's own set, not a "â†’" glyph
   rendered in whatever font the browser picked for it. They size from the icon
   scale and stay square instead of tracking the width of a digit. */
.news-page--arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 42px;
  padding-inline: 0;
}

.news-page--arrow i.ph {
  font-size: var(--icon-control);
  line-height: 1;
}

/* ==========================================================================
   14. REVEAL - OFF BY DECISION on every page this sheet serves.
   Site owner instruction, 13 Aug 2026: the fade-up (opacity + translateY(24px)
   at --dur-slow, same values as the homepage's own .reveal) never read right
   here - one whole-section block moving together, or a coarser cascade than
   the homepage's per-element .reveal[data-d] - and repeated passes did not
   close that gap. Switched off rather than left half-fixed. The homepage's
   .reveal is UNCHANGED and is explicitly not in scope for this decision.

   .sweep/data-d stay in the markup .sweep()/components.py emit - removing
   the class from 15+ files was not worth doing for a rule that now does
   nothing - so this block simply never hides anything, on every viewport and
   regardless of prefers-reduced-motion. Do NOT re-enable this by restoring
   the opacity:0/translateY/transition triple below without a fresh decision:
   this is intentional, not the same silent regression a build later reverted
   once already (see git blame on this block before touching it).
   ========================================================================== */

.sweep,
.sweep.in {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: none;
}

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

  .news-item-media img,
  .news-item:is(:hover, :focus-within) .news-item-media img {
    transform: none;
    transition: none;
  }
}

/* ==========================================================================
   15. RESPONSIVE (COMPONENTS)
   Every breakpoint-specific override for the sections above lives here, in one
   place, each with a note on what it changes. Nothing above this line is
   redefined anywhere else in this file.
   ========================================================================== */

/* Article: the sidebar stops earning a column and moves under the body. */
@media (max-width: 1080px) {
  .article-shell {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Also held to the measure and centred, because the reading card above it now
     is: the track here is the whole wrap, which is wider than one measure. Left
     to stretch, the numbers would run wider than the article they belong to and
     the column would read as two blocks that happen to be stacked rather than
     as one story. */
  .article-aside {
    position: static;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
    gap: var(--article-stack-gap);
    inline-size: 100%;
    max-inline-size: min(100%, var(--article-measure));
    margin-inline: auto;
  }
}

/* Feature card: below this width the horizontal split leaves the headline in a
   column too narrow to read, so the card stacks. */
@media (max-width: 860px) {
  .news-item--feature {
    flex-direction: column;
  }

  .news-item--feature .news-item-media {
    flex: none;
    block-size: clamp(200px, 34vw, 280px);
    min-block-size: 0;
  }
}

/* The chrome half scopes the phone gutter to `main > section > .wrap` and
   `footer > .wrap` (specificity 0,1,1, which is what beats the plain `.wrap`
   rule at 560px). An article page nests its wraps one level deeper, inside
   <article>, so they are restated here by name rather than bending the markup
   to fit a selector. Without this the article column silently keeps 20px on a
   phone while every other section steps to the gutter. */
@media (max-width: 720px) {

  .subpage-hero > .wrap,
  .breadcrumb > .wrap,
  .news-index > .wrap,
  .article-meta-bar > .wrap,
  .article-layout > .wrap,
  .article-related > .wrap {
    padding-inline: var(--page-gutter);
  }
}

/* Hero: the strips narrow so they stay an accent at the reading-start edge
   rather than a full-width wash behind the headline. */
@media (max-width: 720px) {
  /* On a phone the fixed header is shorter and the panel does not need to be
     tall, so the block-start padding drops to just clearing the header. */
  .subpage-hero {
    min-block-size: 0;
    padding-block: clamp(112px, 18vh, 140px) 34px;
  }

  .subpage-hero h1 {
    max-inline-size: none;
  }
}

/* Toolbar below 900: exactly two rows, not three or four. The tabs move to
   their own row, and search plus both dropdowns share the other - together,
   because they are one flex item now (.news-toolbar-filters), not four
   siblings each deciding on its own whether to wrap.
   flex-direction: column puts the two groups on separate rows. The tabs pill
   must NOT stretch across that row: align-items stays flex-start (the column
   default) so it keeps hugging its own buttons, and only the filters group
   opts back into full width with align-self: stretch, because a search field
   and two dropdowns sharing a row need the width to lay out in. */
@media (max-width: 900px) {
  .news-toolbar {
    flex-direction: column;
    align-items: stretch;
  }

  .news-tabs {
    align-self: flex-start;
    max-inline-size: 100%;
  }

  .news-toolbar-filters {
    inline-size: 100%;
  }

  /* min-inline-size stays the 120px floor set on .news-search itself. Setting
     it to 0 here, which an earlier pass did, let the field shrink to nothing
     once two dropdowns needed the room instead of stopping at a usable floor;
     0 is correct for a plain flex child but wrong for this one, which has a
     legitimate minimum content width (a placeholder still needs to read). */
  .news-search {
    flex: 1 1 auto;
  }

  /* Left at the base flex: 0 1 auto (not overridden to 0 0 auto here, which an
     earlier pass did): a dropdown may shrink below its label's natural width
     once the row is genuinely too narrow for search plus both of them, and
     .news-dropdown-value's ellipsis is what makes that survivable instead of
     the row overflowing its own container with nothing visibly wrong. */
  .news-dropdown {
    min-inline-size: 74px;
  }

  /* Anchored to the near edge so a menu on the far dropdown cannot overflow
     the page on a narrow screen. */
  .news-dropdown:last-child .news-dropdown-menu {
    inset-inline-start: auto;
    inset-inline-end: 0;
  }
}

/* Below 480, even two dropdowns plus a shrunk search field get tight next to
   the tab strip's own scroll affordance. The dropdowns drop their labels' grow
   room further by letting the search field shrink to its floor first; nothing
   here wraps a second time. */
@media (max-width: 400px) {
  .news-toolbar-filters {
    gap: 8px;
  }

  .news-dropdown-toggle {
    padding-inline: 12px;
  }
}

/* Both fixed two-track grids above need their own collapse rule: neither is
   auto-fit, so neither reflows on its own, and both selectors are more
   specific than the plain `.news-list` rule in the 560px block below, which
   means that rule never reaches either of them. Two half-width cards stop
   reading at roughly 700px of content width, hence 760px rather than 560px. */
@media (max-width: 760px) {
  .article-related .news-list,
  .news-index .news-list {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Cards: a single column, and the reserved title height is released because a
   full-width card fits the headline in fewer lines. */
@media (max-width: 560px) {
  .news-list {
    grid-template-columns: minmax(0, 1fr);
  }

  .news-item h3 {
    min-block-size: 0;
  }

  .news-item-body {
    padding: 20px 20px 24px;
  }

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

/* =============================================================================
   ENGLISH MIRROR
   -----------------------------------------------------------------------------
   This sheet is shared by the 5 Arabic news pages and the 32 generated English
   ones, and it was authored RTL-first. Almost all of it is direction-agnostic
   because it uses logical properties - but a transform-origin cannot be
   logical, so every underline-grow in here names a PHYSICAL edge, and names
   the Arabic one.

   That is the exact failure CLAUDE.md records: "Decorative geometry uses
   physical coordinates, not logical ones... An English mirror must mirror
   those values by hand." It was mirrored on the English homepage - see the
   html[dir="ltr"] block in sdeira-v2-en.css - and never here, which is why the
   fault showed on the sub-pages only.

   What it looked like: .action-link's underline rests at scaleX(.42) and grows
   to full on hover. Anchored with inset-inline-start: 0 but scaled from
   `right`, the resting 42% sat at the FAR end of the box in English - a short
   bar floating under "more" with nothing under "Read" - and grew backwards
   towards the text instead of away from it. In Arabic the same two values
   agree, because there the right edge IS the reading start.

   Same answer as the homepage's, applied to all three underlines in this file
   rather than to the one that was reported. .news-item's variant is included
   even though those five pages are RTL-only today: it is the same component,
   and leaving one of the three behind is how this drifted in the first place.
   ========================================================================== */
[dir="ltr"] :is(.action-link, .news-item .action-link)::after {
  transform-origin: left center;
}

[dir="ltr"] .more::after {
  transform-origin: left;
}

/* The English mirror's sibling bug to the one above, in the icon rather than
   the underline: `.action-link > i.ph` rests at rotate(45deg), physically
   down-right, which was fixed for English on the homepage the same way as the
   underline - an html[dir="ltr"] override in sdeira-v2-en.css turning the rest
   angle to rotate(-35deg), up-right - and never ported here either. Every
   English sub-page (contact, business/*, projects/*, careers, ...) that
   carries a bare .action-link or .section-view-all - not `.news-item
   .action-link`, which already slides on translateX and needs no rotation fix
   - rested at the Arabic angle. Same value as the homepage's own fix, not a
   new one. */
[dir="ltr"] .action-link > i.ph {
  transform: rotate(-35deg);
}

[dir="ltr"] .action-link:is(:hover, :focus-visible) > i.ph {
  transform: rotate(0deg);
}

/* A square-out glyph is a departure marker, not a directional arrow - see the
   matching i.row-item-action.ph-arrow-square-out rule in sdeira-sections.css.
   This LTR override has higher specificity ([dir="ltr"] + .action-link + i.ph)
   than that one, so it was still winning and rotating the external-link icon
   on English sub-pages (e.g. /en/business/services/) even though the RTL side
   was already correctly static. */
[dir="ltr"] .action-link > i.ph.ph-arrow-square-out,
[dir="ltr"] .action-link:is(:hover, :focus-visible) > i.ph.ph-arrow-square-out {
  transform: none;
}


/* ============================================================================
   SINGLE NEWS ARTICLE
   ----------------------------------------------------------------------------
   Aligned to the reference article layout (website subpage.css, "Article").
   The hero inherits the shared .subpage-hero treatment. The theme article has
   no headline-figures aside, so it uses the reference's full-width shell
   (.article-shell--full): the featured figure runs the section width and the
   running text is capped at a reading measure. Only the single article
   template uses these classes; the news archive and CMS content are untouched. */
.subpage-hero--article h1 {
  font-size: var(--type-feature);
  line-height: var(--leading-feature);
  max-inline-size: 54ch;
}

/* ---- Meta bar: date / category / partner + share -------------------------
   A full-bleed Light Grey band under the hero, content held to the page wrap
   (the same move the news index makes with its breadcrumb band). */
.article-meta-bar {
  background: var(--surface-sunken);
}
.article-meta-bar > .wrap {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-block: 14px;
}
.article-meta-bar .news-item-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 14px;
  color: var(--label);
  font-size: var(--type-meta);
}
.article-share {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.article-share-label {
  color: var(--label);
  font-size: var(--type-meta);
}
.article-share a {
  display: inline-grid;
  place-items: center;
  inline-size: 40px;
  block-size: 40px;
  border-radius: var(--r);
  color: var(--icon);
  transition: color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}
.article-share a:is(:hover, :focus-visible) {
  background: var(--accent-wash);
  color: var(--accent-strong);
}
.article-share a:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

/* ---- Article layout: section proportions ---------------------------------
   Local to the section on purpose (the reference keeps these here, not in the
   token file): they are this section's rhythm, not site-wide spacing. */
.article-layout {
  background: transparent;
  --article-card-pad: clamp(18px, 1.5vw, 24px);
  --article-body-pad: clamp(24px, 3vw, 44px);
  --article-stack-gap: clamp(14px, 1.4vw, 20px);
  --article-shell-gap: clamp(24px, 2.6vw, 40px);
  --article-measure: 900px;
}
.article-shell {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(240px, 3fr);
  gap: var(--article-shell-gap);
  align-items: start;
}
/* No figure aside => one full-width track, so articles without the
   article-figures block keep the single-column reading layout exactly as
   before (matches the reference's .article-shell--full). */
.article-shell--full {
  grid-template-columns: minmax(0, 1fr);
}
/* Below the aside breakpoint the figures stop earning a column and stack under
   the body, the same collapse the reference's .article-shell carries. */
@media (max-width: 1080px) {
  .article-shell {
    grid-template-columns: minmax(0, 1fr);
  }
}
.article-main {
  max-inline-size: min(100%, var(--article-measure));
  margin-inline: auto;
  padding: 0;
  background: transparent;
  border-radius: 0;
}
/* The figure is the one child exempt from the reading cap, so the featured
   photo runs the section's full content width. */
.article-figure {
  margin: 0 0 28px;
}
.article-figure img {
  display: block;
  inline-size: 100%;
  height: auto;
  border-radius: var(--r);
}

/* ---- Article body: running text + Gutenberg content ---------------------- */
.article-body {
  font-size: var(--type-body);
  line-height: var(--leading-body);
  color: var(--ink);
}
.article-shell--full .article-body > :not(.article-figure) {
  max-inline-size: min(100%, 95ch);
}
.article-body > :first-child {
  margin-block-start: 0;
}
.article-body :is(h2, h3, h4) {
  font-family: var(--font-display);
  color: var(--ink);
  line-height: 1.2;
  margin-block: 1.6em 0.6em;
}
.article-body h2 { font-size: var(--type-section); }
.article-body h3 { font-size: var(--type-card); }
.article-body h4 { font-size: var(--type-body); font-weight: 700; }
.article-body p { margin-block: 1em 0; }
.article-body a {
  color: var(--accent-strong);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
.article-body a:hover { text-decoration-thickness: 2px; }
.article-body :is(ul, ol) {
  margin-block: 1em;
  padding-inline-start: 1.4em;
}
.article-body li { margin-block: .4em; }
.article-body blockquote {
  margin-block: 1.6em;
  padding-inline-start: 1em;
  border-inline-start: 3px solid var(--accent);
  color: var(--muted);
}
.article-body :is(figure, img) {
  max-inline-size: 100%;
  height: auto;
}
.article-body .wp-block-image {
  margin-block: 1.6em;
}
.article-body .wp-block-image img {
  border-radius: var(--r);
}
.article-body :is(table, .wp-block-table) {
  inline-size: 100%;
  border-collapse: collapse;
  margin-block: 1.6em;
}
.article-body :is(th, td) {
  border: 1px solid var(--surface-sunken-strong);
  padding: 8px 10px;
  text-align: start;
}

/* ---- Related stories ------------------------------------------------------ */
.article-related {
  padding-block: clamp(40px, 5vw, 72px);
}
.article-related .news-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(18px, 3vw, 32px);
}

@media (max-width: 720px) {
  .article-meta-bar .wrap {
    flex-direction: row;
  }
  .article-share {
    /* inline-size: 100%; */
    justify-content: space-between;
  }
  .article-related .news-list {
    grid-template-columns: 1fr;
  }

  .breadcrumb--hero [aria-current="page"]{
    display: inline-block;
  max-width: 220px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  }
}
