/* port.css — port-authored behaviour CSS. Reimplements Webflow IX2 states with
 * values recovered from the captured ixData (webflow.schunk.a29dd4a398a5bc80).
 * The webflow.css authority file is never edited beyond its 3 URL rewrites.
 * Easings: outQuart = cubic-bezier(.165,.84,.44,1)  outQuint = cubic-bezier(.23,1,.32,1)
 */

/* --- Fade-up (IX2 a-223 "Fade In Bottom": y20px/op0 -> 0/1, 1400ms outQuart).
 * Initial state gated on html.js so a JS-off render shows final content
 * (matches live behaviour: IX2 applies initial states from JS). --- */
html.js [data-anim="fade"] {
  opacity: 0;
  transform: translate3d(0, 20px, 0);
}
html.js [data-anim="fade"].is-inview {
  opacity: 1;
  transform: translate3d(0, 0, 0);
  transition:
    opacity 1.4s cubic-bezier(0.165, 0.84, 0.44, 1),
    transform 1.4s cubic-bezier(0.165, 0.84, 0.44, 1);
}

/* --- Logo marquee (IX2 a-239/a-240, PAGE_START, loop, linear).
 * Implemented in px per tier: one set + one flex gap. Percentages are NOT
 * exact here (3 sets have only 2 inter-set gaps -> track 3800px, 33.333% =
 * 1266.67px != 1300px). <=767 uses the corrected 880px travel with the live
 * velocity (98 px/s) preserved — deliberate divergence, see DEVIATIONS DL-027. --- */
.clients-logos-grid {
  will-change: transform;
  animation: port-marquee-1382 15.95s linear infinite;
}
@keyframes port-marquee-1382 { to { transform: translate3d(-1381.95px, 0, 0); } }
@keyframes port-marquee-1255 { to { transform: translate3d(-1255.09px, 0, 0); } }
@media screen and (max-width: 991px) {
  .clients-logos-grid { animation: port-marquee-1382 14.1s linear infinite; }
}
@media screen and (max-width: 767px) {
  .clients-logos-grid { animation: port-marquee-1255 12.81s linear infinite; }
}

/* --- DL-083: even 80px marquee spacing at every breakpoint. The inner drops
 * webflow's fixed width + space-between (gap size there depended on logo
 * widths, and the inter-set gap was 100px vs ~63px internal) for content width
 * + fixed flex gap; the grid's inter-set gap is pinned to the same 80px so the
 * seam gap equals the internal gap. Travel per loop = one inner width + one
 * 80px gap, measured from the rendered DOM: 1301.95+80 at >=768 (attr-height
 * logos, both tiers identical), 1175.09+80 at <=767 (webflow's forced 25px
 * logo height changes the widths). Velocities preserved from the pre-DL-083
 * rules: 86.67 px/s desktop, 98 px/s at <=991 and <=767 (DL-027). --- */
.home-clients-logos-inner {
  width: auto;
  justify-content: flex-start;
  gap: 80px;
}
.clients-logos-grid {
  grid-column-gap: 80px;
}

/* --- Parallax targets (IX2 a-139; transform driven from interactions.js).
 * .cta-contact-image, .services-hero-image and .about-hero-image are further
 * live targets via their own class-keyed interactions. .case-hero-image is the
 * a-242 pin target (also driven from interactions.js). --- */
.image-cover-parallax, .cta-contact-image,
.services-hero-image, .about-hero-image,
.case-hero-image { will-change: transform; }

/* --- Button dual-text slide (IX2 a-228 reimpl; .button-inner-text-hover sits
 * at top:100% in webflow.css; both texts ride up one line-height on hover) --- */
.button-inner-text,
.button-inner-text-hover {
  transition: transform 0.5s cubic-bezier(0.23, 1, 0.32, 1);
}
.button:hover .button-inner-text,
.button:hover .button-inner-text-hover {
  transform: translate3d(0, -100%, 0);
}

/* --- Label-link dual-text slide (IX2 a-230/a-231 "Label Link Hover", 500ms
 * outQuint: both texts ride up one line-height, same mechanics as buttons) --- */
.label-inner .label-text,
.label-inner .label-text-hover {
  transition: transform 0.5s cubic-bezier(0.23, 1, 0.32, 1);
}
.label-link:hover .label-text,
.label-link:hover .label-text-hover {
  transform: translate3d(0, -100%, 0);
}

/* --- Hover underlines (IX2 line interactions; bases are width:0% in webflow.css) --- */
.navbar-menu-item-line,
.nav-item-line,
.subnav-item-line {
  transition: width 0.5s cubic-bezier(0.23, 1, 0.32, 1);
}
.navbar-menu-item:hover .navbar-menu-item-line,
.nav-item:hover .nav-item-line,
.subnav-item:hover .subnav-item-line { width: 100%; }
.button-text-line {
  width: 100%;
  transition: width 0.4s cubic-bezier(0.23, 1, 0.32, 1);
}
.button-text:hover .button-text-line { width: 0%; }

/* --- Mobile menu (IX2 a-172/a-173: 500ms outQuart; captured initial state was
 * display:none / opacity:0 / translateY(10px)) --- */
.mobile-menu {
  opacity: 0;
  transform: translate3d(0, 10px, 0);
  transition:
    opacity 0.5s cubic-bezier(0.165, 0.84, 0.44, 1),
    transform 0.5s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.mobile-menu.is-open {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}
.mobile-menu-toggle-line {
  transition: transform 0.5s cubic-bezier(0.165, 0.84, 0.44, 1), width 0.5s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.mobile-menu-toggle.is-open ._01 { transform: translate3d(0, 9.5px, 0) rotate(-45deg); }
.mobile-menu-toggle.is-open ._02 { width: 0%; }
.mobile-menu-toggle.is-open ._03 { transform: translate3d(0, -9.5px, 0) rotate(45deg); }

/* --- Anchor scrolling (webflow.js eased-scrolls; nearest native equivalent) --- */
html { scroll-behavior: smooth; }

/* --- Contact new-build layout (design/contact-new-layout.png, DL-054).
 * The mockup's hero is a ~.5fr/1fr split and its offices media row is
 * ~1fr/.6fr; webflow.css has no two-column grid at either ratio that also
 * stacks at the template breakpoints. Only grid-template-columns is
 * overridden — stacking still comes from webflow.css's own <=991 (hero) /
 * <=767 (offices) display:flex variants, which make column values moot
 * there. Scoped to contact-only wrappers; .section-contact-offices is a
 * rule-less hook class used purely as scope. --- */
.contact-hero-grid { grid-template-columns: .5fr 1fr; }
.section-contact-offices .home-mission-grid-right { grid-template-columns: 1fr .6fr; }

/* --- Contact page background unification (#f9f4ea, sampled from the mockup;
 * not a palette token — DL-054/DL-061). Three jobs in one colour:
 * 1. body repaint: webflow.css >=1440 flips .body to navy (template quirk,
 *    shipped verbatim — DL-061). On this reference-less page that made the
 *    transparent light navbar illegible and a hover on the invisible Pages
 *    toggle flashed the dropdown's "More Templates" button over the hero.
 *    Contact-only; ported pages keep the faithful navy band.
 * 2. hero section + card share the colour — card is border-defined, no fill
 *    difference, as in the mockup.
 * 3. input wells match the card (mockup shows no lighter fill; ours sat
 *    darker via --form-default--background). :focus keeps the border-colour
 *    affordance from webflow.css but not the darker fill. --- */
body:has(.section-contact-hero) { background-color: #f9f4ea; }
.section-contact-hero { background-color: #f9f4ea; }
.contact-hero-grid-right-inner > .card { background-color: #f9f4ea; }
.section-contact-hero .form-input,
.section-contact-hero .form-input:focus { background-color: #f9f4ea; }

/* --- The capped card rides the right container edge so the hero's content
 * width matches the offices section's full-container rows below it
 * (.contact-hero-grid-right centres its child by default). Desktop only —
 * in the <=991 stack a right-shifted card under left-aligned text reads
 * broken. --- */
@media screen and (min-width: 992px) {
  .section-contact-hero .contact-hero-grid-right { align-items: flex-end; }
}

/* --- Light-navbar legibility at >=1440 (DL-064; supersedes DL-061's
 * "ported pages keep the navy band" for the CONTENT of the bar only).
 * webflow.css flips .body navy at >=1440; the light navbar is transparent, so
 * every light-chrome page painted dark menu items and the dark Nikka SVG onto
 * navy. Client-directed divergence: reuse the homepage's own inverse
 * mechanism — the same --navbar-default-inverse--* tokens .navbar-menu-item.inverse
 * resolves to — and swap in the cream In Tandem mark (logo-on-dark.svg since
 * DL-073, the homepage navbar asset). Scoped to the light header (.header:not(.absolute); home chrome is
 * already inverse) and off contact, whose body stays #f9f4ea at >=1440
 * (DL-061 repaint) and keeps its dark treatment. Below 1440 the band is cream
 * and the dark treatment already reads — untouched. --- */
.navbar-logo-image.port-logo-inverse { display: none; }
@media screen and (min-width: 1440px) {
  body:not(:has(.section-contact-hero)) .header:not(.absolute) .navbar-menu-item {
    color: var(--navbar-default-inverse--text-color);
  }
  body:not(:has(.section-contact-hero)) .header:not(.absolute) .navbar-menu-item:hover {
    color: var(--navbar-default-inverse--text-color-hover);
  }
  body:not(:has(.section-contact-hero)) .header:not(.absolute) .navbar-menu-item-line {
    background-color: var(--navbar-default-inverse--border-color);
  }
  body:not(:has(.section-contact-hero)) .header:not(.absolute) .navbar-logo-image {
    display: none;
  }
  body:not(:has(.section-contact-hero)) .header:not(.absolute) .navbar-logo-image.port-logo-inverse {
    display: block;
  }
}

/* --- Custom radio/checkbox icons are flex items inside .form-radio /
 * .form-checkbox with no shrink guard; multi-line labels squash them
 * (measured 10.125px on the consent checkbox). Template never pairs them
 * with long labels; the contact form does (DL-062). --- */
.form-radio-icon, .form-checkbox-icon { flex-shrink: 0; }

/* --- Chrome autofill paints its own light-blue background/text over
 * .form-input; repaint to the contact card colour so filled fields keep the
 * cream treatment (production fix — contact form is the only form). --- */
.form-input:-webkit-autofill,
.form-input:-webkit-autofill:hover,
.form-input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px #f9f4ea inset;
  -webkit-text-fill-color: var(--form-default--text-color);
  caret-color: var(--form-default--text-color);
}

/* --- About jobs section full-height (client direction). The testimonial
 * video block reads stronger as a full-viewport moment; webflow.css gives it
 * a fixed height, min-height only extends it. Video is the standard Webflow
 * background-video atom (absolute, object-fit cover) and .overlay is
 * inset-0, so both track the taller section. --- */
.section-about-jobs { min-height: 100vh; }

/* --- Brand mark sizing (DL-073). Navbars use the compact crop (2.381:1,
 * cradle + wordmark; the tagline row is illegible below ~215px width and is
 * cropped out of the navbar variants). Solid navbar: 80px -> 33.6px tall,
 * inside the navbar-inner's 42px slack (buttons cell governs, align-items
 * center), so the bar height does not move. Transparent home and the >=1440
 * inverse state: the imgs carry width="142" (59.6px tall, 14px shorter than
 * the retired PNG's box). At <=479 every navbar mark drops to 60px
 * (25.2px tall). Footer keeps the full lockup at 142px.
 * DL-110 retires the 80px solid-bar size: the light-navbar mark now matches
 * the home navbar's width="142" attribute value, so the mark is one size
 * across all navbars above 479px. The DL-073 height-slack rationale no
 * longer holds, the taller mark may govern the solid bar height, accepted
 * (client direction 2026-07-31). --- */
.port-logo-light { width: 142px; }
.port-logo-footer { width: 142px; }
@media screen and (max-width: 479px) {
  .navbar-logo-image { width: 60px; }
}

/* --- DL-078: dead-route chrome removals. The Utility Pages column (dead
 * /template-info/* links, no route, no redirect) is deleted from the footer
 * and both navbar dropdowns, so each grid has 3 children; webflow.css keeps
 * 4 explicit 1fr tracks and would leave an empty quarter-width track on the
 * right. Re-template to 3 tracks where the 4-track base rule governs
 * (>=992px); below that webflow.css's own 2-col/1-col overrides must keep
 * winning, and the dropdown is hide-tablet there anyway. DL-084 retired the
 * footer half of this override (badge column refilled the 4th track);
 * DL-085 moved the badges to a full-width row and restored it, so both
 * grids are back to 3 tracks. DL-124 splits the two halves: the navbar
 * dropdown lost its Company column and has 2 children (Resources list +
 * contact card), the footer keeps its 3 and is unchanged. --- */
@media screen and (min-width: 992px) {
  .footer-grid { grid-template-columns: 1fr 1fr 1fr; }
  .navbar-dropdown-inner { grid-template-columns: 1fr 1fr; }
}

/* --- DL-126: more air between the logo and the first nav item. webflow.css
 * gives .navbar-left a 3rem flex gap at base, 2rem at <=991 and 1rem at
 * <=767; the box holds exactly the logo and the .navbar-menu, so the gap IS
 * the logo-to-first-item spacing and nothing else moves. Raised to 5rem, and
 * scoped >=992 so webflow.css's own narrower steps keep winning below (port
 * .css loads after webflow.css, so an unscoped rule would beat them; below
 * 768 the menu is display:none anyway). MEASURED, not assumed: the gap reads
 * 80px at 992/1280/1440/1920 and .navbar-inner still has 110px of unused
 * slack at the 992 floor, so nothing crowds. --- */
@media screen and (min-width: 992px) {
  .navbar-left { grid-column-gap: 5rem; }
}

/* --- DL-086: accreditation badge row inside the CMS footer column, a
 * compact trust-mark strip below the News link, left-aligned to the column
 * (client mockup footer-placement.png; supersedes DL-085's full-width band,
 * grid stays DL-078 3-track either way). No strip, no recolor: berkshire
 * (JPEG, no alpha possible) and prestige (no-alpha webp) still show their
 * own light card backgrounds on the navy, flagged, transparent re-exports
 * pending. Heights are optical-balance values, per badge, not uniform,
 * scaled to fit one column track: airy rosette largest, seals mid, solid
 * ICO tile smaller, the 2:1 Prestige card lowest so its area matches the
 * seals. DL-087 scales the DL-086 sizes up ~1.4x (48/44/44/40/36 read too
 * small); the row may wrap within the column, acceptable per client. --- */
.port-footer-badges {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .75rem;
  margin-top: 2rem;
}
.port-footer-badges img { width: auto; display: block; }
.port-footer-badges img[alt^="CIPD"] { height: 62px; }
.port-footer-badges img[alt^="Berkshire"] { height: 62px; }
.port-footer-badges img[alt^="UK Small"] { height: 68px; }
.port-footer-badges img[alt^="Prestige"] { height: 50px; }
.port-footer-badges img[alt^="ICO"] { height: 56px; }
/* DL-105/DL-106: desktop readability bump, every DL-087 height scaled by
 * the same ~1.29 so the per-badge optical hierarchy is untouched. Height
 * only, width stays auto, no mark is distorted (CIPD and ICO are regulated
 * marks). DL-106 re-keys the bump onto .port-contact-badges: DL-105 keyed
 * it on the shared .port-footer-badges rule and unintentionally enlarged
 * the footer rows too; only the contact hero strip carries the second
 * class, so footer badges everywhere return to the shipped DL-087 sizes
 * and the contact hero row keeps the large ones. Mobile untouched. */
@media screen and (min-width: 992px) {
  .port-contact-badges img[alt^="CIPD"] { height: 80px; }
  .port-contact-badges img[alt^="Berkshire"] { height: 80px; }
  .port-contact-badges img[alt^="UK Small"] { height: 88px; }
  .port-contact-badges img[alt^="Prestige"] { height: 65px; }
  .port-contact-badges img[alt^="ICO"] { height: 72px; }
}

/* --- DL-107: the about hero slot takes its image's own ratio. The new
 * about-hero.webp is 1920x1080 (16:9); the shipped slot is 45vw capped
 * 80vh (~2.13 crop window) and would crop it heavily. At >=992px the slot
 * becomes aspect-ratio 1920/1080 with the height and cap released, so the
 * about hero runs taller than the other heroes, accepted and intended.
 * Below 992 the shipped tiers already run taller than 16:9 (75vw, then
 * content-driven at <=479) and stay untouched. Image stays object-fit
 * cover, so any residual rounding is invisible. --- */
@media screen and (min-width: 992px) {
  .section-about-hero { height: auto; max-height: none; aspect-ratio: 1920 / 1080; }
}

/* --- DL-091: mobile size for the homepage hero h1. Deliberate divergence
 * from the DL-024 ported value (span pinned at 4rem/64px at every viewport,
 * no mobile override in webflow.css), NOT a port fidelity fix: the DL-090
 * forced three-liner re-wraps to six lines below 768 because "Pragmatic HR."
 * is 463px in Montserrat at 64px and the text column is 334px at 375.
 * 2.75rem is webflow.css's own <=767 step for the .home-hero-heading
 * wrapper; at 44px the widest line is 318px and all three fit to 767.
 * Scoped to the homepage hero span only. --- */
@media screen and (max-width: 767px) {
  .home-hero-heading .text-color-muted-inverse.is-h1 { font-size: 2.75rem; }
}

/* --- DL-092: contact-page testimonial, plain text in the hero left column
 * (client mockup contact-new-layout.png, then client direction dropping the
 * dark card, avatar and photo in favour of bare text on the cream page).
 * Quote in heading-style-h5, attribution in text-meta muted, both from
 * webflow.css. Uses quote-icon.svg (dark #0C1E21), not the inverse mark
 * the homepage/About testimonials use, because that one is pure white and
 * invisible on cream. New design, no reference. --- */
.port-contact-testimonial .heading-style-h5 { max-width: 34rem; }

/* --- DL-093: the five accreditation badges repeated on the contact page
 * between testimonial and socials, reusing the DL-086/087 footer row
 * (.port-footer-badges flex row and per-badge optical heights). No strip
 * and no recolor: the page is cream, so the opaque Berkshire and Prestige
 * tiles sit on a light ground natively. Only override: drop the footer
 * rule's 2rem margin-top, spacing here comes from margin utilities. --- */
.port-contact-badges { margin-top: 0; }

/* --- DL-094: contact testimonial recolored to #1A1F2BA6 (navy at ~65%
 * alpha, client-specified, intentional), quote, attribution and icon as
 * one shade. The DL-092 quote-icon.svg img had a baked #0C1E21 fill no
 * CSS could override, so the partial now inlines the same glyph with
 * fill="currentColor" and both rules below drive everything: the
 * container color reaches the svg and the inheriting heading-style-h5,
 * the second rule beats .text-color-muted's own color on the
 * attribution. Same DL removes the hero socials list, so the left
 * column ends at the badge row. --- */
.port-contact-testimonial { color: #1a1f2ba6; }
.port-contact-testimonial .text-color-muted { color: #1a1f2ba6; }

/* --- DL-099: EN|GR switcher legibility on the light chrome. webflow.css
 * styles the switcher for the dark home hero only (active cream #faf3e6,
 * not-active navy) because the template never showed it elsewhere; inserted
 * into the light navbar it painted cream on cream. Scoping mirrors DL-064:
 * .header:not(.absolute) is the light chrome (home is untouched, shipped
 * treatment), and the >=1440 non-contact block restores an inverse
 * treatment for the DL-064 navy band, where the not-active navy would
 * otherwise vanish into the band exactly. Alpha steps of the two chrome
 * inks (#1a1f2b, #faf3e6), no new palette values. --- */
.header:not(.absolute) .switcher-block .lang-link-active,
.header:not(.absolute) .switcher-block .lang-link-active:hover { color: #1a1f2b; }
.header:not(.absolute) .switcher-block .lang-link-not-active { color: rgba(26, 31, 43, 0.4); }
.header:not(.absolute) .switcher-block .lang-link-not-active:hover { color: #1a1f2b; }
.header:not(.absolute) .switcher-block .barline { color: rgba(26, 31, 43, 0.4); }
@media screen and (min-width: 1440px) {
  body:not(:has(.section-contact-hero)) .header:not(.absolute) .switcher-block .lang-link-active,
  body:not(:has(.section-contact-hero)) .header:not(.absolute) .switcher-block .lang-link-active:hover { color: #faf3e6; }
  body:not(:has(.section-contact-hero)) .header:not(.absolute) .switcher-block .lang-link-not-active { color: rgba(250, 243, 230, 0.55); }
  body:not(:has(.section-contact-hero)) .header:not(.absolute) .switcher-block .lang-link-not-active:hover { color: #faf3e6; }
  body:not(:has(.section-contact-hero)) .header:not(.absolute) .switcher-block .barline { color: rgba(250, 243, 230, 0.55); }
}

/* --- DL-109: "Get In Touch" filled beige on the dark navbar band (client
 * direction 2026-07-31). Scope mirrors DL-064/DL-099: >=1440 non-contact
 * light chrome, where the navy .button.primary (#1a1f2b on the navy body)
 * had no fill contrast. Tokens are re-pointed to the template's own
 * .button.primary-inverse recipe (#faf3e6 fill, #1a1f2b label, no
 * hover-color shift; the dual-text slide remains the hover affordance),
 * declared on the .navbar-right-button cell so webflow.css's own base,
 * hover and pressed rules all resolve through them, no specificity
 * override of the state rules. Home chrome (.absolute, "Let's Talk") and
 * contact (cream body, dark treatment, DL-061) untouched. --- */
@media screen and (min-width: 1440px) {
  body:not(:has(.section-contact-hero)) .header:not(.absolute) .navbar-right-button {
    --button-primary--border-color: var(--button-primary-inverse--border-color);
    --button-primary--background: var(--button-primary-inverse--background);
    --button-primary--text-color: var(--button-primary-inverse--text-color);
    --button-primary--border-color-hover: var(--button-primary-inverse--border-color-hover);
    --button-primary--background-hover: var(--button-primary-inverse--background-hover);
    --button-primary--text-color-hover: var(--button-primary-inverse--text-color-hover);
    --button-primary--border-color-pressed: var(--button-primary-inverse--border-color-pressed);
    --button-primary--background-pressed: var(--button-primary-inverse--background-pressed);
    --button-primary--text-color-pressed: var(--button-primary-inverse--text-color-pressed);
  }
}

/* --- DL-114: mega-menu Contact subtitle legibility. The role line under
 * Joanna's name ships .text-color-muted-inverse, a hard
 * --_global-colors--text-muted-inverse (#e6d5b8b8, light tan at 72%). That
 * only ever suited the dark panel: the two navbars use different dropdown
 * variants. navbar-light.html carries .navbar-dropdown-inner.primary (navy
 * #1a1f2b panel, colour flipped to text-inverse cream); navbar.html carries
 * the plain variant (cream #faf3e6 panel, colour inherited from .body, navy
 * #1a1f2b). On the cream panel the shipped tan measures 1.19:1, effectively
 * invisible. Inheriting instead of pinning makes the subtitle track whatever
 * the panel already gives its sibling .heading-style-h6 name, navy on cream
 * and cream on navy, muted to .72 so the name keeps its hierarchy: 6.1:1 on
 * cream, 8.3:1 on navy, up from 1.19:1 and 6.5:1. Font-family, size, casing
 * and tracking are untouched; the classes stay on the element and only the
 * colour is overridden. --- */
.navbar-contact .text-color-muted-inverse {
  color: inherit;
  opacity: .72;
}

/* --- DL-120: About Joanna section re-laid out to design/about-section-new-
 * layout.png (client mockup, layout only). The section drops out of the
 * 250px/1fr .about-about-grid rail entirely: .about-team-grid-right is now
 * the direct child of the container and stacks three full-width blocks,
 * intro row, rule, portrait row. Both rows reuse .about-team-grid-right-top
 * (1fr 1fr, 4rem gap, 2rem at <=991, stacks at <=767), so the portrait went
 * from one of three .about-team-grid-right-bottom tracks to half the
 * container and the meta moved out from under the image into the second
 * track. Measured off the mockup with pngjs, not eyeballed: content band
 * x100-1391 (1292 wide), intro columns 571/673 with a 48px gutter, portrait
 * row 637/603 with a 52px gutter, portrait block 635x550. The two rows are
 * built 1fr/1fr because neither mockup ratio (0.85 and 1.06) is a template
 * value and the portrait row is within 3% of even; the deviation is a few
 * percent of column width and buys zero new grid CSS. Only three things the
 * shipped classes cannot express are declared here.
 * 1. aspect-ratio pins the slot to the mockup's measured 635x550 (1.1545).
 *    The shipped slot has no height at all, it takes the image's own ratio
 *    (the DL-107 situation), so at half width the 0.703 portrait placeholder
 *    would render 900px tall and the meta column would float in the void.
 * 2. The image goes absolute so its height resolves against an
 *    aspect-ratio-derived box rather than a percentage of auto.
 * 3. align-items centres the meta against the image, as the mockup has it
 *    (meta band 504-863, centre 683, against the image centre 701). Guarded
 *    to >=768 because webflow.css turns the row into a column flex below
 *    that, where align-items:center would shrink both blocks to content
 *    width instead. margin-bottom on the wrapper is zeroed so centring is
 *    not thrown off by half of the shipped 1rem.
 * NOT taken from the mockup, all flagged to the client: the serif headings
 * and the ~71px heading size (mockup styling, the shipped h2 scale stands),
 * the Twitter glyph (DL-079 put LinkedIn there site-wide), the bronze social
 * circles (declined 2026-07-31, the mockup's bronze is a whole-palette
 * recolour and not a socials treatment, so .badge.inverse ships as is), the
 * top eyebrow set inline with its rule (the shipped stacked idiom is kept,
 * and is what the mockup itself uses for the lower meta eyebrow), and the
 * 31px dash under the name.
 * 4. object-position moves the cover crop off her face. joanna-photo.webp is
 *    1024x1024 square in a 1.1545 slot, so cover drops 137px, 13.39% of its
 *    height. Her hair starts 64px down, and the default 50% would take 68.5px
 *    off the top and clip her head by 5px. The mockup's own portrait carries
 *    4.36% headroom, measured the same way; 18% puts 24.7px on the top and
 *    112.4px on the bottom, coat rather than face, and lands headroom at
 *    4.43%. Derived from those two measurements, not chosen by eye. --- */
@media screen and (min-width: 768px) {
  .port-about-joanna-card { align-items: center; }
}
.port-about-joanna-card .about-team-item-image-wrapper {
  aspect-ratio: 635 / 550;
  margin-bottom: 0;
}
.port-about-joanna-card .image-cover-parallax {
  position: absolute;
  inset: 0;
  object-position: 50% 18%;
}

/* --- DL-121: DL-120's full-width rule between intro and portrait deleted
 * (client 2026-07-31), and both eyebrow rules returned to the shipped short
 * treatment. There is no short-rule class to return them to: .line has
 * exactly two rules in webflow.css (.line and .line.inverse), neither with a
 * width variant, and all 73 .line instances across the partials carry the
 * identical inline width:100%. The short look is a property of the PARENT,
 * not the rule. The template puts every eyebrow in a 250px rail track, and
 * `250px 1fr` appears 19 times in webflow.css across the whole site, home,
 * about, services, case, blog, contact, faq, careers, licensing and
 * changelog grids, so 250px is the shipped rail width and not an invented
 * number. DL-120 took THE PRACTICE out of that rail, which is what made its
 * rule run the full intro column; capping the eyebrow block restores the
 * shipped length without undoing the approved layout. Applied to the meta
 * eyebrow too so the two match, which they did not before DL-120 either:
 * THE PRACTICE sat in the 250px rail while STRATEGIC HR sat in a third of
 * the right column, about 300px. max-width rather than width so the block
 * still shrinks below 250px on narrow viewports. Deleting the rule also
 * retires its wrapper div and the margin-bottom margin-large under it, so
 * the intro to portrait gap is once again the 6rem .about-team-grid-right
 * flex gap alone, the template's own rhythm for that stack. --- */
.port-eyebrow-rail { max-width: 250px; }

/* --- DL-095: privacy policy page, new-build legal document, no reference.
 * container-small (50rem) reading column, content in the webflow.css
 * text-rich-text idiom. Section subheadings are semantic h2 sized by
 * heading-style-h4 (the bare 3.25vw h2 overpowers a legal page). Only
 * override: the rich-text h2 2rem margin-top widened so the twelve
 * numbered sections separate clearly on a long continuous page. --- */
.port-privacy .text-rich-text h2 { margin-top: 3.5rem; }
