*{box-sizing:border-box}
html{scroll-behavior:smooth;overflow-x:hidden}
body{margin:0}

/* ── Design tokens ───────────────────────────────────────────────────────────
   Spacing is a 4pt grid. Radius is a 3-step scale by element size (small
   elements can't carry a large radius and still look deliberate): 4 micro,
   8 cards, 12 media, 999px pills. Use 999px for pills — NOT 50% — so one
   idiom covers both circles and capsules.
   --accent is per-theme by necessity, not preference: the brand teal #20B2AA
   measures 2.42:1 on the light background (WCAG wants 4.5:1 for text, 3:1 for
   UI), and no single teal clears both themes — a teal dark enough for light
   drops to 3.62:1 on dark. Measured, not estimated.
   ──────────────────────────────────────────────────────────────────────────── */
:root, [data-theme="light"]{
  /* color-scheme is what tells the BROWSER which way this page leans, as
     opposed to --bg, which only tells our own CSS. Without it the dark theme
     still gets a light scrollbar, light form controls and a white flash behind
     the canvas during navigation, because the UA has no way to know. It is
     declared per theme rather than as `light dark` so an explicit data-theme
     choice wins over the OS preference — same precedence the rest of the
     theming already follows. */
  color-scheme:light;
  --bg:#F6F6F4;
  --text:#1A1A18;
  --muted:#605F58;
  --line:rgba(26,26,24,0.14);
  --line-soft:rgba(26,26,24,0.07);
  --shot:rgba(26,26,24,0.045);
  --shot-stripe:rgba(26,26,24,0.05);
  --shot-hover:rgba(26,26,24,0.085);
  --surface:#EFEFEC;
  --accent:#0D726D;               /* 5.32:1 on --bg, 4.87:1 on a --shot tinted card */

  --sp-1:4px;   --sp-2:8px;   --sp-3:12px;  --sp-4:16px;  --sp-5:20px;
  --sp-6:24px;  --sp-8:32px;  --sp-10:40px; --sp-11:44px; --sp-12:48px;

  --r-sm:4px; --r-md:8px; --r-lg:12px; --r-pill:999px;
}
[data-theme="dark"]{
  color-scheme:dark;
  --bg:#121212;
  --text:#EDEDEA;
  --muted:#9D9D97;
  --line:rgba(237,237,234,0.16);
  --line-soft:rgba(237,237,234,0.08);
  --shot:rgba(237,237,234,0.04);
  --shot-stripe:rgba(237,237,234,0.055);
  --shot-hover:rgba(237,237,234,0.08);
  --surface:#1B1B1A;
  --accent:#20B2AA;               /* 7.15:1 on --bg */
}

body{
  /* dvh, not vh: on iOS Safari 100vh is the height WITHOUT the address bar, so
     a 100vh min-height leaves the page a bar's worth too tall on first paint
     and settles as the bar retracts. dvh tracks the live viewport instead. */
  min-height:100dvh;
  background:var(--bg);
  color:var(--text);
  font-family:'Instrument Sans',system-ui,sans-serif;
  -webkit-font-smoothing:antialiased;
}
/* The gutter is max(our padding, the notch) rather than the sum: on a phone in
   landscape the safe-area inset can exceed 48px, and adding them would double
   the gutter on exactly the devices with the least width to spare. On every
   device without a notch env() resolves to 0 and the clamp wins unchanged. */
.wrap{
  max-width:1280px;margin:0 auto;
  padding-left:max(clamp(24px,5vw,48px), env(safe-area-inset-left));
  padding-right:max(clamp(24px,5vw,48px), env(safe-area-inset-right));
}
/* Kills the ~300ms tap delay browsers hold in case a second tap makes a
   double-tap zoom. Scoped to things that are actually tapped, so pinch-zoom
   stays available everywhere else — that gesture is an accessibility feature,
   and `touch-action:manipulation` on a scroll container would remove it. */
a, button, summary, [role="button"], input, select, textarea{touch-action:manipulation}

@keyframes fadeUp{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
.view-inline{display:none}
@media (hover:none),(pointer:coarse){
  #cursor-follower{display:none!important}
  .view-inline{display:inline-flex!important}
}
@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto}
  #cursor-follower{display:none!important}
  *{animation-duration:0.001ms!important;transition-duration:0.001ms!important}
}

/* ── Cursor follower ── */
#cursor-follower{position:fixed;top:0;left:0;pointer-events:none;z-index:60;will-change:transform}
#cursor-follower .inner{width:78px;height:78px;margin-left:-39px;margin-top:-39px;border-radius:var(--r-pill);background:var(--text);color:var(--bg);display:flex;align-items:center;justify-content:center;font-family:'Space Mono',monospace;font-size:12px;letter-spacing:0.08em;text-transform:uppercase;transform:scale(0);opacity:0;transition:transform .34s cubic-bezier(.16,1,.3,1),opacity .3s ease}
#cursor-follower.on .inner{transform:scale(1);opacity:1}

/* ── Header / nav ── */
header.site-header{display:flex;align-items:baseline;justify-content:space-between;gap:24px;padding-top:32px;padding-bottom:24px;flex-wrap:wrap}
header.site-header a.logo{min-height:44px;display:inline-flex;align-items:center;text-decoration:none;color:var(--text);font-family:'Bricolage Grotesque',sans-serif;font-weight:700;font-size:17px;letter-spacing:-0.01em}
header.site-header nav{display:flex;gap:clamp(16px,3vw,40px);align-items:baseline}
/* min-WIDTH as well as min-height: "Work" laid out at 38px wide, so the target
   was 38x44 and short of the 44x44 floor on the narrow axis. Short labels are
   centred in the wider box, so only the two shortest links move at all. */
header.site-header nav a{text-decoration:none;color:var(--text);font-size:15px;font-weight:500;opacity:0.85;min-height:44px;min-width:44px;display:inline-flex;align-items:center;justify-content:center}
header.site-header nav .loc{font-family:'Space Mono',monospace;font-size:12px;color:var(--muted);letter-spacing:0.04em}
.rule{height:1px;background:var(--line);width:100%}

/* ── Breadcrumb ── */
.breadcrumb{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-family:'Space Mono',monospace;font-size:12px;letter-spacing:0.04em;margin-top:clamp(28px,5vw,44px)}
/* "Home" is only 31px wide, so min-height alone left a 31x44 target. */
.breadcrumb .bc-link{color:var(--muted);text-decoration:none;min-height:44px;min-width:44px;display:inline-flex;align-items:center;justify-content:center}
.breadcrumb .bc-link:hover{color:var(--text)}
.breadcrumb .bc-current{color:var(--text)}
.breadcrumb .bc-sep{color:var(--line);opacity:0.9}

/* ── Back element ── */
.back-link{display:inline-flex;align-items:center;gap:var(--sp-2);text-decoration:none;color:var(--text);font-size:14px;font-weight:600;margin-top:20px;cursor:pointer;background:none;border:none;padding:var(--sp-3) 0;font-family:'Instrument Sans',system-ui,sans-serif}
.back-link:hover{opacity:0.7}
.back-link .arrow{font-family:'Space Mono',monospace}

/* ── Case study layout ── */
.cs-hero{padding-top:clamp(16px,3vw,28px);padding-bottom:clamp(40px,7vw,72px)}
.cs-kicker{display:flex;align-items:center;gap:12px;margin:28px 0 24px}
.cs-kicker span.label{font-family:'Space Mono',monospace;font-size:12px;color:var(--muted);letter-spacing:0.06em}
.cs-kicker .kicker-rule{flex:1;height:1px;background:var(--line);max-width:120px}
.cs-title{margin:0;font-family:'Bricolage Grotesque',sans-serif;font-weight:800;font-size:var(--fit-size, clamp(40px,8vw,104px));line-height:0.96;letter-spacing:-0.03em}
.cs-intro{margin:24px 0 0;font-size:clamp(17px,2vw,21px);line-height:1.55;color:var(--muted);max-width:60ch}

/* ── Layered hero (headline band over the standard background) ── */
.cs-hero-layered{position:relative;padding-top:clamp(12px,2vw,20px);padding-bottom:clamp(12px,2vw,20px)}
.hero-band{position:relative;width:100%;background:var(--shot-hover);border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:clamp(28px,5vw,48px) 0}
.hero-band .cs-kicker{margin:0 0 20px}
.hero-headline{margin:0;font-family:'Bricolage Grotesque',sans-serif;font-weight:800;font-size:var(--fit-size, clamp(36px,7vw,88px));line-height:0.98;letter-spacing:-0.03em}
.hero-subhead{margin:var(--sp-5) 0 0;font-size:clamp(16px,1.8vw,20px);line-height:1.5;color:var(--muted);max-width:52ch}

.cs-meta{display:grid;grid-template-columns:repeat(4,1fr);gap:24px;margin-top:clamp(32px,5vw,56px);padding-top:20px;border-top:1px solid var(--line)}
.cs-meta-item{grid-column:span 1;min-width:min(100%,140px)}
.cs-meta-item .k{display:block;font-family:'Space Mono',monospace;font-size:11px;color:var(--muted);letter-spacing:0.06em;margin-bottom:8px}
.cs-meta-item .v{line-height:1.5;font-size:15px;font-weight:500}
@media (max-width:800px){.cs-meta{grid-template-columns:repeat(2,1fr)}}

/* padding-bottom was clamp(8px,2vw,16px) against an 80px top — content hugged
   the bottom of its own section and crowded the next section's rule. */
.cs-section{padding-top:clamp(48px,7vw,80px);padding-bottom:clamp(16px,3vw,32px)}
.cs-section-head{display:flex;align-items:baseline;justify-content:space-between;border-top:1px solid var(--text);padding-top:16px;margin-bottom:clamp(28px,4vw,44px)}
.cs-section-head h2{margin:0;font-family:'Bricolage Grotesque',sans-serif;font-weight:700;font-size:clamp(22px,3vw,32px);letter-spacing:-0.02em}
.cs-section-head .idx{font-family:'Space Mono',monospace;font-size:12px;color:var(--muted);letter-spacing:0.04em}
.cs-body{margin:0;font-size:clamp(16px,1.6vw,18px);line-height:1.65;color:var(--muted);max-width:64ch}

/* ── In-content links ────────────────────────────────────────────────────────
   Nothing styled these, so every link inside prose fell through to the UA
   default #0000EE / visited #551A8B. On the dark background those measure
   1.99:1 and 1.84:1 — a WCAG failure on the very links that point at the
   shipped product ("dagwood.com.pk", "Dagwood on Google Play").

   :not([class]) rather than a list of parent selectors: every piece of site
   chrome that is a link already carries a class (.logo, .np-link, .bc-link,
   .diary-jump-link, .btn-primary, .f-email), and the header/footer sit outside
   <main>, so this reaches in-content links only and keeps doing so as new
   sections are added. --accent is theme-aware and already contrast-checked
   (5.32:1 light, 7.15:1 dark — and it was darkened from #0F7A74 precisely
   because these links sit on --shot tinted cards, where 4.78:1 fell to 4.38:1).
   The underline carries the affordance so colour is
   not the only signal. ──────────────────────────────────────────────────────── */
main a:not([class]){color:var(--accent);text-decoration:underline;
  text-decoration-thickness:1px;text-underline-offset:3px;
  transition:color .2s ease}
main a:not([class]):hover{color:var(--text)}
main a:not([class]):visited{color:var(--accent)}
@media (prefers-reduced-motion:reduce){main a:not([class]){transition:none}}

/* ── Vertical rhythm ─────────────────────────────────────────────────────────
   .cs-body previously had no margin reset, so it carried the UA default
   `margin-block:1em` (~18px at desktop). That unauthored value sat in every gap
   on the page and collided with every authored one — the numbers in the source
   were never the numbers on screen. Reset above, then drive rhythm from here.

   The governing number: .cs-body is 18px/1.65 = 29.7px of leading. Any gap
   below that makes a paragraph break read as TIGHTER than the space between two
   lines within a paragraph, which is what read as "squished". Every step below
   clears it.

   Paragraph<->media joins were previously 20px, 24px, or a clamp — three values
   for structurally identical boundaries, all scattered as inline styles on the
   markup. They are declared once here instead.
   ──────────────────────────────────────────────────────────────────────────── */
.cs-body + .cs-body{margin-top:var(--sp-8)}                    /* 32px */

.cs-body + .diary-pair,   .cs-body + .tag-grid,
.cs-body + .bento-grid,   .cs-body + .process-timeline,
.cs-body + .brand-grid,   .cs-body + .subsystems,
.cs-body + .diary-carousel,
.diary-pair + .cs-body,   .tag-grid + .cs-body,
.bento-grid + .cs-body,   .process-timeline + .cs-body,
.brand-grid + .cs-body,   .subsystems + .cs-body,
.component-actions + .cs-body,
.diary-pair + .diary-pair,.diary-pair + .tag-grid,
.tag-grid + .diary-pair,  .process-timeline + .diary-pair,
.component-actions + .diary-pair,
/* .flow-chart and .component-mosaic set their own margin-top, so they were not
   caught by the joins above — .flow-chart + .cs-body measured 0px. */
.flow-chart + .cs-body,   .component-mosaic + .cs-body,
.legacy-gallery + .cs-body,
.diary-jump-link + .cs-body,
/* Dagwood section 07 joins onto the same rhythm scale. */
.cs-body + .dag-mark-block, .dag-mark-block + .cs-body,
.cs-body + .dag-type,     .dag-type + .cs-body,
/* Lucrum section 01: the client roster joins below the bento grid. */
.bento-grid + .client-roster, .client-roster + .cs-body{margin-top:var(--sp-10)}  /* 40px */

.tag-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}
.tag-chip{grid-column:span 1;border:1px solid var(--line);border-radius:var(--r-md);padding:var(--sp-5);font-size:14px;font-weight:500;line-height:1.5;transition:border-color .2s ease}
.tag-chip:hover{border-color:var(--accent)}
@media (prefers-reduced-motion:reduce){.tag-chip{transition:none}}
@media (max-width:800px){.tag-grid{grid-template-columns:repeat(2,1fr)}}
/* 3-up variant for the research insight cards. These hold 3-5 sentences each,
   not a short chip, so they stack fully on phones. This was an inline
   grid-template-columns on the element, which beat every media query no matter
   the viewport and left three ~110px columns of two-word lines on a phone.
   These three rules must stay AFTER the .tag-grid rule above so the cascade
   resolves in this order at each width. */
.tag-grid-3{grid-template-columns:repeat(3,1fr)}
@media (max-width:800px){.tag-grid-3{grid-template-columns:repeat(2,1fr)}}
@media (max-width:640px){.tag-grid-3{grid-template-columns:1fr}}

/* ── User flow chart: home screen branching into every module and action.
     Rendered as a real flow chart (Figma export, recolored to a single
     neutral tone per theme, no branch color-coding) rather than a nested
     list, which read too much like a nav dropdown. Wrapped in a horizontal
     scroller with a min-width floor so labels stay legible on narrow
     screens instead of shrinking to illegibility. ── */
.flow-chart{margin-top:var(--sp-10);overflow-x:auto;padding-bottom:8px;-webkit-overflow-scrolling:touch}
/* height:auto is required now that the <img> carries width/height attributes:
   those are presentational hints and would otherwise fight width:100% and
   distort the diagram. Same rule as the mosaic tiles. */
.flow-chart-img{display:block;width:100%;min-width:760px;height:auto}
/* The .flow-chart-light/.flow-chart-dark display-swap pair is gone: it kept both
   SVGs in the DOM and display:none does not stop a fetch. One <img> now, src
   swapped by applyThemeAssets() in site.js. */

/* ── Bento grid: problem statement, addressed issues, and metrics composed as one asymmetric layout ── */
.bento-grid{display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:minmax(152px,auto);gap:16px}
.bento-cell{border:1px solid var(--line);border-radius:var(--r-md);padding:clamp(20px,3vw,28px);display:flex;flex-direction:column;justify-content:center}
.bento-cell.problem{grid-column:1/3;justify-content:flex-start;background:var(--shot)}
/* The fixed row placement below describes ONE composition: a double-height
   problem block with the stat beside it and a row of issues underneath. Pages
   that instead want a simple side-by-side row say so with an inline
   `grid-column:span N`, and for those the hardcoded rows are wrong — the cell
   keeps spanning two 152px tracks and renders as a short paragraph floating in a
   half-empty box. Only apply the fixed rows when the cell has not been placed by
   hand. (Page-scoped rules like .oms below carry higher specificity and still
   win.) */
.bento-cell.problem:not([style*="grid-column"]){grid-row:1/3}
.bento-cell.problem .cell-label{display:block;font-family:'Space Mono',monospace;font-size:11px;color:var(--muted);letter-spacing:0.06em;margin-bottom:var(--sp-4)}
.bento-cell.problem p{margin:0;font-size:clamp(16px,1.7vw,19px);line-height:1.6;color:var(--muted)}
.bento-cell.stat{grid-column:3/5;align-items:flex-start}
.bento-cell.stat .stat-num{font-family:'Bricolage Grotesque',sans-serif;font-weight:800;font-size:clamp(36px,5vw,58px);line-height:1;letter-spacing:-0.02em}
.bento-cell.stat .stat-label{margin-top:var(--sp-3);line-height:1.5;font-size:14px;color:var(--muted);max-width:32ch}
.bento-cell.issue{grid-column:span 1}
.bento-cell.issue:not([style*="grid-column"]){grid-row:3/4}
.bento-cell.issue .cell-label{display:block;font-family:'Space Mono',monospace;font-size:11px;color:var(--muted);letter-spacing:0.05em;margin-bottom:var(--sp-3)}
/* Strike drawn in currentColor, not --line: at --line it measured 1.33:1 and read
   as a rendering artefact rather than a deliberate strike — and the strike is the
   only thing distinguishing the old state from the new one. */
.bento-cell.issue .issue-from{display:block;font-size:13px;color:var(--muted);text-decoration:line-through;text-decoration-color:currentColor;text-decoration-thickness:1px}
.bento-cell.issue .issue-to{display:block;margin-top:4px;font-size:15px;font-weight:700}
/* Scoped to .bento-cell, not .bento-cell.issue: .issue-desc is also used inside
   .problem cells (dagwood x4, order-management-system x3, zeny x2), where it was
   matching nothing and so inherited full --text at body size. That inverted the
   hierarchy — the supporting detail rendered brighter and larger than the lead
   line above it, which is muted 16-19px. */
.bento-cell .issue-desc{display:block;margin-top:var(--sp-3);font-size:14px;line-height:1.55;color:var(--muted)}
@media (max-width:800px){
  .bento-grid{grid-template-columns:repeat(2,1fr)}
  .bento-cell.problem{grid-column:1/3}
  .bento-cell.stat{grid-column:span 1}
  .bento-cell.issue{grid-column:span 1}
  /* The :not() qualifier is repeated here on purpose. The base row placement is
     .bento-cell.problem:not([style*="grid-column"]) — specificity 0,3,0 — and a
     media query adds none, so a plain .bento-cell.problem{grid-row:auto} (0,2,0)
     silently loses and the cell keeps spanning two rows into its neighbours.
     Matching the qualifier is what actually lets the stacked layout take over. */
  .bento-cell.problem:not([style*="grid-column"]),
  .bento-cell.stat:not([style*="grid-column"]),
  .bento-cell.issue:not([style*="grid-column"]){grid-row:auto}
}

/* ── Legacy "before" reference: hidden by default, revealed as an overlay
     via the "View old applications" text link so it never sits inline in
     the case-study reading flow. Reuses the same overlay/close visual
     language as the image lightbox for consistency. ── */
.legacy-gallery{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;padding:clamp(16px,4vw,48px);background:rgba(0,0,0,0.82);opacity:0;visibility:hidden;transition:opacity .3s ease}
.legacy-gallery.open{opacity:1;visibility:visible}
.legacy-gallery-grid{display:flex;flex-wrap:wrap;justify-content:center;gap:28px;max-height:86vh;max-width:100%;overflow:auto}
.legacy-gallery-grid figure{margin:0}
.legacy-gallery-grid img{display:block;max-height:65vh;max-width:80vw;width:auto;height:auto;border-radius:var(--r-lg);box-shadow:0 20px 60px rgba(0,0,0,0.4)}
.legacy-gallery-grid figcaption{margin-top:var(--sp-3);line-height:1.5;font-family:'Space Mono',monospace;font-size:13px;color:#EDEDEA;text-align:center}
@media (prefers-reduced-motion:reduce){.legacy-gallery{transition:none}}
@media (max-width:520px){
  .bento-grid{grid-template-columns:1fr}
  .bento-cell.problem,.bento-cell.stat,.bento-cell.issue{grid-column:1/2}
}

/* ── Process timeline: numbered stages zigzagging across a week ruler,
     each with its own cluster of sub-activity tags — a richer "what
     happened, when" breakdown than a plain numbered strip. Arrow curve
     coordinates are hand-tuned to the .pt-node vertical rhythm below;
     if node spacing changes, re-check the arrows still land on the circles. ── */
.process-timeline{position:relative;margin-top:8px}
.pt-ruler{display:flex;border-top:1px dashed var(--line);padding-top:var(--sp-3);margin-bottom:28px}
.pt-ruler span{flex:1;font-family:'Space Mono',monospace;font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:var(--muted);padding:0 12px}
.pt-ruler span:first-child{padding-left:0}
.pt-ruler span:last-child{padding-right:0}
.pt-ruler span:not(:last-child){border-right:1px dashed var(--line)}
.pt-arrows{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;z-index:0}
.pt-arrows path{fill:none;stroke:var(--accent);stroke-width:2;stroke-dasharray:6 5}
.pt-row{position:relative;z-index:1;display:flex;gap:clamp(16px,2.5vw,32px)}
.pt-node{flex:1;min-width:0}
.pt-node--down{margin-top:clamp(128px,17vw,196px)}
.pt-card{position:relative;z-index:1;border:1px solid var(--line);border-radius:var(--r-md);background:var(--surface);padding:clamp(20px,2.2vw,24px)}
.pt-card-top{display:flex;align-items:center;gap:12px;margin-bottom:16px}
.pt-num{display:flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:var(--r-pill);background:var(--text);color:var(--bg);font-family:'Space Mono',monospace;font-size:15px;font-weight:700}
.pt-week{display:none;font-family:'Space Mono',monospace;font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:var(--muted)}
.pt-title{margin:0 0 var(--sp-4);font-family:'Bricolage Grotesque',sans-serif;font-weight:700;font-size:clamp(16px,1.7vw,20px);letter-spacing:-0.01em}
.pt-tags{display:flex;flex-wrap:wrap;gap:8px}
.pt-tags span{border:1px solid var(--line);color:var(--text);border-radius:var(--r-pill);padding:var(--sp-1) var(--sp-3);font-size:12px;font-weight:600;white-space:nowrap}
@media (max-width:800px){
  .pt-arrows{display:none}
  .pt-ruler{display:none}
  .pt-week{display:inline-block}
  .pt-row{flex-direction:column;gap:32px}
  .pt-node{width:100%}
  .pt-node--down{margin-top:0}
  .pt-tags{width:100%}
  .pt-tags span{white-space:normal}
}
@media (max-width:520px){
  .pt-ruler{flex-wrap:wrap;row-gap:var(--sp-2)}
  .pt-ruler span{flex:1 1 50%;border-right:none!important}
}

/* ── Brand swatches: name/hex/rgb overlaid on the color itself, plus a
     tint/shade scale strip per color (via color-mix, no precomputed hex
     needed) so each swatch reads as a small palette rather than one flat
     chip. Text color is chosen per swatch for AA contrast against that
     specific color, not tied to the page's light/dark theme. ── */
.brand-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(20px,3vw,28px)}
.brand-swatch{display:flex;flex-direction:column;gap:var(--sp-2)}
.brand-swatch .chip{position:relative;aspect-ratio:1.3/1;border-radius:var(--r-md);border:1px solid var(--line);background:var(--c);display:flex;flex-direction:column;justify-content:flex-end;padding:clamp(14px,2vw,20px)}
.brand-swatch .sw-name{font-family:'Bricolage Grotesque',sans-serif;font-weight:700;font-size:clamp(15px,1.6vw,19px);letter-spacing:-0.01em}
.brand-swatch .sw-code{font-family:'Space Mono',monospace;font-size:12px;margin-top:var(--sp-3);opacity:0.82}
.brand-swatch .sw-rgb{font-family:'Space Mono',monospace;font-size:11px;opacity:0.82;margin-top:var(--sp-1)}
.brand-scale{display:grid;grid-template-columns:repeat(5,1fr);gap:var(--sp-1)}
.brand-scale span{display:block;aspect-ratio:1/1;border-radius:var(--r-sm);border:1px solid var(--line)}
/* ── Brand showcase: logo lockup + typeface specimen + brand pattern strip,
     modeled on the brand's own reference sheet. No card background/border —
     sits directly on the page. Two monochrome logo variants (dark/white)
     swap based on [data-theme], same as the rest of the site's theming,
     since a single-color logo asset can't invert itself via CSS alone.
     Surrounding text uses theme-aware colors too. ── */
.brand-showcase{margin-top:var(--sp-10)}
.bs-top{display:flex;align-items:flex-start;justify-content:space-between;gap:clamp(20px,4vw,40px);flex-wrap:wrap;padding-bottom:clamp(24px,4vw,40px)}
/* Single element, src swapped by applyThemeAssets() — same reason as the flow
   chart: the display:none pair fetched both files. */
.bs-logo{height:clamp(44px,6vw,68px);width:auto;display:block}
.bs-type{display:flex;align-items:flex-start;gap:20px}
.bs-aa{font-family:'Bricolage Grotesque',sans-serif;font-weight:800;font-size:clamp(44px,6vw,68px);line-height:1;color:var(--text)}
.bs-type-info{display:flex;flex-direction:column;gap:var(--sp-2)}
.bs-type-name{font-family:'Instrument Sans',system-ui,sans-serif;font-size:14px;font-weight:600;color:var(--text)}
.bs-type-sample{font-family:'Instrument Sans',system-ui,sans-serif;font-size:clamp(13px,1.3vw,16px);line-height:1.5;color:var(--muted)}
.bs-pattern{display:block;width:100%;height:auto;opacity:0.3}
@media (max-width:560px){
  .bs-top{flex-direction:column;align-items:flex-start}
}
@media (max-width:800px){.brand-grid{grid-template-columns:repeat(2,1fr)}}
@media (max-width:560px){
  .brand-grid{grid-template-columns:1fr}
  .brand-swatch .chip{aspect-ratio:2/1}
}

.visual-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(24px,3vw,32px)}
.visual-block{grid-column:span 2;position:relative;aspect-ratio:4/3;border-radius:4px;overflow:hidden;border:1px solid var(--line);background:var(--shot);background-image:repeating-linear-gradient(135deg, var(--shot-stripe) 0, var(--shot-stripe) 1px, transparent 1px, transparent 11px)}
.visual-block.wide{grid-column:span 4;aspect-ratio:16/7}
.visual-block .v-tag{position:absolute;top:16px;left:16px;font-family:'Space Mono',monospace;font-size:12px;font-weight:700}
.visual-block .v-cap{position:absolute;bottom:16px;left:16px;font-family:'Space Mono',monospace;font-size:12px;color:var(--muted)}
@media (max-width:640px){.visual-block{grid-column:span 4}}

/* ── Mockups, alternating large and small placeholders in one strip ── */
.mockup-strip{display:flex;align-items:flex-end;gap:clamp(16px,2.5vw,24px);overflow-x:auto;padding-bottom:8px;-webkit-overflow-scrolling:touch}
.mockup-frame{position:relative;flex-shrink:0;border-radius:4px;overflow:hidden;border:1px solid var(--line);background:var(--shot);background-image:repeating-linear-gradient(135deg, var(--shot-stripe) 0, var(--shot-stripe) 1px, transparent 1px, transparent 11px)}
.mockup-frame.large{width:clamp(220px,26vw,320px);aspect-ratio:9/16}
.mockup-frame.small{width:clamp(160px,19vw,230px);aspect-ratio:4/3}

/* ── Real product mockups, shown as physical hardware (tablet, till stand)
   rather than a browser window, no people, just the device and the screen ── */
.pos-mockup-stack{display:flex;flex-direction:column;gap:clamp(28px,4vw,44px)}
.pos-mockup-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:clamp(28px,4vw,44px)}
@media (max-width:800px){.pos-mockup-grid{grid-template-columns:1fr}}

.device{display:flex;flex-direction:column;align-items:center}
.device-frame{position:relative;width:100%;background:#151515;border-radius:16px;padding:12px 12px 20px;box-shadow:0 30px 60px -28px rgba(0,0,0,0.5)}
.device-frame::before{content:'';position:absolute;top:5px;left:50%;transform:translateX(-50%);width:5px;height:5px;border-radius:50%;background:#3a3a3a}
.device-screen{border-radius:5px;overflow:hidden;background:#fff;line-height:0}
.device-screen img{display:block;width:100%;height:auto}
.device-home{position:absolute;bottom:7px;left:50%;transform:translateX(-50%);width:32px;height:3px;border-radius:2px;background:#3a3a3a}

.device.on-stand .device-frame{border-radius:12px 12px 4px 4px}
.stand-neck{width:16px;height:26px;background:#151515;margin-top:-1px}
.stand-base{width:130px;height:9px;border-radius:5px;background:#151515;margin-top:-1px}

.device-caption{margin-top:16px;font-family:'Space Mono',monospace;font-size:12px;letter-spacing:0.04em;color:var(--muted)}
.mockup-frame .v-tag{position:absolute;top:14px;left:14px;font-family:'Space Mono',monospace;font-size:12px;font-weight:700}

.lifestyle-shot img{width:100%;height:auto;border-radius:16px;display:block;box-shadow:0 30px 60px -28px rgba(0,0,0,0.35)}
.lifestyle-shot figcaption{margin-top:10px}

/* ── Hero lifestyle image, large contained card under the headline/stats ── */
.hero-lifestyle{margin-top:clamp(28px,4vw,44px)}
.hero-lifestyle img{width:100%;height:auto;border-radius:var(--r-lg);display:block;box-shadow:0 30px 60px -28px rgba(0,0,0,0.4)}

/* ============================================================================
   EXPERIMENTAL: Gemini "subsystem showcase" styling (see work/lucrum-pos.html
   section 05 and memory lucrum_pos_gemini_showcase). Flat bordered containers,
   NO drop shadow, teal (#20B2AA brand) uppercase subsystem tags, plain-sans
   captions instead of the site's "//" Space Mono convention. Work in progress,
   intended to be revertible — delete this block and restore the previous
   .device-frame / .pos-mockup-stack markup to revert.
   ============================================================================ */
.subsystems{display:flex;flex-direction:column;gap:clamp(40px,6vw,72px)}
.subsystem-tag{display:block;font-size:12px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;color:var(--accent);margin-bottom:var(--sp-4);line-height:1.5}
.ui-shot{border:1px solid var(--line);border-radius:var(--r-lg);overflow:hidden;background:var(--shot);margin:0}
.ui-shot img{width:100%;height:auto;display:block}
.ui-shot img.placeholder{aspect-ratio:16/10;height:auto;object-fit:cover;opacity:.9}
.ui-shot figcaption{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);padding:var(--sp-3) var(--sp-4);font-size:13px;line-height:1.5;color:var(--muted);border-top:1px solid var(--line-soft)}
.sub-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start}
.sub-grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;align-items:start}
.sub-grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;align-items:start}
.sub-single{max-width:620px}
.sub-feature{margin-bottom:16px}
.sub-split{display:grid;grid-template-columns:1.4fr 1fr;gap:16px;align-items:start}
.sub-stack{display:flex;flex-direction:column;gap:16px}
@media (max-width:800px){.sub-grid-3,.sub-grid-4,.sub-split{grid-template-columns:1fr 1fr}}
@media (max-width:520px){.sub-grid-3,.sub-grid-4,.sub-split{grid-template-columns:1fr}}
/* .sub-grid-2 holds 2880x1920 full-app screenshots, so it collapses far earlier
   than the site's usual 800px breakpoint. Two dense POS screens side by side are
   ~338px each at iPad portrait and ~226px at 521px — unreadable. Collapsing at
   1024px gives every tablet a full-width screen and puts the narrowest possible
   2-up column at ~456px. */
@media (max-width:1024px){.sub-grid-2{grid-template-columns:1fr}}

/* Tall portrait shots — whole phone screens (uncropped 390x844 app captures) and
   the site's own tall modals — cannot be sized by the column the way a landscape
   browser shot can. They run ~2x as tall as they are wide, so a ~584px
   .sub-grid-2 cell would render one 1200px+ tall, and capping the image inside a
   full-width cell just leaves ~240px of dead air on either side of it. These rows
   are sized by the shot instead — fixed-width tracks, centred as a group — and
   the track width is what steps down at each breakpoint. */
.tall-row{display:grid;justify-content:center;gap:16px}
.tall-row-4{grid-template-columns:repeat(4,minmax(0,272px))}
.tall-row-2{grid-template-columns:repeat(2,minmax(0,336px))}
@media (max-width:1120px){.tall-row-4{grid-template-columns:repeat(2,minmax(0,300px))}}
@media (max-width:700px){.tall-row-4,.tall-row-2{grid-template-columns:minmax(0,340px)}}

/* Lightbox affordance for .ui-shot screens.
   The global img[data-lightbox] hover pop (translateY(-4px) scale(1.03), further
   down this file) cannot be used inside .ui-shot: the card is overflow:hidden, so
   the scale clips against the rounded border and the lift tears a gap above the
   caption. Cancel it and move the feedback to the card instead. */
.ui-shot{position:relative}
.ui-shot img[data-lightbox],
.ui-shot img[data-lightbox]:hover,
.ui-shot img[data-lightbox]:focus-visible{transform:none}
.ui-shot:hover{border-color:var(--accent)}
/* Inset so overflow:hidden can't clip it. Required — the transform above used to
   carry focus indication, so removing it needs an explicit replacement. */
.ui-shot img[data-lightbox]:focus-visible{outline:3px solid var(--accent);outline-offset:-3px}
/* Expand hint, in the caption bar rather than overlaid on the screenshot: these
   images are themselves full of UI, so a chip floating over one reads as part of
   the captured app (it landed squarely on the app's own "Close POS" button). In
   the caption bar it is unambiguously site chrome. Always visible, not hover-only
   — touch devices never get a hover state. Decorative; the image is the target. */
.shot-zoom{flex:none;width:16px;height:16px;fill:currentColor;opacity:.85;pointer-events:none;transition:opacity .2s ease}
.ui-shot:hover .shot-zoom{opacity:1}
/* ===== END EXPERIMENTAL subsystem showcase styling ===== */

/* ── Component library bento mosaic (section 05, "User flow and components").
     Every component sits in its own individually padded white card — no
     shared clusters, no bare floating pieces — for a uniform, coherent
     rhythm (per the user's "My stacked components.png" reference and
     follow-up request to give every tile the same treatment). Each
     .component-card is a grid item (grid-column/row span set inline per
     card, sized to its image's true size + padding) laid out with
     grid-auto-flow:dense so they pack tightly, the same bento-grid
     language the rest of this design system already uses.

     These Figma exports are all at 2x scale, so each image's real/crisp
     display size is its native px ÷ 2 (set inline per <img> as
     max-width/max-height) — images are never stretched to fill their card,
     only capped and centered, so nothing is upscaled past its true
     resolution (that's what caused the pixelated-status-pills bug two
     rounds ago). White is a deliberate, functional choice, not just
     styling: several exports carry label text colored for a white Figma
     canvas that's nearly invisible on this page's near-black background —
     confirmed by compositing samples onto the real #121212 bg, not by
     eyeballing. Flat, no border/shadow — matches this section's established
     borderless preference (rejected bordered .ui-shot cards twice already;
     see memory lucrum-pos-component-library). ── */
.component-mosaic{display:grid;grid-auto-flow:dense;grid-template-columns:repeat(24,1fr);grid-auto-rows:28px;gap:var(--sp-2);margin-top:var(--sp-10)}
.component-card{display:flex;align-items:center;justify-content:center;background:#fff;border-radius:var(--r-lg);padding:var(--sp-4);place-self:center;box-sizing:border-box;max-width:100%;min-width:0}
.component-card img{display:block;max-width:100%;min-width:0}

/* ── Mosaic cell spans. These were inline `style="grid-column:span N;grid-row:
     span M"` on all 19 cards until 2026-08-29. An inline style outranks every
     media query at every viewport, so the ≤640px stack below could only undo
     them with `grid-column:unset !important` — the !important was not a
     judgement call, it was the only tool left. Moving the spans to classes puts
     them back in the cascade at ordinary (0,1,0) specificity, so the media
     query overrides them on source order alone and the !important is gone.
     Values are unchanged, one class per distinct pair; renders identically at
     every width. Keep this block ABOVE the ≤640px query — that ordering is what
     makes the override work now that nothing is shouting. ── */
.cc-2x4{grid-column:span 2;grid-row:span 4}
.cc-3x3{grid-column:span 3;grid-row:span 3}
.cc-4x3{grid-column:span 4;grid-row:span 3}
.cc-4x4{grid-column:span 4;grid-row:span 4}
.cc-4x6{grid-column:span 4;grid-row:span 6}
.cc-4x7{grid-column:span 4;grid-row:span 7}
.cc-6x2{grid-column:span 6;grid-row:span 2}
.cc-6x3{grid-column:span 6;grid-row:span 3}
.cc-6x4{grid-column:span 6;grid-row:span 4}
.cc-6x8{grid-column:span 6;grid-row:span 8}
.cc-7x3{grid-column:span 7;grid-row:span 3}
.cc-7x5{grid-column:span 7;grid-row:span 5}
.cc-7x6{grid-column:span 7;grid-row:span 6}
.cc-7x7{grid-column:span 7;grid-row:span 7}
.cc-8x3{grid-column:span 8;grid-row:span 3}
.cc-8x7{grid-column:span 8;grid-row:span 7}
/* CRITICAL: place-self:center sizes a grid item to its own CONTENT, then
   centers that within the grid area — it does NOT cap the item at the
   area's width. Every per-image max-width below is a fixed px value (the
   image's true 2x-corrected size), tuned against this project's own
   desktop testing width (~1184px content). On any narrower real viewport —
   Chrome's Android "Request Desktop Site" mode renders the full grid at a
   narrower virtual width than that, and so does any ordinary desktop
   window narrower than ~1280px — the grid tracks shrink but the fixed-px
   images don't, so a .component-card overflows its own grid cell and
   visually overlaps its neighbors. box-sizing:border-box + max-width:100%
   above forces the card (and, by inheriting the same rule, the image
   inside it) to shrink with its actual grid track instead of overflowing.
   Confirmed via a real device screenshot (Chrome/Android desktop-site
   mode), not just the remote browser test harness used elsewhere in this
   section's history — that harness never caught this because it never
   rendered at a narrower-than-testing width. ── */

/* Sizing is self-correcting at every width (inline max-width/max-height
   never exceeds true crisp size) — only the grid itself needs a mobile
   fallback: 24 narrow tracks stop making sense on a phone, so collapse to
   a simple centered stack of cards, each keeping its own consistent padding. */
@media (max-width:640px){
  .component-mosaic{display:flex;flex-direction:column;align-items:center;gap:16px}
  /* No !important needed since the spans became .cc-* classes above: equal
     specificity, and this block is later in the file. */
  .component-card{grid-column:unset;grid-row:unset}
}

/* ── Action buttons ("Payment", "Cancel Order", etc.) — deliberately OUTSIDE
   .component-mosaic's CSS Grid, as a plain block-flow sibling after it.

   Why: these buttons wrap via flex-wrap, and how many rows that produces
   depends on the actual rendering width — which is exactly the kind of
   thing that's unpredictable across real devices (this project has now hit
   that twice: Chrome/Android "Request Desktop Site" renders at a virtual
   width neither this project's own ~1568px test harness nor a hand-picked
   980px iframe test happened to match). A CSS Grid item's row-track size
   is FIXED once set (grid-row:span N reserves exactly N tracks) — if
   flex-wrap content needs more rows than N at some width the layout wasn't
   tuned for, it overflows the cell vertically and collides with whatever
   sits below, which is exactly the "Redeem Points"/"Card" overlap and the
   diary-photos-clipping-the-mosaic bug the user found. There is no fixed
   grid-row span that is correct at every width for content whose row count
   is itself width-dependent — so these buttons don't get one. As normal
   block-flow (height:auto), each group always takes exactly the space its
   wrapped content needs, at any width, with no overflow possible. Trade-off:
   these no longer tile into the bento grid's gaps — correctness over
   novelty, after two real-device bugs from the grid-integrated version. */
.component-actions{display:flex;flex-direction:row;flex-wrap:wrap;align-items:flex-start;gap:var(--sp-2);margin-top:var(--sp-2)}
/* row + flex-wrap (not a stacked column): the group cards sit side by side
   and reflow naturally, matching the mosaic's varied-width multi-card rows
   instead of reading as three full-width bars orphaned below it. gap and
   margin-top match .component-mosaic's own 8px so this reads as a
   continuation of the same rhythm rather than a detached block. Still pure
   flow properties — no spans, height stays auto — so the fixed-grid-row
   overflow bug documented above cannot recur from this.

   align-items:flex-start also overrides the flex default of "stretch" —
   without it, every .component-card-group stretches to fill the FULL width
   of this container, leaving a large dead white area to the right of a
   3-4-button card. Every other tile in this mosaic sizes to its own
   content; these groups should too. */
/* Each group of related buttons (confirm / neutral / destructive) shares
   ONE card — not each button individually boxed with gaps between, which
   read as a jagged, mismatched-width "pyramid" rather than a deliberate
   grouping. Same white/radius/padding language as .component-card, just
   holding multiple buttons instead of one image. */
.component-card-group{display:flex;flex-wrap:wrap;align-items:center;gap:12px;background:#fff;border-radius:var(--r-lg);padding:var(--sp-4);box-sizing:border-box;max-width:100%;min-width:0}
.component-card-group img{display:block;max-width:100%;min-width:0}

/* ── Diary/process-artifact pairs: real 2-column grid (equal-width columns,
     not flex sized off each image's own aspect ratio), compact height since
     these are supporting context beside the section text, not a feature.
     No card frame — image scales down from its natural size via max-height/
     max-width, so no distortion and no letterboxing background. ── */
.diary-pair{display:grid;grid-template-columns:1fr 1fr;gap:clamp(20px,3vw,32px);align-items:start}
.diary-pair figure{margin:0}
.diary-pair img{width:auto;height:auto;max-width:100%;max-height:170px;display:block;border-radius:var(--r-lg)}
.diary-pair figcaption{margin-top:var(--sp-3);line-height:1.5;font-size:13px;color:var(--muted)}
@media (max-width:640px){.diary-pair{grid-template-columns:1fr}}

/* ── Diary notes / research carousel: every notebook photo in one place,
     mixed portrait/landscape shown at natural aspect ratio (no cropping
     frame), height-matched, horizontally scrolling. ── */
.diary-carousel{display:flex;align-items:flex-end;gap:clamp(20px,3vw,32px);overflow-x:auto;padding-bottom:8px;-webkit-overflow-scrolling:touch;scroll-snap-type:x proximity}
.diary-carousel figure{flex-shrink:0;margin:0;scroll-snap-align:start}
.diary-carousel img{height:280px;width:auto;max-width:64vw;display:block;border-radius:var(--r-lg)}
.diary-carousel figcaption{margin-top:var(--sp-3);line-height:1.5;max-width:260px;font-size:13px;color:var(--muted)}
@media (max-width:640px){.diary-carousel img{height:220px}}

.diary-jump-link{padding:var(--sp-3) 0;min-height:44px;display:inline-flex;align-items:center;margin:var(--sp-4) 0 var(--sp-5);font-family:'Space Mono',monospace;font-size:12px;letter-spacing:0.04em;color:var(--muted);text-decoration:none;border-bottom:1px solid var(--line);transition:color .2s ease,border-color .2s ease}
.diary-jump-link:hover{color:var(--text);border-color:var(--text)}

/* ── Lightbox-enabled images: hover pop (same easing as .work-card),
     click/Enter/Space opens the full-size overlay built by site.js. ── */
img[data-lightbox]{cursor:zoom-in;transition:transform .4s cubic-bezier(.16,1,.3,1)}
img[data-lightbox]:hover,img[data-lightbox]:focus-visible{transform:translateY(-4px) scale(1.03)}

body.lightbox-lock{overflow:hidden}

.lightbox{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;padding:clamp(16px,4vw,48px);background:rgba(0,0,0,0.82);opacity:0;visibility:hidden;transition:opacity .3s ease}
.lightbox.open{opacity:1;visibility:visible}
.lightbox figure{margin:0;max-width:100%;max-height:100%;display:flex;flex-direction:column;align-items:center}
.lightbox-img{max-width:100%;max-height:82vh;width:auto;height:auto;object-fit:contain;border-radius:var(--r-lg);box-shadow:0 20px 60px rgba(0,0,0,0.4)}
.lightbox-caption{margin-top:var(--sp-4);line-height:1.5;font-family:'Space Mono',monospace;font-size:13px;color:#EDEDEA;text-align:center;max-width:60ch}
.lightbox-close{position:absolute;top:var(--sp-5);right:var(--sp-6);width:44px;height:44px;padding:0;border-radius:var(--r-pill);border:1px solid rgba(255,255,255,0.3);background:rgba(255,255,255,0.08);color:#fff;font-size:26px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .2s ease,transform .2s ease}
.lightbox-close:hover{background:rgba(255,255,255,0.18);transform:scale(1.05)}

/* Wide landscape shots on phones (.lightbox-wide is set by site.js from
   data-lightbox-wide). Fitting a 3:2 desktop screenshot to a 375px viewport
   yields a ~343px image — no bigger than the thumbnail it was opened from. So
   below 640px, size it by height instead and let the overlay pan horizontally.
   flex-start matters: with justify-content:center the overflow to the left of
   centre is unreachable by scrolling. The close button switches to fixed so it
   cannot scroll out of reach, and the caption sticks to the left edge so it
   stays on screen while the image pans. */
@media (max-width:640px){
  .lightbox.lightbox-wide{overflow:auto;align-items:flex-start;justify-content:flex-start;padding:72px 16px 16px}
  .lightbox.lightbox-wide figure{max-width:none;max-height:none}
  .lightbox.lightbox-wide .lightbox-img{max-width:none;max-height:none;width:auto;height:70vh}
  .lightbox.lightbox-wide .lightbox-caption{position:sticky;left:0;max-width:calc(100vw - 32px);text-align:left}
  .lightbox.lightbox-wide .lightbox-close{position:fixed}
}
@media (prefers-reduced-motion:reduce){.lightbox{transition:none}}
.mockup-frame .v-cap{position:absolute;bottom:14px;left:14px;font-family:'Space Mono',monospace;font-size:11px;color:var(--muted)}

/* ── Reflection pull quote ── */
.reflection-quote{position:relative;margin:0;padding-top:8px}
.reflection-quote .quote-mark{display:block;font-family:'Bricolage Grotesque',sans-serif;font-weight:800;font-size:clamp(60px,8vw,100px);line-height:1;color:var(--line);margin-bottom:-0.2em}
.reflection-quote p{margin:0;font-family:'Bricolage Grotesque',sans-serif;font-weight:500;font-size:clamp(26px,4vw,48px);line-height:1.25;letter-spacing:-0.015em;max-width:26ch}
.reflection-quote cite{display:block;margin-top:28px;font-family:'Space Mono',monospace;font-size:12px;font-style:normal;color:var(--muted);letter-spacing:0.05em}

.next-project{padding-top:clamp(48px,7vw,80px);padding-bottom:clamp(48px,8vw,88px);border-top:1px solid var(--text)}
.next-project .np-label{font-family:'Space Mono',monospace;font-size:12px;color:var(--muted);letter-spacing:0.06em}
.next-project a.np-link{display:flex;align-items:baseline;justify-content:space-between;gap:24px;text-decoration:none;color:var(--text);margin-top:16px;padding-bottom:8px;border-bottom:1px solid var(--line);transition:border-color .3s ease}
.next-project a.np-link:hover{border-color:var(--text)}
.next-project a.np-link .np-name{font-family:'Bricolage Grotesque',sans-serif;font-weight:700;font-size:clamp(30px,6vw,64px);letter-spacing:-0.03em}
.next-project a.np-link .np-arrow{font-family:'Space Mono',monospace;font-size:20px;white-space:nowrap}

.coming-soon{display:inline-flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:var(--r-pill);padding:var(--sp-3) var(--sp-5);font-family:'Space Mono',monospace;font-size:12px;letter-spacing:0.05em;color:var(--muted);margin-top:32px}

/* ── Homepage hero intro + About ─────────────────────────────────────────────
   These two grids were written as inline style="" on index.html and were the
   only grids on the site with no breakpoint — an inline grid-template-columns
   beats every media query regardless of viewport, exactly the failure already
   documented for .tag-grid-3 above. At 371px they held four 53px tracks, so
   the intro paragraph and the CTA both rendered 138px wide (three words per
   line) with ~190px of dead gutter beside them.

   The old inline guard was min-width:min(100%,320px) on the paragraph. That
   can never work: a percentage min-width resolves against the grid area, so
   it cannot widen its own track. Collapsing the grid is the only fix.
   Same 800px tier as .cs-meta, which is the identical 4-column pattern. ── */
.home-intro{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--sp-8);margin-top:clamp(40px,6vw,72px);align-items:end}
.home-intro .intro-copy{grid-column:span 2;margin:0;font-size:clamp(17px,2.2vw,22px);line-height:1.5;color:var(--muted);max-width:46ch}
.home-intro .intro-cta{grid-column:span 2;display:flex;justify-content:flex-start}

.home-about{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--sp-8)}
.home-about .about-label{grid-column:span 1}
.home-about .about-body{grid-column:span 3}

@media (max-width:800px){
  .home-intro,.home-about{grid-template-columns:1fr;gap:var(--sp-6)}
  .home-intro .intro-copy,.home-intro .intro-cta,
  .home-about .about-label,.home-about .about-body{grid-column:1/-1}
}

/* Primary CTA. Was inline onmouseover/onmouseout on index.html, which meant
   the fill never appeared on keyboard focus — :focus-visible fixes that. */
.btn-primary{display:inline-flex;align-items:center;gap:12px;text-decoration:none;color:var(--text);background:transparent;border:1px solid var(--text);border-radius:var(--r-pill);padding:16px 28px;font-size:15px;font-weight:600;transition:background .35s ease,color .35s ease}
.btn-primary:hover,.btn-primary:focus-visible{background:var(--text);color:var(--bg)}
.btn-primary .btn-arrow{font-family:'Space Mono',monospace}

/* ── Work grid (homepage) ── */
.work-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(32px,4vw,48px) 32px}
.work-card{grid-column:span 2}

/* ── Card overlay labels ("01", the category, "// slug").
     These were twelve inline styles in three different treatments: card 1 used
     var(--muted) with a two-layer shadow, cards 2-3 used #EDEDEA with a lighter
     one, card 4 used var(--muted) with no shadow at all. Measured against the
     actual screenshot pixels underneath (scripts/overlay-contrast.mjs), four of
     the twelve came in below WCAG AA, worst 1.95:1.

     The root cause was var(--muted): it flips with the page theme, but the
     backdrop is a photograph that does not. A label over an image has to be
     coloured against the image, so all twelve are now light and unconditional.

     Light text alone is not enough on an arbitrary screenshot, so the scrim
     below guarantees the backdrop rather than hoping for it — WCAG gives no
     credit for a text-shadow, and a shadow cannot be measured honestly anyway
     (hiding the text to photograph the backdrop hides its shadow too). The
     gradients are confined to the top and bottom strips where labels actually
     sit; the middle of the thumbnail is untouched.

     The scrim must paint above the <img> but below the labels. ::after is the
     last child so it clears the image on DOM order alone, and the labels are
     lifted over it with z-index — they set no z-index inline, so this wins
     without needing !important. ── */
/* The gradient HOLDS at full strength across the band the labels occupy, then
   fades. A plain 0 -> transparent ramp was tried first and failed the bright
   cards: a label at top:16px with ~13px of height sits around 22px into a 76px
   ramp, where only ~70% of the stated alpha has been laid down. Two of the four
   thumbnails are near-white composites, and 0.37 effective alpha does not take
   a near-white pixel anywhere near the 0.14 luminance that #EDEDEA text needs
   for 4.5:1. Holding .62 to 38px puts the floor under the text instead of
   under the top edge, and the fade still lands well before the image middle. */
.work-card .shot::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:
    linear-gradient(to bottom, rgba(0,0,0,.62) 0, rgba(0,0,0,.62) 38px, rgba(0,0,0,0) 88px),
    linear-gradient(to top,    rgba(0,0,0,.62) 0, rgba(0,0,0,.62) 36px, rgba(0,0,0,0) 84px);
}
/* Named card-* rather than shot-*: the exploration clone already defines a
   .shot-tag of its own for the category label, and a bare .shot-tag here would
   silently restyle it through the shared stylesheet. */
.card-label{
  position:absolute;z-index:1;
  font-family:'Space Mono',monospace;
  color:#EDEDEA;
  text-shadow:0 1px 3px rgba(0,0,0,.75);
}
.card-label.card-num {top:16px;left:16px;font-size:13px;font-weight:700}
.card-label.card-cat {top:16px;right:16px;font-size:11px;letter-spacing:0.08em;text-transform:uppercase}
.card-label.card-slug{bottom:16px;left:16px;font-size:12px}

.work-card .shot{transition:transform .4s cubic-bezier(.16,1,.3,1),background-color .4s ease}
.work-card:hover .shot,.work-card:focus-visible .shot{transform:translateY(-4px);background-color:var(--shot-hover)}
@media (max-width:640px){.work-card{grid-column:span 4}}

/* ── Work card thumbnail: tilted 15deg for depth, oversized and centered so
     the rotated corners never leave a gap, scales with the card at any
     breakpoint since sizing is percentage-based off the .shot container. ── */
.shot img.thumb-img{position:absolute;top:50%;left:50%;width:135%;height:135%;object-fit:cover;transform:translate(-50%,-50%) rotate(15deg);transform-origin:center center;transition:transform .4s cubic-bezier(.16,1,.3,1)}
.work-card:hover .thumb-img,.work-card:focus-visible .thumb-img{transform:translate(-50%,-50%) rotate(15deg) scale(1.04)}

/* ── Footer ── */
footer.site-footer{padding-top:clamp(48px,7vw,80px);padding-bottom:64px;border-top:1px solid var(--text)}
footer.site-footer .f-row{display:flex;justify-content:space-between;align-items:flex-start;gap:40px;flex-wrap:wrap}
/* line-height 1.02 on a clamped display size means the link's box is only as
   tall as the glyphs — 29px at the 28px end of the clamp, under the 44px floor.
   fit-content keeps the box hugging the address exactly as display:block did
   here, so only the height changes, and only at the narrow end. */
footer.site-footer .f-email{display:flex;align-items:center;width:fit-content;min-height:44px;text-decoration:none;color:var(--text);font-family:'Bricolage Grotesque',sans-serif;font-weight:700;font-size:clamp(28px,5vw,56px);letter-spacing:-0.03em;line-height:1.02;margin-top:20px}
footer.site-footer .f-else{display:flex;flex-direction:column;gap:12px;align-items:flex-start}
footer.site-footer .f-else a{text-decoration:none;color:var(--text);font-size:16px;font-weight:500;opacity:0.85;padding:var(--sp-3) 0;display:inline-block}
footer.site-footer .f-bottom{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-top:clamp(48px,8vw,88px);flex-wrap:wrap}
footer.site-footer .f-bottom span{font-family:'Space Mono',monospace;font-size:12px;color:var(--muted)}

/* ── Theme toggle ── */
#theme-toggle{position:fixed;bottom:24px;right:24px;z-index:65;width:56px;height:56px;border-radius:var(--r-pill);border:1px solid var(--line);background:var(--bg);color:var(--text);display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 6px 24px rgba(0,0,0,0.10);transition:transform .3s cubic-bezier(.16,1,.3,1)}
#theme-toggle:hover{transform:scale(1.06)}
#theme-toggle .dot{width:22px;height:22px;border-radius:var(--r-pill);border:1.5px solid var(--text);background:linear-gradient(90deg,var(--text) 0,var(--text) 50%,transparent 50%,transparent 100%)}

/* ── Cross document view transitions ── */
@view-transition{
  navigation: auto;
}
::view-transition-group(root){animation-duration:460ms}
::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal}
::view-transition-old(root){z-index:1}
::view-transition-new(root){z-index:2}

@keyframes cs-slide-in-right{from{transform:translateX(100%)}to{transform:translateX(0)}}
@keyframes cs-slide-out-right{from{transform:translateX(0)}to{transform:translateX(100%)}}
@keyframes cs-slide-in-bottom{from{transform:translateY(100%)}to{transform:translateY(0)}}
@keyframes cs-slide-out-bottom{from{transform:translateY(0)}to{transform:translateY(100%)}}

:root:active-view-transition-type(forward){
  &::view-transition-new(root){animation:cs-slide-in-right 460ms cubic-bezier(.16,1,.3,1) both}
}
:root:active-view-transition-type(back){
  &::view-transition-old(root){animation:cs-slide-out-right 460ms cubic-bezier(.16,1,.3,1) both;z-index:2}
  &::view-transition-new(root){z-index:1}
}
@media (max-width:640px){
  :root:active-view-transition-type(forward){
    &::view-transition-new(root){animation:cs-slide-in-bottom 460ms cubic-bezier(.16,1,.3,1) both}
  }
  :root:active-view-transition-type(back){
    &::view-transition-old(root){animation:cs-slide-out-bottom 460ms cubic-bezier(.16,1,.3,1) both}
  }
}
@media (prefers-reduced-motion: reduce){
  ::view-transition-group(root),
  ::view-transition-old(root),
  ::view-transition-new(root){animation:none!important}
}


/* ── Focus visibility ────────────────────────────────────────────────────────
   Nothing on the case study previously had a designed focus state; keyboard
   users got whatever the browser drew. --text rather than --accent so the ring
   is theme-safe at full contrast in both themes. */
a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible,
summary:focus-visible{outline:2px solid var(--text);outline-offset:3px;border-radius:var(--r-sm)}
/* .ui-shot images keep their own inset ring (overflow:hidden would clip an
   offset one) — declared with the subsystem block above. */
.ui-shot img[data-lightbox]:focus-visible{outline-offset:-3px;border-radius:0}

/* Skip link — visually hidden until focused. */
/* The first thing a keyboard user lands on, so it holds itself to the same
   44px floor as every other control: padding alone left it at 42. */
.skip-link{position:absolute;left:-9999px;top:0;z-index:200;background:var(--text);color:var(--bg);
  padding:var(--sp-3) var(--sp-4);border-radius:var(--r-sm);font-size:14px;font-weight:600;text-decoration:none;
  min-height:44px;display:inline-flex;align-items:center}
.skip-link:focus{left:var(--sp-4);top:var(--sp-4)}

/* The component cards are deliberately #fff in BOTH themes: several Figma
   exports carry muted label text coloured for a white canvas, so a white card
   is functional, not decorative. But white on the light background is a 1.08:1
   boundary, making 23 cards structurally invisible. Give them an edge in light
   theme only; dark theme must not change. */
[data-theme="light"] .component-card,
[data-theme="light"] .component-card-group{border:1px solid var(--line)}

/* Horizontal scrollers are keyboard-focusable (Firefox and Safari do not
   auto-focus scroll containers the way Chrome does). */
.flow-chart:focus-visible,
.diary-carousel:focus-visible{outline:2px solid var(--text);outline-offset:2px}

/* width/height attributes are on every <img> to reserve space and stop layout
   shift. Those attributes are presentational hints, so any image capped by
   max-width/max-height needs an explicit height:auto or the hint would fight
   the cap and stretch it — this matters most for the mosaic, whose tile sizing
   took a long time to get right. */
.component-card img,
.component-card-group img{height:auto}

/* Diary photos get the same expand hint as the section-06 screens. These are
   170-280px renders of handwriting that is illegible at that size, so the
   lightbox is not a bonus — it is the only way to read them. On touch the sole
   previous cue was cursor:zoom-in, which does not exist on touch.

   Note flex-start, not space-between: these captions are far wider than the
   portrait photos above them, so pushing the icon to the caption's right edge
   left it floating unattached to its image. Sitting it directly after the text
   keeps it associated. (.ui-shot captions DO use space-between — there the
   caption and the image are the same width, so the right edge reads correctly.) */
.diary-pair figcaption,
.diary-carousel figcaption{display:flex;align-items:center;justify-content:flex-start;gap:var(--sp-2)}

/* ═══════════════════════════════════════════════════════════════════════════
   ZENY — scoped to .zeny on <main> (work/zeny.html)

   Palette is the approved style-guide system ONLY: black #030303, coffee bean
   #311C21, terracotta #F74E35, dust grey #DDD3CB. The brand book's teal/mint/
   navy/violet system was never approved and appears nowhere.

   --zeny-accent is per-theme for the same reason the site accent is: true
   terracotta #F74E35 measures 3.18:1 on the light page background, which is
   fine for large text and UI but fails 4.5:1 for body-size text. Light theme
   uses #C93A1A (4.73:1); dark keeps the true brand colour (5.44:1). The palette
   SWATCHES always render true #F74E35 — they are content, not text.
   Dust grey is 1.36:1 on light: never use it as text outside a dark surface.
   ═══════════════════════════════════════════════════════════════════════════ */
.zeny{--zeny-accent:#C93A1A}
[data-theme="dark"] .zeny{--zeny-accent:#F74E35}

/* LT Saeada — LyonsType, SIL Open Font License 1.1 (see lt_saeada/OFL.txt).
   Self-hosting is permitted by the licence. Four weights only: the family ships
   eleven (~231 KB); the specimen needs four (~85 KB). */
@font-face{font-family:'LT Saeada';src:url('../work/images/Zeny/lt_saeada/LTSaeada-Regular.otf') format('opentype');font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:'LT Saeada';src:url('../work/images/Zeny/lt_saeada/LTSaeada-Medium.otf') format('opentype');font-weight:500;font-style:normal;font-display:swap}
@font-face{font-family:'LT Saeada';src:url('../work/images/Zeny/lt_saeada/LTSaeada-Bold.otf') format('opentype');font-weight:700;font-style:normal;font-display:swap}
@font-face{font-family:'LT Saeada';src:url('../work/images/Zeny/lt_saeada/LTSaeada-Black.otf') format('opentype');font-weight:900;font-style:normal;font-display:swap}
.zeny-face{font-family:'LT Saeada','Instrument Sans',system-ui,sans-serif}
.zeny .bs-aa.zeny-face{font-weight:900}

/* Tag-chip internals — shared by the failure modes, personas, principles,
   voice rules and pillars. .tag-chip itself already supplies border, radius,
   padding and line-height. */
.chip-title{display:block;font-weight:700;font-size:15px;letter-spacing:-0.01em;color:var(--text)}
.chip-body{display:block;margin-top:var(--sp-2);font-size:14px;line-height:1.5;color:var(--muted);font-weight:400}
/* Falls back to the site accent off the Zeny page, where --zeny-accent is not defined. */
.chip-num{display:block;margin-bottom:var(--sp-2);font-family:'Space Mono',monospace;font-size:11px;letter-spacing:0.06em;color:var(--zeny-accent, var(--accent))}

/* Grid variants. .tag-grid is 4-up; principles need 5-up so five items do not
   wrap 4 + 1, and the palette needs 4-up. Both collapse on the site's
   established 800/520 breakpoints. */
.tag-grid-4{grid-template-columns:repeat(4,1fr)}
.tag-grid-5{grid-template-columns:repeat(5,1fr)}
.brand-grid-4{grid-template-columns:repeat(4,1fr)}
@media (max-width:800px){
  .tag-grid-4,.tag-grid-5{grid-template-columns:repeat(2,1fr)}
  .brand-grid-4{grid-template-columns:repeat(2,1fr)}
}
@media (max-width:520px){
  .tag-grid-4,.tag-grid-5{grid-template-columns:1fr}
  .brand-grid-4{grid-template-columns:1fr}
}

/* Audience matrix — two columns of three, B2B by scale against B2C by intent. */
.aud-matrix{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-4)}
.aud-col{display:flex;flex-direction:column;gap:var(--sp-2)}
.aud-head{display:block;font-family:'Space Mono',monospace;font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:var(--zeny-accent);margin-bottom:var(--sp-1)}
.aud-sub{color:var(--muted);text-transform:none;letter-spacing:0.04em}
.aud-cell{border:1px solid var(--line);border-radius:var(--r-md);padding:var(--sp-4)}
.aud-name{display:block;font-weight:700;font-size:15px;line-height:1.4}
.aud-meta{display:block;margin-top:var(--sp-1);font-family:'Space Mono',monospace;font-size:12px;line-height:1.5;color:var(--muted)}
@media (max-width:640px){.aud-matrix{grid-template-columns:1fr}}

/* Zeny rhythm joins — the sibling rules in the Vertical rhythm block cover
   .cs-body <-> media/grid, but not these pairings. Declared here rather than as
   inline margins on the markup, so all spacing stays in one system. */
.zeny .tag-grid + .tag-grid{margin-top:var(--sp-4)}
.zeny .cs-body + .tag-grid,
.zeny .tag-grid + .cs-body,
.zeny .cs-body + .aud-matrix,
.zeny .aud-matrix + .cs-body,
.zeny .bento-grid + .cs-body,
.zeny .cs-body + .brand-showcase,
.zeny .reflection-quote + .cs-body{margin-top:var(--sp-10)}
.zeny .brand-showcase + .brand-grid,
.zeny .brand-grid + .subsystems{margin-top:var(--sp-10)}
.zeny .subsystem .cs-body{margin-bottom:var(--sp-6)}

/* Section labels on this page must use the Zeny accent, not the site accent.
   .subsystem-tag defaults to var(--accent), which is the site's teal-green —
   on a terracotta brand page that reads as off-brand, and worse, it looks like
   the teal direction that was explicitly rejected. */
.zeny .subsystem-tag{color:var(--zeny-accent)}

/* C2C framework block — the organising idea of the Zeny identity.
   "Chaos" is set in the muted tone and "Craft" in the brand accent so the line
   performs the move it describes. Both sit at body weight/size, so the accent
   colour still has to clear 4.5:1 — which is why --zeny-accent darkens in light
   theme rather than using the true terracotta. */
.c2c{display:flex;align-items:flex-start;gap:clamp(20px,4vw,40px);flex-wrap:wrap;
     border-top:1px solid var(--line);border-bottom:1px solid var(--line);
     padding:clamp(24px,4vw,40px) 0}
.c2c-mark{font-size:clamp(56px,9vw,112px);line-height:0.9;font-weight:900;
          letter-spacing:-0.03em;color:var(--zeny-accent);flex:none}
.c2c-body{flex:1 1 320px;min-width:0}
.c2c-line{margin:0 0 var(--sp-3);font-family:'Bricolage Grotesque',sans-serif;
          font-weight:700;font-size:clamp(22px,3vw,32px);line-height:1.2;
          letter-spacing:-0.02em;color:var(--text)}
.c2c-chaos{color:var(--muted)}
.c2c-craft{color:var(--zeny-accent)}
.zeny .c2c + .cs-body{margin-top:var(--sp-10)}
.zeny .cs-section-head + .c2c{margin-top:0}
@media (max-width:520px){.c2c{gap:var(--sp-4)}}

/* ── Scroll-triggered reveals ────────────────────────────────────────────────
   Progressive enhancement, deliberately: the hidden state is scoped to
   .reveal-ready, which site.js puts on <html> ONLY when JS is running and the
   user has not asked for reduced motion. So with JS off, or with reduced motion
   on, every element is simply visible — the content is never hidden by a script
   that might not run.

   Motion is small on purpose (14px, 500ms). This is an editorial portfolio; the
   reveal should register as the page settling, not as an entrance.

   NOTE for layout verification: an element that has not yet revealed sits 14px
   low, which will skew getBoundingClientRect measurements. Any overlap/overflow
   audit should run `document.documentElement.classList.remove('reveal-ready')`
   first. Revealed elements are transform:none and measure correctly. */
.reveal-ready [data-reveal]{opacity:0;transform:translateY(14px);
  transition:opacity .5s cubic-bezier(.16,1,.3,1),transform .5s cubic-bezier(.16,1,.3,1)}
.reveal-ready [data-reveal].is-in{opacity:1;transform:none}
@media (prefers-reduced-motion:reduce){
  .reveal-ready [data-reveal]{opacity:1!important;transform:none!important;transition:none!important}
}

/* Print: reveals must never withhold content from paper. Only the elements the
   reader happened to scroll past would otherwise print, and a portfolio page is
   a plausible thing to print or "Save as PDF". */
@media print{
  .reveal-ready [data-reveal]{opacity:1!important;transform:none!important;transition:none!important}
}

/* Photography direction cards — fixed frame, image fills it.
   The three source photos are 0.893, 0.684 and 0.668 wide-to-tall, so letting
   each keep its own ratio rendered them ragged and unequal. A single 3:4 frame
   (closest to the set, so the least crop) plus object-fit:cover makes the row
   uniform and means the sources never need cropping by hand — the frame does it.
   object-position centres the crop; the subject is centred in all three. */
.photo-shot img{width:100%;height:auto;aspect-ratio:3/4;object-fit:cover;object-position:center;display:block}

/* ═══════════════════════════════════════════════════════════════════════════
   ORDER MANAGEMENT SYSTEM — scoped to .oms (work/order-management-system.html)

   Same product family as Lucrum POS (this is its back-office/branch-ops
   counterpart), so it intentionally shares the site's global palette/type
   scale rather than getting its own accent — the redesign is about layout
   discipline, not a new brand. Two things earned page-specific rules:

   1. The two .bento-grid sections had every cell forced to an identical
      grid-column:span 2 inline, flattening the grid's asymmetric design into
      a uniform checkerboard. Fixed with layout-only modifier classes so the
      site's existing 800/520 breakpoints keep working (an inline
      grid-column, like an inline grid-template-columns elsewhere in this
      file, would beat any media query regardless of viewport).
   NOT A GAP — resolved 2026-08-20 by looking at the actual screen. This note
   used to say section 05's copy ("collections are green and additive,
   settlements red and subtractive") described something the page did not do. The
   ledger *does* do exactly that: green +amounts for collections, red -amounts
   for settlements. Nobody could see it because the rider-financials screenshot
   was captured before the ledger loaded, so the table was empty in every image.
   The note was only ever true of these CHIPS, which carry no tone colour and do
   not need any — the copy refers to the ledger, not to them. An earlier version
   also claimed --tone-positive/--tone-warn made it literal; those properties
   never existed. Verify against a working capture before trusting a note here.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Section 02 "System architecture": ERPNext + Go are the two dominant halves
   (tall, tinted via the existing .problem background), WHY SPLIT / THE SEAM
   are a shorter supporting row underneath. nth-child matches this section's
   fixed problem-problem-issue-issue order. */
.oms .oms-systems .bento-cell.problem:nth-child(1){grid-column:1/3;grid-row:1/3}
.oms .oms-systems .bento-cell.problem:nth-child(2){grid-column:3/5;grid-row:1/3}
.oms .oms-systems .bento-cell.issue:nth-child(3){grid-column:1/3;grid-row:3/4}
.oms .oms-systems .bento-cell.issue:nth-child(4){grid-column:3/5;grid-row:3/4}
/* :nth-child raises specificity above a plain .bento-cell.problem selector, so
   the breakpoint overrides below repeat the same :nth-child qualifiers —
   otherwise the "desktop" rule above would outrank them at any width. */
@media (max-width:800px){
  .oms .oms-systems .bento-cell.problem:nth-child(1),
  .oms .oms-systems .bento-cell.problem:nth-child(2),
  .oms .oms-systems .bento-cell.issue:nth-child(3),
  .oms .oms-systems .bento-cell.issue:nth-child(4){grid-column:1/3;grid-row:auto}
}
@media (max-width:520px){
  .oms .oms-systems .bento-cell.problem:nth-child(1),
  .oms .oms-systems .bento-cell.problem:nth-child(2),
  .oms .oms-systems .bento-cell.issue:nth-child(3),
  .oms .oms-systems .bento-cell.issue:nth-child(4){grid-column:1/2}
}

/* Section 05 "Prototype to production": THE CONTRACT and WHAT IT BOUGHT are
   cause and payoff, not a hierarchy — equal-width, single row, differentiated
   by the existing .problem/.issue tint rather than by size. */
.oms .oms-contract .bento-cell.problem{grid-column:1/3;grid-row:1/2}
.oms .oms-contract .bento-cell.issue{grid-column:3/5;grid-row:1/2}
@media (max-width:800px){
  .oms .oms-contract .bento-cell.problem,
  .oms .oms-contract .bento-cell.issue{grid-column:1/3;grid-row:auto}
}
@media (max-width:520px){
  .oms .oms-contract .bento-cell.problem,
  .oms .oms-contract .bento-cell.issue{grid-column:1/2}
}

/* The hero de-duplication crop that used to live here is gone. It forced the
   OMS hero into a 3/1 band purely to disguise the fact that orders-queue.jpg
   was both the hero and section 03's first screenshot — treating the symptom
   of an asset shortage with CSS. The hero is now its own image
   (orders-intelligence.jpg), so the page needs no hero override at all. */

/* ── Dagwood: brand foundation, type specimen, palette provenance ────────────
   Section 07 of work/dagwood.html shows the client's own brand book resolving
   into the shipped product's design system. Two consequences for the CSS:

   1. The specimens are set in the REAL faces. Bebas Neue and Lato are loaded on
      dagwood.html for this block alone. Setting a type specimen in the
      portfolio's own typeface (as the Lucrum showcase does) makes the section a
      claim rather than a demonstration — the whole point here is that you can
      see the face.
   2. The brand band is deliberately NOT theme-aware. #B41E05 is the brand's
      constant, the reversed lockup is the variant the book specifies for it,
      and the pattern PNG is a white silhouette so it tints by opacity alone.
      Everything else in the section follows the page's light/dark tokens.
   ──────────────────────────────────────────────────────────────────────────── */
.dag-bebas{font-family:'Bebas Neue','Bricolage Grotesque',sans-serif;font-weight:400}
.dag-lato{font-family:'Lato','Instrument Sans',system-ui,sans-serif}

/* Brand mark + essence. Replaced a full-bleed red slab carrying a reversed
   lockup, a tiled pattern and 40px display type — too loud for an opener to a
   section whose job is to be systematic. The mark is the app's real splash
   animation, converted from its Lottie to inline SVG so it costs ~3KB instead of
   lottie-web's ~150KB, and so it can be paused for prefers-reduced-motion and
   tinted with currentColor. Brand red is kept as the mark's own colour rather
   than as a background, so the block sits on the page's theme like everything
   else in the section. */
.dag-mark-block{margin:0;display:flex;flex-direction:column;align-items:center;
  gap:var(--sp-6);text-align:center;padding:clamp(24px,4vw,44px) var(--sp-4)}
.dag-mark{width:clamp(84px,11vw,124px);height:auto;color:#B41E05;display:block}
.dag-mark-cap{display:flex;flex-direction:column;align-items:center;gap:var(--sp-3)}
.dag-essence{margin:0;max-width:24ch;font-size:clamp(21px,3.2vw,32px);line-height:1.15;
  letter-spacing:0.015em;color:var(--text);text-wrap:balance}
.dag-mark-src{font-family:'Space Mono',monospace;font-size:11px;letter-spacing:0.08em;
  text-transform:uppercase;color:var(--muted)}

/* The two halves drop in staggered, mirroring the Lottie: at 60fps the lead half
   runs frames 0-60 and the trailing half 30-60, so both land together. Gated on
   the section's existing .is-in reveal rather than firing on load, since this
   sits ~10,000px down the page. Without JS, .reveal-ready is never set and the
   mark simply renders in place. */
@keyframes dag-mark-drop{
  from{opacity:0;transform:translateY(-100%)}
  to{opacity:1;transform:translateY(0)}
}
.reveal-ready .dag-mark path{opacity:0}
.reveal-ready [data-reveal].is-in .dag-mark path{transform-box:view-box;
  animation:dag-mark-drop .95s cubic-bezier(.16,1,.3,1) both}
.reveal-ready [data-reveal].is-in .dag-mark .dag-mark-b{animation-duration:.5s;
  animation-delay:.45s}
@media (prefers-reduced-motion:reduce){
  .reveal-ready .dag-mark path{opacity:1}
  .reveal-ready [data-reveal].is-in .dag-mark path{animation:none;transform:none}
}

/* Type specimen — reuses Lucrum's .bs-type / .bs-aa / .bs-type-info /
   .bs-type-name / .bs-type-sample verbatim rather than defining a parallel set of
   dag-* card classes. Borderless and inline by design: the bordered cards this
   replaced read as a different component from the identical block on the Lucrum
   page. All this file adds is the 2-up grid (Dagwood shows two faces where Lucrum
   shows one) and a mono role line. */
.dag-type{display:grid;grid-template-columns:repeat(2,1fr);gap:clamp(24px,4vw,48px)}
.dag-type .bs-type-sample{font-size:clamp(14px,1.5vw,18px);overflow-wrap:anywhere}
/* Bebas Neue is condensed all-caps with no lowercase, so at the shared sample
   size it sets far smaller and tighter than Lato does and the specimen stops
   being readable. Two corrections, both of which the brand book already asks for:
   size it up so the cap height roughly matches Lato's x-height-plus-ascender, and
   apply the book's own display tracking (2% of font size — the same rule stated
   in the "Tracking on display type" note further down this section). */
.dag-type .bs-type-sample.dag-bebas{font-size:clamp(19px,2.2vw,26px);
  letter-spacing:0.02em;line-height:1.3}
@media (max-width:860px){.dag-type{grid-template-columns:1fr}}

/* Logo usage tiles. These are deliberately NOT theme-aware: each tile shows the
   background the brand book specifies that lockup for, so the trio has to look
   the same in light and dark or it stops being a usage exhibit. */
.dag-lockups{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-4)}
.dag-lockups figure{margin:0}
.dag-lockup{display:flex;align-items:center;justify-content:center;min-height:118px;
  /* 18/30 were both off the 4pt grid; 20/32 are the neighbouring steps
     (--sp-5 / --sp-8). A clamp cannot take var() for its bounds here without
     losing the 3vw middle term, so the literals stay — but they are now grid
     values. Net effect is 2px more padding at each end of the range. */
  padding:clamp(20px,3vw,32px);border:1px solid var(--line);border-radius:var(--r-md);
  background:var(--bg)}
.dag-lockup img{width:100%;max-width:200px;height:auto;display:block}
.dag-lockups figcaption{margin-top:var(--sp-3);font-family:'Space Mono',monospace;
  font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:var(--muted)}
@media (max-width:640px){.dag-lockups{grid-template-columns:1fr}}

/* ── Client roster ───────────────────────────────────────────────────────────
   Fifteen brands running Lucrum POS, as two rows scrolling in opposite
   directions. It sits under the §01 bento grid because it is the same argument
   the stat tiles are making — this shipped and it is in daily use — in a form a
   reader can verify rather than take on trust.

   Monochrome at rest, colour on hover. The reason is not taste: these fifteen
   marks carry fifteen unrelated palettes (a red tile, a dark-green tile, an
   orange tile, a navy wordmark), and at full colour side by side they read as a
   ransom note and pull the eye off the case study. One ink makes them read as a
   single component; the hover returns the brand to anyone who looks closer.

   NOTE ON TOUCH: phones have no hover, so on a phone this stays monochrome
   throughout. That is deliberate — the alternative, full colour on touch only,
   makes the phone and desktop pages look like two different designs.

   Sizing: each logo carries its own inline --h. A single shared height cannot
   work when the set runs from a 9.5:1 serif wordmark to a 1:1 app tile — at
   equal height the wordmark carries six times the area. The values are set by
   eye for equal optical weight, which is the same judgement a designer makes on
   a logo wall, and is why they are per-instance rather than a class.
   ──────────────────────────────────────────────────────────────────────────── */
.client-roster{border-top:1px solid var(--line);border-bottom:1px solid var(--line);
  padding:var(--sp-6) 0}
.roster-head{display:flex;justify-content:space-between;align-items:center;gap:var(--sp-4);
  margin-bottom:var(--sp-5);font-family:'Space Mono',monospace;font-size:11px;
  letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.roster-head .count{color:var(--accent)}
/* On a phone the three items in one row squeeze the label onto two lines and
   crowd the 44px button. Give the label its own line and let the control and
   the count share the next one. */
@media (max-width:640px){
  .roster-head{flex-wrap:wrap;gap:var(--sp-3)}
  .roster-head > span:first-child{flex:1 0 100%}
  .roster-head .count{margin-left:auto}
}

.roster-marquee{overflow:hidden;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent);
          mask-image:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent)}
.roster-marquee + .roster-marquee{margin-top:var(--sp-6)}
/* Three of the roster logos link out to the client. Those anchors were as tall
   as their artwork (38px for Dagwood) rather than as tall as a tap target, and
   a target inside a moving marquee is hard enough to hit already. The track is
   align-items:center, so a 44px floor on the anchor only changes the row height
   if the tallest logo is under 44 — it is not. */
.roster-track a{min-height:44px;display:inline-flex;align-items:center}
.roster-track{display:flex;width:max-content;align-items:center;gap:var(--sp-12);
  animation:roster-l 48s linear infinite}
/* The second row holds seven items to the first row's eight, so one copy of it
   measures ~1050px — narrower than a desktop viewport, which puts the same logo
   on screen twice. The wider gap pushes one copy past the fold. Desktop only:
   on a phone one copy already outruns the viewport, and a 120px gap there would
   cost exactly what this two-row layout was chosen for, which is how many
   brands are on screen at once. */
.roster-track.rev{animation-name:roster-r}
@media (min-width:1024px){ .roster-track.rev{gap:120px} }

/* Phone: scale the marks down to 80% and tighten the gap. Two rows on a phone
   were chosen over a static grid precisely to get more brands on screen at
   once, and at full size only two fit per row, which gives that back. */
@media (max-width:640px){
  .roster-track, .roster-track.rev{gap:var(--sp-8)}
  .roster-logo{height:calc(var(--h,40px) * .8)}
  .roster-logo.tile{width:calc(var(--h,48px) * .8)}
  .roster-name{font-size:16px}
}
@keyframes roster-l{from{transform:translateX(0)}    to{transform:translateX(-50%)}}
@keyframes roster-r{from{transform:translateX(-50%)} to{transform:translateX(0)}}

/* Hovering a row pauses it, so the logo under the cursor is readable rather than
   sliding away from it. Focus does the same, for the Dagwood link inside. */
.roster-marquee:hover .roster-track,
.roster-marquee:focus-within .roster-track,
.client-roster.is-paused .roster-track{animation-play-state:paused}

/* The duplicate copy must not become a flex item of its own — display:contents
   flattens it into the track. It lives here and not inline because the
   reduced-motion rule below needs to be able to hide it, and an inline style
   would outrank a media query. */
.roster-dupe{display:contents}

/* Greyscale alone is not enough, because these marks do not share a luminance
   range: Layers is pale gold and Cinnabon is near-black. Desaturated straight,
   the gold lands almost exactly on the light background and disappears. Each
   theme therefore also pushes luminance toward its own contrast direction —
   darker on light, lighter on dark — so every mark lands in a visible band. */
.roster-logo{height:var(--h,40px);width:auto;max-width:190px;object-fit:contain;display:block;
  filter:grayscale(1) contrast(1.1) brightness(.72) opacity(.85);
  transition:filter .25s ease}
[data-theme="dark"] .roster-logo{filter:grayscale(1) contrast(1.05) brightness(1.45) opacity(.8)}
/* The four app-icon marks carry their own opaque background, so they are already
   self-contained and must NOT get the per-theme luminance push the transparent
   marks need — pushed lighter on dark they flatten into featureless grey blocks
   and lose the artwork inside them. Greyscale and a little contrast only. */
.roster-logo.tile,
[data-theme="dark"] .roster-logo.tile{width:var(--h,48px);border-radius:var(--r-md);
  filter:grayscale(1) contrast(1.15) opacity(.82)}
.roster-logo:hover,
[data-theme="dark"] .roster-logo:hover,
.roster-logo.tile:hover,
[data-theme="dark"] .roster-logo.tile:hover{filter:none}

/* Theme safety. Artwork that is pure black or pure white vanishes against one of
   the two backgrounds; invert+hue-rotate flips lightness while holding hue, so a
   coloured mark survives the flip instead of turning into its complement. These
   have to restate the greyscale stack because they replace the whole filter. */
[data-theme="dark"]  .roster-logo[data-ink="dark"]{
  filter:invert(1) hue-rotate(180deg) grayscale(1) brightness(1.45) opacity(.8)}
[data-theme="light"] .roster-logo[data-ink="light"]{
  filter:invert(1) hue-rotate(180deg) grayscale(1) brightness(.72) opacity(.85)}
[data-theme="dark"]  .roster-logo[data-ink="dark"]:hover,
[data-theme="light"] .roster-logo[data-ink="light"]:hover{
  filter:invert(1) hue-rotate(180deg)}
[data-theme="light"] .roster-only-dark{display:none}
[data-theme="dark"]  .roster-only-light{display:none}

/* Two brands have no usable logo file, so they are set rather than faked. */
.roster-name{font-family:'Bricolage Grotesque',sans-serif;font-weight:700;font-size:18px;
  letter-spacing:-.01em;white-space:nowrap;color:var(--muted);transition:color .25s ease}
.roster-name:hover{color:var(--text)}

/* WCAG 2.2.2: motion that starts on its own and runs past five seconds needs a
   real pause control — hover is not one, since it does not exist on touch and
   cannot be reached from the keyboard. Hidden until site.js wires it up, so it
   is never a dead button on a page where the script failed. */
.roster-pause{display:none;font:inherit;color:var(--muted);background:transparent;
  border:1px solid var(--line);border-radius:var(--r-pill);
  min-height:44px;padding:0 var(--sp-4);cursor:pointer;letter-spacing:.08em}
.js-roster .roster-pause{display:inline-flex;align-items:center}
.roster-pause:hover{color:var(--text);border-color:var(--accent)}
.roster-pause:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* Failsafe: with motion off, a track is no longer a track. It wraps like a grid
   and the duplicate copy is dropped, so all fifteen stay readable and none is
   read out twice. The pause button has nothing left to pause. */
@media (prefers-reduced-motion:reduce){
  .roster-marquee{-webkit-mask-image:none;mask-image:none}
  .roster-track{animation:none;width:100%;flex-wrap:wrap;justify-content:center;
    gap:var(--sp-6) var(--sp-8)}
  .roster-track.rev{gap:var(--sp-6) var(--sp-8)}
  .roster-dupe{display:none}
  .roster-logo{transition:none}
  .js-roster .roster-pause{display:none}
}
