/* ==========================================================================
   themes/portfolio.css — Apex Holding
   PURPOSE: The founder's portfolio world only — its layout and its decoration.
   A drafting studio at night: graphite ground, blueprint rules, lines that
   draw on, measured fades, zero bounce.

   SCOPE RULES OBSERVED (see tokens.css §4):
   - This file NEVER redefines a --w-* semantic token. tokens.css owns those;
     [data-world="portfolio"] already supplies graphite / blueprint blue /
     drafting white to every shared component on the page.
   - It consumes only --w-* semantic tokens and the global scales. No raw hex.
   - It adds constructs that exist nowhere else on the site, and retunes
     exactly ONE shared component: .stat__value moves from the display face to
     the utility face in §5, because in this world the figures are readings off
     a drawing rather than headlines. That override is scoped under .numbers so
     it cannot reach a .stat anywhere else.

   INVENTORY
     .founder-intro   §1  name, role, restrained portrait, blueprint field
     .philosophy      §2  the four-discipline intersection + the standard
     .scope           §3  the drawn scope line and its five stops
     .ventures        §4  the eleven-company constellation
     .numbers         §5  the three figures, set in the utility face
     .return          §6  the single way back to the portal
                      §7  narrow viewports
                      §8  reduced motion

   JS CONTRACT — assets/js/worlds/portfolio.js writes exactly one custom
   property and four state classes into this file:
     --scope-progress   0 → 1 on [data-scope], drives the rail's dash offset
     .is-driven         on [data-scope], added only once the scope controller
                        has confirmed it will actually run. Every faint or
                        undrawn state in §3 hangs off this class, so the page
                        is complete with no script, under reduced motion, and
                        even if this module fails to import.
     .is-reached        on a [data-scope-stop] the drawn line has passed
     .is-quiet          on [data-ventures] while one node is engaged
     .is-engaged        on the engaged [data-venture]

   Loaded LAST, after motion.css.
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. INTRO
   The page opens quietly. This is deliberately not a full-height hero and the
   portrait is deliberately small — the visitor chose to be curious, they do
   not need to be sold to.
   -------------------------------------------------------------------------- */

.founder-intro {
  overflow: hidden;
}

/* The blueprint field: a 32px working grid with a 160px major grid over it,
   fading out downward so the section dissolves into the page rather than
   ending on a hard edge. Decorative, behind everything, never interactive. */
.founder-intro::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right, color-mix(in srgb, var(--w-accent) 22%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in srgb, var(--w-accent) 22%, transparent) 1px, transparent 1px),
    linear-gradient(to right, color-mix(in srgb, var(--w-accent) 9%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in srgb, var(--w-accent) 9%, transparent) 1px, transparent 1px);
  background-size: 10rem 10rem, 10rem 10rem, 2rem 2rem, 2rem 2rem;
  background-position: center top;
  opacity: 0.55;
  -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 20%, rgba(0, 0, 0, 0) 92%);
  mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 20%, rgba(0, 0, 0, 0) 92%);
}

/* An elevation study, pinned to the right of the field. Purely decorative,
   absolutely positioned so it can never introduce layout shift. */
.founder-intro__study {
  position: absolute;
  z-index: 0;
  inset-block-start: calc(var(--header-h) + var(--space-6));
  inset-inline-end: calc(var(--gutter) * -1);
  inline-size: 26rem;
  block-size: auto;
  color: var(--w-accent);
  opacity: 0.14;
  pointer-events: none;
}

.founder-intro__inner {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: var(--space-9);
}

.founder-intro__lede > * + * {
  margin-block-start: var(--space-5);
}

.founder-intro__name {
  font-size: var(--step-6);
  line-height: var(--lh-tight);
}

/* The one-line role. Utility face so it reads as a caption on a drawing
   rather than as a second headline. */
.founder-intro__role {
  font-family: var(--font-utility);
  font-size: var(--step-0);
  letter-spacing: var(--track-wide);
  color: var(--w-accent-soft);
}

.founder-intro__note {
  max-inline-size: 46ch;
  color: var(--w-text-dim);
}

/* Portrait: the mount matches the photograph's own shape, so the picture fills
   it edge to edge with nothing showing down the sides.

   The delivered file is 200x200 — a square crop. A 4/3 mount left a band of the
   drawn plate at each side, which read as though the photograph had been
   trimmed to fit. Squaring the frame removes that entirely.

   Width is held just above the file's 200px so the upscale is ~1.04x, which is
   imperceptible. Do not enlarge this further without a higher-resolution
   original: past roughly 260px a 200px source visibly softens, badly so on a
   high-DPI screen. */
.founder-intro__portrait {
  inline-size: min(13rem, 22vw);
}

/* The frame owns the ratio, so the space is held open before the photograph
   loads, while it loads, and for good if it never arrives. Nothing here can
   shift the page. The blueprint hatch and the hairline make it read as a
   drawn plate rather than an empty slot. */
.founder-intro__frame {
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--w-accent) 34%, transparent);
  border-radius: var(--radius-sm);
  background-color: var(--w-surface);
  background-image:
    linear-gradient(to right, color-mix(in srgb, var(--w-accent) 10%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in srgb, var(--w-accent) 10%, transparent) 1px, transparent 1px);
  background-size: 2rem 2rem;
  background-position: center center;
}

/* The drawn plate: the frame's backing, and its whole content whenever the
   photograph is unavailable. Decorative — the figcaption carries the meaning. */
.founder-intro__plate {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  color: var(--w-accent);
  opacity: 0.4;
}

/* *** DO NOT SIMPLIFY THIS SELECTOR ***
   base.css carries `img[width][height] { height: auto }` as its CLS safety net
   at specificity (0,2,1). block-size IS height, so a lone `.founder-intro__photo`
   at (0,1,0) would silently lose and the portrait would render at its intrinsic
   height, overflowing the frame. Two classes plus the type selector ties that
   specificity, and this file loads after base.css, so it wins on source order. */
.founder-intro__frame > img.founder-intro__photo {
  position: absolute;

  /* Fills the mount edge to edge. The frame is square and so is the file, so
     there is no band of plate at any side and nothing reads as trimmed. The
     upscale from 200px to the frame's ~208px is ~1.04x — imperceptible.
     If a higher-resolution original arrives, only the width/height attributes
     on the <img> need correcting; this rule already scales to the frame. */
  inset: 0;
  inline-size: 100%;
  block-size: 100%;

  /* CONTAIN, NOT COVER. The delivered file is a seated corporate portrait of
     unknown exact ratio. `cover` would trim whichever axis is long — under the
     4/5 frame this replaces, a landscape source lost more than half its width,
     which is precisely the "photos half cut" fault being repaired. `contain`
     fits the whole photograph inside the mount, so nothing is cropped, at any
     source ratio and any viewport width. Any ratio difference shows as a band
     of the drawn mount at the edges, which is a designed margin, not a gap. */
  object-fit: contain;
  object-position: 50% 50%;

  /* GRACEFUL DEGRADATION WITHOUT SCRIPT. The file may not be on disk yet. A
     failed <img> renders its alt text — and, in Chromium and WebKit, a broken
     glyph inline before it — as ordinary inline content of this box. Indenting
     that line out of the box and clipping it means a missing file paints
     literally nothing and the drawn plate below is what the frame shows. A
     loaded photograph is replaced content with no inline text, so none of the
     three declarations can affect it. The alt attribute is untouched, so it is
     still exposed to assistive technology, and the figcaption names the sitter
     visually either way. No JS, no onerror, no broken-image icon. */
  overflow: hidden;
  text-indent: -200%;
  white-space: nowrap;
}

.founder-intro__portrait figcaption {
  text-align: end;
}


/* --------------------------------------------------------------------------
   2. PHILOSOPHY
   The intersection is set large in the display face and separated by drawn
   multiplication marks, so it reads as one expression rather than four words.
   -------------------------------------------------------------------------- */

.philosophy__intersection {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3) var(--space-5);
  margin-block-end: var(--space-10);
}

.philosophy__term {
  font-family: var(--font-display);
  font-size: var(--step-4);
  line-height: var(--lh-snug);
  letter-spacing: var(--track-tight);
  color: var(--w-text);
}

/* The × between terms. Content-generated because it is punctuation between
   list items, not a word a screen reader should announce. */
.philosophy__term + .philosophy__term::before {
  content: "×";
  display: inline-block;
  margin-inline-end: var(--space-5);
  font-family: var(--font-utility);
  font-size: var(--step-1);
  color: var(--w-accent);
  vertical-align: 0.15em;
}

/* The standard the studio measures itself against. A two-column ledger:
   ruled key, plain value. */
.philosophy__standard {
  max-inline-size: var(--maxw-content);
  border-block-start: 1px solid var(--w-line);
}

.philosophy__standard-item {
  display: grid;
  grid-template-columns: minmax(0, 14rem) minmax(0, 1fr);
  align-items: baseline;
  gap: var(--space-3) var(--space-6);
  padding-block: var(--space-5);
  border-block-end: 1px solid var(--w-line);
}

.philosophy__standard-key {
  font-family: var(--font-utility);
  font-size: var(--step--1);
  letter-spacing: var(--track-widest);
  text-transform: uppercase;
  color: var(--w-accent-soft);
}

.philosophy__standard-value {
  color: var(--w-text);
}


/* --------------------------------------------------------------------------
   3. THE SCALE OF THE WORK

   Geometry: .scope__body is the positioning context. The rail SVG is pinned
   to the full height of the stop list and centred inside a fixed-width first
   column; every stop repeats that same first column, so each marker lands
   exactly on the line at every viewport width with no measurement in JS.

   The draw: the line path carries pathLength="1", so a dash array of 1 is the
   whole line and the offset is simply 1 − progress. No getTotalLength(), no
   resize recalculation, correct at any height.

   NOTE ON THE TRANSFORM-ONLY RULE: stroke-dashoffset is not a transform, and
   motion.css reserves it for one-shot entrances. This is the documented
   exception — it is the single property the world's whole motion idea rests
   on, it is written once per frame to ONE path, it triggers paint on a 4px
   sliver and never layout, and portfolio.js drives it only while the section
   is on screen and never at all under reduced motion.

   RESTING STATE: the line is drawn and every stop is at full strength. The
   undrawn, dimmed state is scoped to .is-driven, which portfolio.js adds ONLY
   once it has confirmed it will actually run — motion allowed, observer
   present. So no-JS, reduced motion, and a portfolio.js that failed to import
   all land on the same complete page, and nothing can ever be left faint with
   no controller alive to un-faint it. This is stricter than gating on .js,
   which would survive the module failing to load.
   -------------------------------------------------------------------------- */

.scope__body {
  --scope-rail-w: 3rem;
  /* Must equal the rail viewBox width in px, so the horizontal scale factor is
     exactly 1 and the 2-unit stroke renders as a true 2px hairline. */
  --scope-rail-box: 4px;
  --scope-progress: 1;

  position: relative;
}

.scope__body.is-driven {
  --scope-progress: 0;
}

.scope__rail {
  position: absolute;
  inset-block: 0;
  inset-inline-start: calc((var(--scope-rail-w) - var(--scope-rail-box)) / 2);
  inline-size: var(--scope-rail-box);
  block-size: 100%;
  pointer-events: none;
}

.scope__rail-track {
  stroke: var(--w-line);
}

.scope__rail-line {
  stroke: var(--w-accent);
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--scope-progress));
}

.scope__stops {
  display: grid;
  gap: var(--space-9);
}

/* Each stop repeats the rail column, so the marker is always on the line. */
.scope__stop {
  display: grid;
  grid-template-columns: var(--scope-rail-w) minmax(0, 1fr);
  align-items: start;
  column-gap: var(--space-5);
  row-gap: var(--space-2);
  grid-template-areas:
    "marker index"
    "marker name"
    "marker line";
  padding-block: var(--space-1);
  border-radius: var(--radius-sm);
}

/* Dimmed until the drawn line reaches it. Controller-scoped, opacity only. */
.scope__body.is-driven .scope__stop {
  opacity: 0.4;
  transition: opacity var(--dur-reveal) var(--ease-out-soft);
}

.scope__body.is-driven .scope__stop.is-reached,
.scope__body.is-driven .scope__stop:hover,
.scope__body.is-driven .scope__stop:focus-within {
  opacity: 1;
}

/* The node on the line. A drafting square, filled at rest and hollowed out
   only while the controller is drawing towards it. */
.scope__marker {
  grid-area: marker;
  justify-self: center;
  align-self: start;
  inline-size: 0.875rem;
  block-size: 0.875rem;
  margin-block-start: 0.35rem;
  border: 1px solid var(--w-accent);
  background-color: var(--w-accent);
  transform: rotate(45deg);
  transition: background-color var(--dur-reveal) var(--ease-out-soft);
}

.scope__body.is-driven .scope__marker {
  background-color: var(--w-base);
}

.scope__body.is-driven .scope__stop.is-reached .scope__marker,
.scope__body.is-driven .scope__stop:focus-within .scope__marker {
  background-color: var(--w-accent);
}

.scope__index {
  grid-area: index;
  font-family: var(--font-utility);
  font-size: var(--step--1);
  letter-spacing: var(--track-widest);
  color: var(--w-accent-soft);
}

.scope__name {
  grid-area: name;
  font-size: var(--step-3);
}

.scope__line {
  grid-area: line;
  max-inline-size: 52ch;
  color: var(--w-text-dim);
}


/* --------------------------------------------------------------------------
   4. THE VENTURES CONSTELLATION
   Eleven nodes on a faint blueprint field. Three carry a real link; the other
   eight are readable nodes with nothing to activate, made keyboard-reachable
   with tabindex in the markup and arrow-key movement from portfolio.js.
   -------------------------------------------------------------------------- */

/* The same blueprint field as the intro, at half strength and feathered at
   both ends, so the map reads as a second sheet on the same drafting table. */
.ventures {
  overflow: hidden;
}

.ventures::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right, color-mix(in srgb, var(--w-accent) 14%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in srgb, var(--w-accent) 14%, transparent) 1px, transparent 1px);
  background-size: 10rem 10rem;
  background-position: center center;
  opacity: 0.5;
  -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 30% 70%, rgba(0, 0, 0, 0));
  mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 30% 70%, rgba(0, 0, 0, 0));
}

.ventures .section__inner {
  position: relative;
  z-index: 1;
}

.ventures__grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
}

.venture {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: var(--space-2);
  min-block-size: 7.5rem;
  padding: var(--space-5);
  border: 1px solid var(--w-line);
  border-radius: var(--radius-sm);
  background-color: var(--w-surface);
  transition:
    opacity var(--dur-micro) var(--ease-out-soft),
    border-color var(--dur-micro) var(--ease-out-soft),
    background-color var(--dur-micro) var(--ease-out-soft);
}

/* Registration tick in the corner of every node — a drawing's origin mark. */
.venture::before {
  content: "";
  position: absolute;
  inset-block-start: var(--space-4);
  inset-inline-start: var(--space-4);
  inline-size: 0.625rem;
  block-size: 0.625rem;
  border-block-start: 1px solid var(--w-accent);
  border-inline-start: 1px solid var(--w-accent);
  opacity: 0.5;
  transition: opacity var(--dur-micro) var(--ease-out-soft);
}

.venture:hover,
.venture:focus-within,
.venture.is-engaged {
  border-color: var(--w-accent);
  background-color: var(--w-surface-2);
}

.venture:hover::before,
.venture:focus-within::before,
.venture.is-engaged::before {
  opacity: 1;
}

/* Quiet the rest of the map while one node is engaged. .is-quiet exists only
   while portfolio.js is running, and it is opacity only — nothing is hidden,
   nothing reflows, and every node is still fully readable at 0.45. */
.ventures__grid.is-quiet .venture:not(.is-engaged) {
  opacity: 0.45;
}

.venture__name {
  font-family: var(--font-display);
  font-size: var(--step-1);
  line-height: var(--lh-snug);
  letter-spacing: var(--track-tight);
  color: var(--w-text);
}

/* The three nodes that open a world. The whole node is the target. */
.venture--linked {
  border-inline-start: 2px solid var(--w-accent);
}

.venture__link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

/* The stretched link means the browser would draw its focus ring around the
   text alone. Lift it onto the whole node so the ring matches the target. */
.venture--linked:has(.venture__link:focus-visible) {
  outline: 2px solid var(--w-focus);
  outline-offset: 2px;
}

.venture__go {
  font-family: var(--font-utility);
  font-size: var(--step--1);
  letter-spacing: var(--track-wide);
  color: var(--w-accent-soft);
}

.venture__go::after {
  content: " →";
  display: inline-block;
  transform: translateX(0);
  transition: transform var(--dur-micro) var(--ease-out-soft);
}

.venture--linked:hover .venture__go::after,
.venture--linked:focus-within .venture__go::after {
  transform: translateX(4px);
}

.venture__region {
  font-family: var(--font-utility);
  font-size: var(--step--1);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--w-text-dim);
}

.ventures__footnote {
  margin-block-start: var(--space-7);
  color: var(--w-text-dim);
  font-size: var(--step--1);
}


/* --------------------------------------------------------------------------
   5. THE NUMBERS
   The shared .stat component, retuned to the utility face for this world —
   these are readings off a drawing, not display headlines.
   -------------------------------------------------------------------------- */

.numbers__grid {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
}

.numbers .stat {
  border-block-start-color: var(--w-accent);
}

.numbers .stat__value {
  font-family: var(--font-utility);
  font-size: var(--step-3);
  letter-spacing: var(--track-tight);
}


/* --------------------------------------------------------------------------
   6. RETURN
   One way out. Centred, unhurried, with a hairline above it.
   -------------------------------------------------------------------------- */

.return__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-7);
  padding-block-start: var(--space-9);
  border-block-start: 1px solid var(--w-line);
  text-align: center;
}

.return__title {
  font-size: var(--step-3);
  color: var(--w-text-dim);
}


/* --------------------------------------------------------------------------
   7. NARROW VIEWPORTS
   The portrait drops below the name, the ledger becomes single-column, and
   the scope rail tightens so the stop copy keeps its measure.
   -------------------------------------------------------------------------- */

@media (max-width: 62em) {
  .founder-intro__study {
    inline-size: 18rem;
    opacity: 0.1;
  }
}

@media (max-width: 52em) {
  .founder-intro__inner {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    gap: var(--space-8);
  }

  /* Stacked under the name, so it may take a wider measure — still capped well
     short of the text column. The ratio and the contain fit are unchanged, so
     the face is no more croppable here than at full width. */
  .founder-intro__portrait {
    inline-size: min(22rem, 78vw);
  }

  .founder-intro__portrait figcaption {
    text-align: start;
  }

  .philosophy__standard-item {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-1);
  }

  .scope__body {
    --scope-rail-w: 2rem;
  }

  .scope__stop {
    column-gap: var(--space-4);
  }

  .scope__stops {
    gap: var(--space-8);
  }
}

@media (max-width: 40em) {
  .founder-intro__study {
    display: none;
  }

  .venture {
    min-block-size: 6.5rem;
  }
}


/* --------------------------------------------------------------------------
   8. REDUCED MOTION
   Most of this world needs no override at all: portfolio.js never adds
   .is-driven when reduced motion is requested, so the scope line is simply
   already drawn and every stop is already at full strength — the resting
   state above IS the reduced-motion state. The rules below only strip the
   remaining decorative travel.

   The ventures map keeps its engaged and quiet states. Those communicate
   where you are rather than decorate, and motion.css has already collapsed
   their transitions to nothing.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .scope__stop,
  .scope__marker,
  .venture,
  .venture::before,
  .venture__go::after {
    transition: none;
  }

  .venture--linked:hover .venture__go::after,
  .venture--linked:focus-within .venture__go::after {
    transform: none;
  }
}
