/* Shared shell for lukasuhrstrom.com: / and /about/.
   Separate from /assets/touch.css, which belongs to LukasOS and carries
   !important overrides tuned for that estate. */

@font-face{
  font-family:"BigShoulders";font-style:normal;font-weight:100 900;font-display:swap;
  src:url("/fonts/big-shoulders.woff2") format("woff2");
}
@font-face{
  font-family:"PublicSans";font-style:normal;font-weight:100 900;font-display:swap;
  src:url("/fonts/public-sans.woff2") format("woff2");
}
@font-face{
  font-family:"MartianMono";font-style:normal;font-weight:400;font-display:swap;
  src:url("/fonts/martian-mono-400.woff2") format("woff2");
}
@font-face{
  font-family:"MartianMono";font-style:normal;font-weight:700;font-display:swap;
  src:url("/fonts/martian-mono-700.woff2") format("woff2");
}

/* Light is the default token set below (a plain :root, no media query
   needed to get it), dark is layered on top two ways: automatically via
   prefers-color-scheme for whichever device default the visitor already has
   set, and explicitly via [data-theme] once someone clicks the sun/moon
   toggle (assets/theme.js) - that choice is stored and then wins over the
   system preference in both directions, which is why light is restated
   explicitly under its own [data-theme="light"] block too, not left to fall
   through to the bare :root by default alone. Every ratio below is measured
   the same way as its dark counterpart: text pairs against --bg and --card,
   not just --bg, since --card is common ground beneath --muted and links. */
:root{
  --bg:#F5F3EE;
  --surface:#FFFFFF;
  --card:#ECE8DE;
  --line:#DBD5C6;
  --ink:#191B1D;          /* 15.58:1 on bg */
  --muted:#5B584C;        /* 6.43:1 on bg, 5.83:1 on card */
  --accent:#A8460F;       /* 5.34:1 on bg, 4.84:1 on card - reserved for "this is live/ongoing" (timeline tips, tracking) */
  --accent2:#0B6E8F;      /* 5.20:1 on bg, 4.71:1 on card - link underlines + icons */
  --f-display:"BigShoulders",system-ui,sans-serif;
  --f-body:"PublicSans",system-ui,-apple-system,sans-serif;
  --f-mono:"MartianMono",ui-monospace,monospace;

  /* Bumped a step larger across the board 11 Aug 2026. Section headings
     carry hierarchy through --f-mono + --muted rather than size (see .sec)
     - there's no separate large heading size in play. */
  --fs-micro:14px;
  --fs-label:15px;
  --fs-body:17px;
  --fs-lede:20px;

  /* Real margins and real air between sections. */
  --gutter:clamp(16px,2.8vw,56px);
  --vgap:clamp(44px,6.4vh,96px);
  --colgap:clamp(32px,3.6vw,80px);
}

/* Base sits at L* 8.9 with a cool blue-grey tint rather than pure black:
   #000 makes bright text bloom and edges vibrate against it. Surfaces step up
   the tonal ladder (L* 8.9 → 11.4 → 13.8 → 20.4) instead of using shadows.
   The accent is the LukasOS orange lightened for a dark ground - #DE5109 sits
   at 4.42:1 here and fails AA as text, #F2793A clears it at 6.32:1. */
@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]){
    --bg:#151A1D;
    --surface:#1A1F23;
    --card:#1E2429;
    --line:#2B3238;
    --ink:#E9ECEA;
    --muted:#98A1A5;
    --accent:#F2793A;
    --accent2:#3FC1F0;
  }
}
:root[data-theme="dark"]{
  --bg:#151A1D;
  --surface:#1A1F23;
  --card:#1E2429;
  --line:#2B3238;
  --ink:#E9ECEA;
  --muted:#98A1A5;
  --accent:#F2793A;
  --accent2:#3FC1F0;
}
:root[data-theme="light"]{
  --bg:#F5F3EE;
  --surface:#FFFFFF;
  --card:#ECE8DE;
  --line:#DBD5C6;
  --ink:#191B1D;
  --muted:#5B584C;
  --accent:#A8460F;
  --accent2:#0B6E8F;
}

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;background:var(--bg)}
body{
  margin:0;background:var(--bg);color:var(--ink);
  font:400 var(--fs-body)/1.6 var(--f-body);
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
  font-variant-numeric:tabular-nums;
  min-height:100vh;display:flex;flex-direction:column;
  /* No element on this site is meant to scroll horizontally at the page
     level. A few decorative absolutely-positioned overlays sit a handful of
     pixels outside their own box on purpose (the hover-highlight rects
     under /reading, /ratings, /been) - harmless on their own, but col 3 sits
     close enough to the right margin that they were poking past the
     viewport edge and adding a few pixels of real page-wide horizontal
     scroll. Overflow on body propagates to the viewport per spec when html
     doesn't set its own, so this is the one place this needs to be set. */
  overflow-x:hidden;
}
a{color:inherit;text-decoration:none}
a:focus-visible,input:focus-visible + label{outline:2px solid var(--accent);outline-offset:3px}
img{max-width:100%}
/* top/left:0 pins this to a known position rather than its static position
   plus a -1px margin - for the homepage's hidden <h1>, first in the body,
   that static position was already flush with the document edge, so the
   negative margin alone was enough to push it 1px past it and contribute
   to the page's real (if invisible) horizontal scroll. */
.sr{position:absolute;top:0;left:0;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}

/* ── topbar ───────────────────────────────────────────────────────── */
.topbar{
  display:flex;align-items:center;justify-content:space-between;gap:16px 24px;flex-wrap:wrap;
  padding:calc(8px + var(--vgap) * 0.14) var(--gutter) calc(8px + var(--vgap) * 0.22);
}
.mark{display:block;line-height:0}
.mark svg{display:block;height:clamp(64px,6vw,88px);width:auto}
.mark svg path{fill:none;stroke:var(--ink);stroke-width:3.4;stroke-linecap:round;stroke-linejoin:round}
.topbar-right{display:flex;align-items:center;gap:12px;flex-wrap:wrap}

.modeswitch{display:flex;border:1px solid var(--line);font-family:var(--f-mono)}
.modeswitch label{
  padding:9px 15px;font-size:var(--fs-micro);color:var(--muted);cursor:pointer;user-select:none;
  transition:background .12s ease,color .12s ease;
}
.modeswitch label:first-of-type{border-right:1px solid var(--line)}
.modeswitch input:checked + label{background:var(--ink);color:var(--bg)}
/* Mobile drops the human/agent switch entirely rather than duplicating it
   below the fold - the plain-text mirror is still reachable at /llms.txt
   for anything that actually wants it (a crawler, an agent), which was
   always the switch's real audience more than a phone visitor's thumb. */

/* A second copy of the theme button sits right under the topbar on mobile
   (see the ── mobile section below), rather than at the bottom of the page:
   tapping a control and having the *visible* part of the page - not a
   footer the reader has scrolled away from - change colour in response is
   what makes the toggle read as responding to you at all. Hidden here by
   default; the mobile breakpoint shows it. */
.theme-row{display:none}

/* Sun/moon toggle: icon-only so it reads as one unit with .osbtn/.modeswitch
   rather than competing with their text labels. Both icons ship in the
   button; which one is visible is pure CSS keyed off the same [data-theme]/
   prefers-color-scheme rules that drive the palette itself (assets/theme.js
   only ever sets that attribute, it doesn't touch the icons directly) - the
   icon shown is always the CURRENT theme, sun for light and moon for dark,
   not the theme a click would switch to. */
.themebtn{
  display:inline-flex;align-items:center;justify-content:center;
  width:38px;height:38px;padding:0;border:1px solid var(--line);color:var(--ink);
  background:transparent;cursor:pointer;flex:0 0 auto;
  transition:background .12s ease,color .12s ease,border-color .12s ease;
}
/* Scoped to real hover capability so a touch tap doesn't leave the button
   stuck showing its hover colours - a touchscreen fires :hover on tap but
   has no pointer-out event to clear it, so an unscoped rule here reads as
   the button "flashing" or landing in the wrong state until something else
   is tapped. */
@media (hover:hover){
  .themebtn:hover{background:var(--ink);color:var(--bg);border-color:var(--ink)}
}
.themebtn svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.themebtn .ic-sun{display:block}
.themebtn .ic-moon{display:none}
:root[data-theme="dark"] .themebtn .ic-sun{display:none}
:root[data-theme="dark"] .themebtn .ic-moon{display:block}
@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]) .themebtn .ic-sun{display:none}
  :root:not([data-theme="light"]) .themebtn .ic-moon{display:block}
}

.osbtn{
  font-family:var(--f-mono);font-size:var(--fs-micro);
  padding:9px 15px;border:1px solid var(--line);color:var(--ink);
  transition:background .12s ease,color .12s ease,border-color .12s ease;
}
.osbtn:hover{background:var(--ink);color:var(--bg);border-color:var(--ink)}

/* ── section headings, slash-command style ────────────────────────── */
.sec{
  margin:0 0 12px;font-family:var(--f-mono);font-size:var(--fs-label);font-weight:400;
  color:var(--muted);
}
.sec a{color:var(--muted);border-bottom:1px solid var(--line)}
.sec a:hover{color:var(--ink);border-bottom-color:var(--ink)}

/* ── media rows ───────────────────────────────────────────────────── */
.strip{display:flex;flex-wrap:wrap;gap:12px}
.poster,.book,.album{display:block;line-height:0;background:var(--card)}
.poster img,.book img,.album img{display:block;width:100%;height:100%;object-fit:cover}
.poster{width:clamp(72px,5.4vw,104px);aspect-ratio:2/3}
.book{width:clamp(68px,4.9vw,96px);aspect-ratio:2/3}
.album{width:clamp(64px,4.8vw,92px);aspect-ratio:1}
.poster:hover,.book:hover,.album:hover{outline:2px solid var(--accent);outline-offset:2px}

/* A fixed 19-column grid rather than flex-wrap: 38 countries divide exactly
   into two rows of 19, so the two rows end flush instead of leaving the
   second one visibly short wherever the wrap happens to land. */
/* .flag-wrap's padding-bottom:100% (which always resolves against its own
   width) is what keeps these square - aspect-ratio directly on the flag
   image measured as non-square in this context despite computing correctly,
   so this sidesteps that rather than chasing why. */
.flags{display:grid;grid-template-columns:repeat(19,1fr);gap:5px;max-width:473px}
.flag-wrap{display:block;position:relative;width:100%;height:0;padding-bottom:100%}
.flag{
  display:block;position:absolute;inset:0;width:100%;height:100%;border-radius:50%;
  object-fit:cover;background:var(--card);
}

/* ── brand icon links ─────────────────────────────────────────────── */
.icons{display:flex;flex-wrap:wrap;gap:10px}
.ico{
  display:inline-flex;align-items:center;justify-content:center;
  width:42px;height:42px;border:1px solid var(--line);
  transition:border-color .12s ease,background .12s ease;
}
.ico svg{width:19px;height:19px;display:block}
.ico:hover{border-color:var(--ink);background:var(--surface)}

.note{margin:0;font-size:var(--fs-body);line-height:1.65;color:var(--muted);max-width:46ch}
.note a{color:var(--ink);border-bottom:1px solid var(--line)}
.note a:hover{border-bottom-color:var(--ink)}

/* What /tracking uses instead of a full page: an icon standing in for the
   category (rather than a text label competing with the value for space -
   a fixed-width text column here is what let "listening" collide into the
   value next to it) and a plain one-line fact. Separated by spacing, not a
   line - a row is a real link (IMDb/Spotify/Strava) whenever a target
   exists, so it gets the same background-highlight hover as anything else
   clickable rather than a border. */
.pulse{display:flex;flex-direction:column;gap:4px}
.pulse-row{
  display:flex;align-items:center;gap:12px;padding:6px 10px;margin:0 -10px;
  border-radius:2px;transition:background .12s ease;
  width:fit-content;max-width:100%;
}
a.pulse-row:hover,a.pulse-row:focus-visible{background:var(--card)}
.pulse-row .pi{
  flex:0 0 auto;width:24px;align-self:flex-start;margin-top:3px;
  display:flex;align-items:center;justify-content:center;color:var(--accent2);
}
.pulse-row .v{flex:1;min-width:0;display:flex;align-items:center;gap:10px;font-size:var(--fs-body);color:var(--ink);text-wrap:pretty}
.pulse-row .vt{min-width:0}
.pulse-row .vt b{font-weight:600;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pulse-row .vt small{display:block;margin-top:2px;font-size:var(--fs-body);color:var(--muted)}
.pulse-row .v > small{font-size:var(--fs-body);color:var(--muted)}
.pulse-cover{flex:0 0 auto;width:32px;height:46px;object-fit:cover;background:var(--card)}

/* Animated /tracking icons. These are inline SVG rather than emoji purely so
   they can move in ways a font glyph can't - a note bobbing, a pulse tracing
   itself, a wheel turning, eyes blinking. Each animation is slow and
   low-amplitude on purpose: this is a liveness cue in peripheral vision, not
   something competing with the text beside it. */
.ic{width:18px;height:18px;display:block;overflow:visible;fill:currentColor}
.ic-pulse{width:22px}
@keyframes ic-bob{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-2.5px) rotate(-6deg)}}
.ic-note{animation:ic-bob 2.4s ease-in-out infinite;transform-origin:60% 70%}
/* The ECG line draws itself left-to-right on a loop: one dash the length of
   the whole path, walked across by animating the offset. */
@keyframes ic-trace{0%{stroke-dashoffset:62}70%,100%{stroke-dashoffset:-62}}
.ic-pulse path{stroke-dasharray:62;animation:ic-trace 2.6s linear infinite}
@keyframes ic-spin{to{transform:rotate(360deg)}}
.ic-wheel{animation:ic-spin 5s linear infinite;transform-origin:50% 50%}
/* The pin's inner ring widens and fades on a long cycle, the way a locator
   ping does on a map. Scaled with a transform rather than by animating the
   circle's own r: r is an SVG geometry property, and Safari animates none of
   those from CSS - it would simply sit still there while moving everywhere
   else. transform-box:fill-box makes "center" mean the circle's own centre
   rather than the whole 24-unit viewBox's. */
@keyframes ic-ping{0%,70%,100%{transform:scale(1);opacity:1}85%{transform:scale(1.75);opacity:0}}
.ic-pin circle{animation:ic-ping 4.2s ease-out infinite;transform-box:fill-box;transform-origin:center}
@media (prefers-reduced-motion:reduce){
  .ic-note,.ic-wheel{animation:none}
  .ic-pin circle{animation:none}
  .ic-pulse path{animation:none;stroke-dasharray:none}
}

/* The route drawing behind the homepage and /about: every run recorded
   this year, scattered one-per-cell across the viewport and flowing.
   Painted by assets/routes-bg.js from assets/routes.json - see the note at
   the top of tools/build-routes.js for why the file holds shapes and no
   coordinates.

   Fixed rather than absolute so it covers the actual viewport at any page
   length: on a phone the board is one column and four screens tall, and a
   drawing anchored to the document would spend nearly all of that scrolled
   off. Revealed on .on, which the script sets after its first real paint -
   the data arrives some way after first paint, and a drawing that snaps
   into existence a beat late is the one bit of extra movement this page
   doesn't otherwise have.

   z-index:0 rather than a negative value: a negative z-index would put it
   behind body's own background and it would never be seen at all. */
#routes-bg{
  position:fixed;inset:0;z-index:0;
  pointer-events:none;
  opacity:0;transition:opacity .6s ease;
}
#routes-bg.on{opacity:1}
@media (prefers-reduced-motion:reduce){ #routes-bg{transition:none} }
/* Everything the page actually says sits above it. These are the stacking
   contexts that matter on the two pages that carry the canvas; anything
   else inherits from body. */
.topbar,.board,.wrap,footer{position:relative;z-index:1}

/* Track titles can be long enough to truncate (soundtrack names especially).
   A script marks only the ones that actually overflow, and those scroll their
   own text instead of clipping it. The keyframe distance is per-element,
   measured and set as --sc by that same script. */
.marq{display:block;overflow:hidden}
.marq > span{display:inline-block;white-space:nowrap}
@keyframes marq{0%,12%{transform:translateX(0)}88%,100%{transform:translateX(var(--sc))}}
.marq.on > span{animation:marq 11s ease-in-out infinite alternate}
@media (prefers-reduced-motion:reduce){ .marq.on > span{animation:none} }

/* Hover/focus reveal - a short fact that does not need to sit on the page all
   the time. Positioned above by default; the timeline flips it with .tip-dn
   where there is no room above. Wraps rather than white-space:nowrap - a
   nowrap tooltip on a long data-tip (the /projects taglines) stays invisible
   until hover but still inflates the page's scrollWidth the whole time,
   which is real horizontal overflow, not a hover-only concern. */
[data-tip]{position:relative}
[data-tip]::after{
  content:attr(data-tip);position:absolute;left:50%;bottom:calc(100% + 9px);
  transform:translate(-50%,3px);
  background:var(--ink);color:var(--bg);
  font-family:var(--f-mono);font-size:11px;line-height:1.3;
  width:max-content;max-width:200px;text-align:center;
  padding:6px 10px;pointer-events:none;opacity:0;
  transition:opacity .14s ease,transform .14s ease;z-index:20;
}
/* .tip-dn is only ever used on the LukasOS button, which sits at the far
   right of the topbar - centring it (like the default) let the bubble's
   right half extend past the viewport edge even while invisible, which
   still counts as real page width and was the actual source of the
   sitewide horizontal scroll. Anchored to the trigger's own right edge and
   growing left instead removes the possibility outright rather than just
   capping the width tighter. */
[data-tip].tip-dn::after{bottom:auto;top:calc(100% + 9px);left:auto;right:0;transform:translateY(-3px)}
[data-tip]:hover::after,[data-tip]:focus-visible::after{opacity:1;transform:translate(-50%,0)}
[data-tip].tip-dn:hover::after,[data-tip].tip-dn:focus-visible::after{transform:translateY(0)}
@media (prefers-reduced-motion:reduce){
  [data-tip]::after{transition:opacity .01s linear}
}
/* The timeline's eras/segs still carry data-tip (their date range), but the
   bubble itself is switched off there: positioned above the bar, it sat
   right on top of the era name label occupying that same spot, and on the
   rightmost (ongoing) items ran into the viewport edge. The hover-wash glow
   (.eraglow/.yearband below) already carries the "you're looking at this
   one" signal without a second, colliding box. */
.tl [data-tip]::after{display:none}

.agentview{
  display:none;margin:0;padding:var(--vgap) var(--gutter);
  font-family:var(--f-mono);font-size:var(--fs-label);line-height:1.8;color:var(--ink);
  white-space:pre-wrap;word-wrap:break-word;
}
body:has(#m-agent:checked) .board{display:none}
body:has(#m-agent:checked) .agentview{display:block}

/* ── life timeline (/about) ──────────────────────────────────────────
   Every row - the era strip, each lane, the axis - is a two-column
   .tl-row: a fixed-width label on the left, the year-grid on the right.
   All year-grids share the same --tl-cols template, so a segment in any
   row lands on the same year as the same column in every other row. The
   label column (plus its gap) is --tl-gutter, reused by the .eraglow
   bands below so their left/width math lines up with the bars exactly.
   Full width, no horizontal scroll on desktop; below 760px that trade
   stops being worth it (33 columns leaves no room for a label like
   "Consulting & IB Internships" to wrap), so narrow viewports get
   scrollable behaviour back, same breakpoint the homepage board
   collapses at. */
.tl{
  --tl-cols:repeat(var(--tl-years),minmax(0,1fr));
  --tl-label-w:108px;
  --tl-gap:14px;
  /* The bands below are positioned in % of .tl's own width, so this has to
     stay in lockstep with the label column - any breakpoint that changes
     --tl-label-w or --tl-gap must be inside .tl for this to follow. */
  --tl-gutter:calc(var(--tl-label-w) + var(--tl-gap));
  position:relative;
  /* flex:1 + space-between is what actually fills the page: .tlwrap/.wrap/
     body all pass flex:1 down to here, so on a tall viewport the leftover
     room becomes real gaps between rows instead of dead space below the
     last one. This is the one property this section keeps needing re-added -
     it was dropped rewriting the palette and not put back, which is what
     left a block of empty space under a short, cramped-looking timeline. */
  flex:1;display:flex;flex-direction:column;justify-content:space-between;gap:10px;
}
.tl-row{display:grid;grid-template-columns:var(--tl-label-w) 1fr;column-gap:var(--tl-gap);align-items:center}
/* The era row's own label (.era .nm) renders ABOVE its box via absolute
   positioning, so it contributes nothing to this row's height and needs
   explicit clearance above it - and it is the first row, so that clearance
   has to come from the row itself. */
.tl-row:has(.tl-eras){margin-top:26px}
.tl-track{display:grid;grid-template-columns:var(--tl-cols);column-gap:2px}
.lane-name{
  font-family:var(--f-body);font-weight:600;font-size:15px;color:var(--muted);
  text-align:right;white-space:nowrap;
}

/* Hovering an era washes its whole year-range down through every lane, and
   hovering a year (or its age) does the same for that single year. Both use
   the same mechanism: a band that sits immediately after its trigger in the
   DOM (or inside it, for years), revealed by a plain sibling/descendant
   hover - more reliable across engines than :has(:hover), whose
   ancestor-wide style invalidation is inconsistent. Both are absolutely
   positioned against .tl (the nearest positioned ancestor, regardless of
   how deep the trigger is nested) so they span the full timeline height,
   and both are pointer-events:none so they never block hovering the bars
   underneath. Neutral white, not the orange accent: at the size of a
   multi-year era this reads as a highlight rather than a colour block
   fighting the lane palette. */
.eraglow,.yearband{
  position:absolute;top:0;bottom:0;background:var(--ink);
  opacity:0;pointer-events:none;transition:opacity .15s ease;z-index:1;
}
.era:hover + .eraglow{opacity:.09}
.yr:hover .yearband,.age:hover .yearband{opacity:.11}

/* A segment or era that runs through to the present ends on a solid
   triangle instead of a square edge - "this keeps going," not "this
   stopped here." Only the (filled) .seg bars pulse-glow, not the eras -
   the era strip is a thin outlined ruler, not a filled shape, so the same
   box-shadow glow just looked like flicker there rather than a glow. Both
   the triangle and the glow read their colour from --c, set inline per
   element by the generator, so they always match their own bar. */
@keyframes tl-glow{0%,100%{box-shadow:0 0 0 0 transparent}50%{box-shadow:0 0 14px 2px var(--c)}}
.seg.ongoing{animation:tl-glow 1.8s ease-in-out infinite}
.tip{display:none}
.era.ongoing .tip,.seg.ongoing .tip{
  display:block;position:absolute;top:0;right:-8px;width:8px;height:100%;
  background:var(--c);clip-path:polygon(0 0,100% 50%,0 100%);
}
/* The eras row's own ongoing triangle (always --accent orange, since eras
   don't get a per-lane --c override) is dropped - the lane rows below still
   get their own colour-matched arrow, but the strip at the top reads as a
   plain ruler now rather than adding a second, redundant "this is current"
   marker next to the one the lanes already show. */
.era.ongoing .tip{display:none}
@media (prefers-reduced-motion:reduce){ .seg.ongoing{animation:none} }

/* Eras are drawn in neutral grey, not the orange accent - the accent is
   reserved for the one thing that is genuinely live (the ongoing tip). */
.era{
  --c:var(--accent);
  align-self:center;position:relative;height:13px;margin:0 2px;
  border-top:2px solid var(--muted);cursor:default;
}
.era::before,.era:not(.ongoing)::after{content:"";position:absolute;top:0;width:2px;height:6px;background:var(--muted)}
.era::before{left:0}
.era:not(.ongoing)::after{right:0}
.era .tooth{position:absolute;left:50%;top:0;width:2px;height:10px;background:var(--muted);transform:translateX(-50%)}
.era .nm{
  position:absolute;bottom:100%;left:0;right:0;margin-bottom:7px;text-align:center;
  font-family:var(--f-body);font-size:15px;font-weight:600;line-height:1.3;color:var(--ink);
  overflow-wrap:break-word;padding:0 2px;
}

/* Axis: a thin tick ruler, then a Year row and an Age row, each with its
   own left-side label like every lane has. Ticks mark year BOUNDARIES while
   the numbers span the full year between two of them, so each label sits
   centred inside the year it names rather than hugging one edge of it. */
/* Ticks/Year/Age are grouped in their own flex column with a small fixed
   gap, pulled out of .tl's own flex/space-between distribution - otherwise
   they'd each get the same large, page-filling gap as the lane rows above,
   even though they're meant to be read together as one axis. */
.tl-axis{display:flex;flex-direction:column;gap:3px}
.tl-ticks{height:9px}
.tick{position:relative;border-left:1px solid var(--line);align-self:stretch}
.tick.maj{border-left-color:var(--muted)}
.tick.last{border-right:1px solid var(--line)}
.yr,.age{
  font-family:var(--f-body);color:var(--muted);white-space:nowrap;text-align:center;
}
.yr{font-size:13px}
.age{font-size:13px;opacity:.85}
.yr.maj,.age.maj{color:var(--ink);font-weight:700}
/* Every whole year gets a column, but only the life-stage-relevant ones
   (.maj) get full size/weight - the rest stay legible but visibly secondary. */
.yr:not(.maj),.age:not(.maj){font-size:10px;opacity:.5}

/* Slim, colour-coded bars with the segment name set directly on the fill in
   white - each .seg gets its colour inline (--c: one hue per lane, a
   lightness step per segment) from the generator, not from here. */
.seg{
  height:34px;background:var(--c);position:relative;cursor:default;border-radius:2px;
  display:flex;align-items:center;justify-content:center;
}
.seg .nm{
  font-family:var(--f-body);font-size:13px;font-weight:600;line-height:1.2;color:#fff;
  text-align:center;overflow-wrap:break-word;padding:0 6px;
}
.seg:hover,.seg:focus-visible{filter:brightness(1.3)}

/* ── life timeline (/about), mobile ───────────────────────────────────
   The desktop grid (33 year-columns × 6 lanes) genuinely needs the width -
   a vertical reshuffle was tried here and traded away the thing that made
   the timeline worth having (seeing every lane against the same year at a
   glance), so this asks for the width instead of working around not having
   it. Portrait phones under 900px get a rotate prompt in .tl-rotate's
   place; landscape keeps the real grid, horizontal scroll and all, same as
   a narrow desktop window gets below the breakpoint further down. */
.tl-rotate{
  display:none;flex-direction:column;align-items:center;gap:16px;
  padding:56px 24px;text-align:center;color:var(--muted);
}
@media (max-width:900px) and (orientation:portrait){ .tl-rotate{display:flex} }
/* .tlwrap's own display:flex is declared in about/index.html's page-level
   <style>, which loads after this file - same specificity there beats a
   media-conditional rule here on source order alone regardless of which
   query actually matches, so the "hide it in portrait" half of this toggle
   has to live next to that declaration instead of here. See .tlwrap in
   about/index.html. */
.tl-rotate svg{
  width:40px;height:40px;stroke:currentColor;fill:none;stroke-width:1.5;
  stroke-linecap:round;stroke-linejoin:round;
  animation:tl-rotate-hint 2.4s ease-in-out infinite;
}
@keyframes tl-rotate-hint{0%,42%,100%{transform:rotate(0)}70%,88%{transform:rotate(-90deg)}}
.tl-rotate p{margin:0;font-size:15px;line-height:1.5;max-width:32ch}
@media (prefers-reduced-motion:reduce){ .tl-rotate svg{animation:none} }

/* Landscape phones are still under 900px wide (a 14 Pro is ~852px on its
   long edge), so the desktop grid's own ≥900px assumption doesn't clear
   just because the device rotated - it gets the same shrunk-labels,
   scrollable treatment a narrow desktop window would. A flat 820px floor
   here used to give a 66-column, decades-spanning grid about 12px per
   column regardless of how much content actually needed to fit in it, so
   segment and era labels overlapped their neighbours. --tl-years is already
   set inline on .tl per page, so scaling the floor by it (28px per half-
   year column) gives the grid the same "real width, not squeezed" treatment
   the desktop layout gets for free from a wide viewport, and it stays
   correct if the timeline's span ever changes. */
@media (max-width:900px) and (orientation:landscape){
  .tl{--tl-label-w:74px;--tl-gap:10px;overflow-x:auto;overflow-y:visible;padding-bottom:4px}
  .lane-name{font-size:12px;white-space:normal}
  .tl-track{min-width:calc(var(--tl-years) * 28px)}
}

/* Wordmark logos standing in for a company's name in running text. Every
   supplied file is dark-on-transparent, which already reads correctly on a
   light background - only dark mode needs the invert-to-white. The hover
   wash is mixed from --accent2 itself (color-mix, not a fixed rgba) so it
   tracks whichever accent the active theme is actually using instead of
   staying the dark palette's cyan on a light background. The underline sits
   on the link rather than the image so it still reads as a link the way the
   surrounding text links do. */
.logolink{display:inline-flex;align-items:center;vertical-align:baseline;border-bottom:1.5px solid var(--accent2)}
.logolink:hover{background:color-mix(in srgb,var(--accent2) 14%,transparent)}
.wm{width:auto;display:block;opacity:.92}
:root[data-theme="dark"] .wm{filter:brightness(0) invert(1)}
@media (prefers-color-scheme:dark){ :root:not([data-theme="light"]) .wm{filter:brightness(0) invert(1)} }
.logolink:hover .wm{opacity:1}

/* ── footer ───────────────────────────────────────────────────────── */
footer{padding:10px var(--gutter) 14px}
.foot{
  display:flex;justify-content:space-between;align-items:baseline;gap:10px 20px;flex-wrap:wrap;
  font-size:var(--fs-micro);color:var(--muted);
}

/* ── mobile ───────────────────────────────────────────────────────────
   The theme toggle moves out of the topbar into its own row directly under
   it, in normal document flow (not position:fixed, so it scrolls away with
   the rest of the page). The human/agent switch is dropped rather than
   moved - see the note by .theme-row above. The topbar keeps just /about
   and LukasOS, which now have the whole row to themselves next to the
   signature (nowrap so they never drop to their own line under it). */
@media (max-width:700px){
  .topbar{padding:6px var(--gutter) 14px;gap:10px 12px;flex-wrap:nowrap}
  .mark svg{height:clamp(42px,12vw,58px)}
  .osbtn{padding:6px 9px;font-size:11px}
  /* .topbar's own nowrap only stops it wrapping against the signature -
     .topbar-right is a flex container in its own right and kept its base
     flex-wrap:wrap, so /about and LukasOS were free to wrap onto their own
     second line the moment they didn't quite fit the space left after the
     signature, which they didn't on anything around 360px wide. */
  .topbar-right{flex-wrap:nowrap;gap:8px}
  .topbar .modeswitch,.topbar .themebtn{display:none}
  /* Right-aligned so it sits directly under LukasOS, the button closest to
     it in the topbar above - not centred, which would float it under
     nothing in particular. */
  .theme-row{display:flex;justify-content:flex-end;padding:0 var(--gutter) 14px}
  .agentview{padding:var(--gutter);font-size:13px;line-height:1.7}
}
/* Signature + two nav buttons is a tight fit under ~380px even with the
   above - a smaller signature buys back enough room without shrinking the
   buttons' own touch target below 44px. */
@media (max-width:380px){
  .mark svg{height:36px}
  .osbtn{padding:6px 7px;font-size:10.5px}
}

/* This estate does not load /assets/touch.css, so the 44px floor the rest of
   the site enforces is reproduced here, scoped to coarse pointers. */
@media (pointer:coarse){
  .ico{width:48px;height:48px}
  .mark,.sec a{display:inline-flex;align-items:center;min-height:44px}
  .note a{display:inline-block;padding:12px 0;margin:-12px 0}
  /* .osbtn's own padding shrinks a couple of times as the topbar narrows
     (see the mobile breakpoints below) to keep /about and LukasOS from
     wrapping onto their own line - none of those steps were ever checked
     against this floor, so the visible box kept shrinking well under it. */
  .osbtn{display:inline-flex;align-items:center;min-height:44px;box-sizing:border-box}
  .themebtn{width:44px;height:44px}
}
@media (prefers-reduced-motion:reduce){
  *{animation:none!important;transition:none!important}
}
