/* =====================================================================
   TOKENS
   - Palette: "Cool Grayscale" 8-gray scale (slight blue tint), gray-50
     (lightest) to gray-900 (darkest). Replaces the old 5-color palette
     (white/black/3 neutral grays) — see roles (bg/surface/text/muted)
     below, which now vary by theme in step CHOICE too, not just role name.
   - Typography: one size scale, reused by every component.
   - Radius: one border-radius scale.
   ===================================================================== */
:root{
  /* raw palette — don't use directly in components, only to define the roles below */
  --gray-50:#ffffff;
  --gray-100:#f0f0f0;
  --gray-300:#c8c8c8;
  --gray-400:#ababab;
  --gray-600:#636363;
  --gray-700:#414141;
  --gray-800:#1a1a1a;
  --gray-900:#000000;

  /* raw numeric scale — the site's second primitive, alongside the color palette above.
     Each --dim-N is just the number N (unitless): every semantic size token below
     (typography, radius, icon, sizing) derives from one of these via calc(), never a raw
     px/rem. calc(var(--dim-N) * 1px) gives pixels; calc(var(--dim-N) * 1rem / 16) gives
     rem while staying relative to the user's font-size preference (not a disguised px —
     the /16 cancels out, leaving just the ratio, so it still scales with root font-size).
     Don't use --dim-N directly in a component — only to define the semantic tokens
     below, same rationale as the raw color palette. */
  --dim-4:4;
  --dim-8:8;
  --dim-12:12;
  --dim-14:14;
  --dim-16:16;
  --dim-18:18;
  --dim-20:20;
  --dim-24:24;
  --dim-26:26;
  --dim-28:28;
  --dim-32:32;
  --dim-34:34;
  --dim-36:36;
  --dim-40:40;
  --dim-44:44;
  --dim-48:48;
  --dim-56:56;
  --dim-64:64;
  --dim-66:66;
  --dim-80:80;
  --dim-100:100;
  --dim-112:112;
  --dim-360:360;
  --dim-430:430;
  --dim-999:999;
  --dim-1124:1124;

  /* color roles (light) */
  --bg:var(--gray-100);
  --surface:var(--gray-50); /* form controls and "elevated" content (card, modal, timeline
     badge) — a single token: in both themes they always rendered the same, so the old
     --surface-2 was just a second name for the same value. If they ever need to diverge,
     reintroduce --surface-2 here (not before — a token with zero difference today just confuses). */
  --text:var(--gray-900);
  /* gray-600 (#636363): 6.0:1 on white surface, 5.3:1 on gray-100 bg — passes AA everywhere */
  --muted:var(--gray-600);
  /* one step dimmer than --muted, pulled toward --bg — not a new raw gray, always a mix
     of these two roles, so it inherits the same per-theme inversion automatically (used
     for "past" timeline badges/line, see .exp-logo/.timeline-connector) */
  --muted-dim:color-mix(in srgb,var(--muted) 85%,var(--bg));
  --border:color-mix(in srgb,var(--gray-900) 14%,transparent);
  --border-on-text:color-mix(in srgb,var(--gray-50) 18%,transparent);

  /* single shadow scale — every elevation box-shadow on the site references one of these 4 levels */
  --shadow-resting:0 24px 48px -24px color-mix(in srgb,var(--gray-900) 22%,transparent);
  --shadow-hover:0 36px 64px -28px color-mix(in srgb,var(--gray-900) 34%,transparent);
  --shadow-pressed:0 14px 30px -20px color-mix(in srgb,var(--gray-900) 28%,transparent);
  --shadow-fab:0 4px 16px color-mix(in srgb,var(--gray-900) 35%,transparent);

  /* single duration/easing for every theme-linked color change, keeping everything in sync */
  --theme-transition:var(--duration-slower) ease;
  /* curve used for every motion/size transition on the site (used to be repeated identically in ~10 rules) */
  --ease-smooth:cubic-bezier(.2,.7,.3,1);
  /* single scale factor for every BUTTON's :active state on the site (not links, not
     .card — see the consolidated rule right after :focus-visible below). Each used to
     have its own value (.94/.96/.99) or no tap feedback at all; now one value, one place. */
  --press-scale:.96;
  /* the case-image lightbox is always dark (a near-black scrim over the page), in both themes — so these
     are fixed colors from the raw palette, not --text/--surface, which would flip with the theme */
  --lightbox-scrim:color-mix(in srgb,var(--gray-900) 92%,transparent);
  --lightbox-fg:var(--gray-50);
  --lightbox-border:color-mix(in srgb,var(--gray-50) 30%,transparent);
  --lightbox-btn-bg:color-mix(in srgb,var(--gray-50) 8%,transparent);
  --lightbox-btn-bg-hover:color-mix(in srgb,var(--gray-50) 18%,transparent);
  /* motion durations: the whole site picks from these instead of a bare .2s/.3s (script.js reads
     them back for the few setTimeouts that wait on a CSS transition, so the two can't drift) */
  --duration-fast:.15s;
  --duration-base:.2s;
  --duration-md:.25s;
  --duration-slow:.3s;
  --duration-slower:.4s;
  /* stacking order of the page-level layers, lowest to highest. Local stacking inside a component
     (z-index 0/1/2/5/20 next to a sibling) stays a plain number — it only competes within its parent. */
  --z-modal:40;
  --z-modal-progress:45;
  --z-viewer:48;
  --z-header:50;
  --z-mobile-menu:55;
  --z-fab:60;
  --z-lightbox:100;
  --z-skip-link:200;

  --font-heading:"Barlow Condensed",sans-serif;
  --font-body:"Geist",sans-serif;

  /* single type scale — every font-size on the site references one of these.
     In rem (scales with the browser/user font-size preference), and each value lands on
     a 4px multiple at the default 16px base, with one exception: --text-sm is aligned to
     --text-base itself (16px) instead of inventing a second off-grid size at that step. */
  --text-2xs:calc(var(--dim-12) * 1rem / 16);       /* 12px — all-caps labels (company, role, role/team/duration) */
  --text-xs:calc(var(--dim-14) * 1rem / 16);        /* 14px — .skill-pill label */
  --text-sm:var(--text-base);                        /* 16px — secondary text/card descriptions */
  --text-base:calc(var(--dim-16) * 1rem / 16);      /* 16px — buttons, metadata values */
  --text-md:calc(var(--dim-18) * 1rem / 16);        /* 18px — subtitles, highlighted paragraphs */
  --text-md-lg:calc(var(--dim-20) * 1rem / 16);     /* 20px — mobile quote, mid-level highlighted text */
  --text-lg:calc(var(--dim-28) * 1rem / 16);        /* 28px — case section titles in the project modal */
  --text-2xl:calc(var(--dim-36) * 1rem / 16);       /* 36px — modal header title, card/timeline titles on mobile */
  --text-logo:calc(var(--dim-26) * 1rem / 16);       /* 26px — the TG. logo; rem like the rest of the scale, so it follows the browser's font-size preference */
  --text-xl:calc(var(--dim-44) * 1rem / 16);        /* 44px — section titles, list item */

  /* semantic role token, not a scale step: every body-copy paragraph (.m-p/.m-list,
     .about-p, .exp-desc, .card-desc, resume card text) references this
     instead of --text-md directly, so resizing body text site-wide is a one-line change
     here instead of touching every consuming class. Currently aliases --text-md (18px) —
     .section-subtitle/.m-subtitle intentionally keep referencing --text-md directly,
     since they're a different role (subtitle) that only coincidentally shares the same
     size today. See docs/design-system.md. */
  --text-body:var(--text-md);
  /* same idea for color: the one color of body-copy paragraphs (same consumers as --text-body).
     A per-theme role like --muted: one step stronger than --muted for long-form reading, by the same
     proportion in both themes (light: gray-700 vs --muted's gray-600; dark: gray-300 vs gray-400, set in
     [data-theme="dark"] below). Labels/metadata keep --muted. Don't override it on a single paragraph class. */
  --text-body-color:var(--gray-700);

  --tracking-caps:.14em; /* single letter-spacing for every all-caps label — not a
     dimension from the scale above (it's a tracking coefficient in em, not a px/rem
     count), see documented exception in design-system.md */

  /* single radius scale — every border-radius on the site references one of these */
  --radius-sm:calc(var(--dim-8) * 1px);
  --radius-md:calc(var(--dim-12) * 1px);
  --radius-lg:calc(var(--dim-16) * 1px);
  --radius-xl:calc(var(--dim-24) * 1px);
  --radius-full:calc(var(--dim-999) * 1px);

  /* ===== Spacing — three tiers (see "Spacing" in docs/design-system.md) =====
     Tier 1, --dim-N (above): raw numbers. Tier 2, the scale below: the ONLY spacing values a rule may use
     (4px base, steps 4/8/12/16/24/32/48/64/80 — no 6/10/14/20/28/60, those were drift). Tier 3, the
     patterns after it: named by ROLE (what the space is for), built from the scale, and the only
     thing that changes per breakpoint — so a component says "card inset", not "36px", and
     desktop→mobile behaviour lives in one place. Related things sit close, unrelated far
     (Atlassian/NN-g), and every step is on the grid. */
  --space-4:calc(var(--dim-4) * 1px);
  --space-8:calc(var(--dim-8) * 1px);
  --space-12:calc(var(--dim-12) * 1px);
  --space-16:calc(var(--dim-16) * 1px);
  --space-24:calc(var(--dim-24) * 1px);
  --space-32:calc(var(--dim-32) * 1px);
  --space-48:calc(var(--dim-48) * 1px);
  --space-64:calc(var(--dim-64) * 1px);
  --space-80:calc(var(--dim-80) * 1px);
  /* stack: the gap between siblings stacked inside one component. Same three densities as
     Primer's stack-gap (condensed/normal/spacious = 8/16/24); use --space-4 straight for list items. */
  --stack-tight:var(--space-8);   /* eyebrow under a title, icon + label, tags */
  --stack-normal:var(--space-16); /* title block → body, button pairs, entry blocks */
  --stack-loose:var(--space-24);  /* paragraphs, blocks inside a card, title → content */
  /* inset: padding inside a container. --card-inset is for content cards (project, reference,
     resume, case intro); it steps down to 24 at ≤880px (see the mobile :root override). The compact
     inset */
  --card-inset:calc(var(--dim-36) * 1px);
  /* layout: one gap for every grid of cards/fields/columns, at every breakpoint. */
  --grid-gap:var(--space-24);
  /* bottom distance of the hero CTA from the hero's edge (desktop; the mobile hero overrides it) */
  --hero-cta-bottom:calc(var(--dim-112) * 1px);

  --header-h:calc(var(--dim-66) * 1px);
  /* default square tap/click target (icon button, badge) — same value repeated across
     several components (menu-toggle, modal-back-link, hero scroll cue) */
  --tap-size:calc(var(--dim-44) * 1px);
  --phone-w-min:calc(var(--dim-360) * 1px); /* narrowest / widest logical width of the simulated phone in the prototype */
  --phone-w-max:calc(var(--dim-430) * 1px); /* viewer (a small real phone: 360; a large one: 430) — below/above it the phone is scaled, not resized */
  --btn-h:calc(var(--dim-56) * 1px);
  /* theme/language switch: one segment and the thumb sliding under it (circles of the same size) */
  --switch-seg:calc(var(--dim-36) * 1px);
  /* timeline company badge: 64px on desktop/tablet, --btn-h on mobile */
  --exp-logo-size:calc(var(--dim-64) * 1px);
  /* fade at both ends of the marquee/ticker masks (half of it on mobile) */
  --marquee-fade:var(--space-80);
  /* height for pill-shaped components smaller than --btn-h (e.g. .skill-pill) */
  --pill-h:calc(var(--dim-32) * 1px);

  /* single icon size scale (.icon-sprite) — same spirit as the type scale: every svg
     width/height on the site references one of these 3 steps. --icon-base is
     .icon-sprite's own default size (the --text-base of the icon scale). Consolidated
     from 5 down to 3 steps: the old --icon-sm (14px) and --icon-md (16px) were absorbed
     into --icon-md-lg (18px), and --icon-base moved from 20px to 24px — two values too
     close together to justify separate tokens. */
  --icon-md-lg:calc(var(--dim-18) * 1px);
  --icon-base:calc(var(--dim-24) * 1px);
  --icon-lg:calc(var(--dim-40) * 1px);

  /* Vertical padding (top+bottom) of the case/resume modal's scroll, same across all three breakpoints
     (no responsive override, unlike the old --section-gap this token replaces). */
  --section-pad-y:var(--space-80);
  /* Main page sections (About … Contact) carry NO top padding; the only breathing room is this bottom
     padding, so two neighbouring sections are 100px apart (it used to be 80 + 80). The hero keeps its own
     spacing. --section-pad-y stays for the gaps between case sections (.m-h, .m-intro-cards). */
  --section-pad-bottom:calc(var(--dim-100) * 1px);
  /* distance between the header's bottom edge and a section's top edge after an anchor jump / nav click
     (scroll-margin-top, and the offset in script.js, which reads this token) */
  --section-scroll-gap:calc(var(--dim-36) * 1px);
  /* case opening: title → cover image and cover image → opening line are the same distance (measured to the visible
     text, the two texts are trimmed to their caps) */
  --case-intro-gap:var(--space-32);
  /* what sticks to the navbar while a case scrolls (back button, index): this far under it. Sticky offsets are measured from
     .modal-scroll's content edge, i.e. after its --section-scroll-gap padding, so the `top` values subtract that padding */
  --sticky-gap:var(--space-64);
  /* space between a section's header (.section-marker) and its main content — same
     value is the distance above every CTA group (--cta-offset below) */
  --section-content-gap:var(--space-48);
  /* CTA groups (hero, About, Aipim, the resume's bottom pair): one gap between buttons, in both directions
     when a group wraps or stacks, and one distance from the content above to the group */
  --cta-gap:var(--stack-normal);
  --cta-offset:var(--section-content-gap);
  --wrap-max:calc(var(--dim-1124) * 1px);
  /* single gutter: side spacing from .wrap/bleed to the viewport edge AND the
     column-gap of the 12/8/4 grids (.projects/.references-grid/.contact-fields/.exp-row)
     — same value for both uses, redefined alongside the column count in the
     tablet/mobile breakpoints below (see @media (min-width:621px) and (max-width:880px)
     and @media (max-width:620px)). */
  --wrap-gutter:var(--space-24);
  /* mobile FAB: size and distance from the bottom — referenced by the menu that floats above it */
  --fab-size:calc(var(--dim-64) * 1px);
  --fab-offset:calc(var(--dim-24) * 1px);
  /* fixed-color fill for .btn-dark-outline and .modal-back-link's ≤1100px icon-only
     variant (see both below) — shared here so the two don't repeat the same color-mix()
     values independently. */
  --dark-outline-bg:color-mix(in srgb,var(--gray-900) 70%,transparent);
  --dark-outline-bg-hover:color-mix(in srgb,var(--gray-900) 85%,transparent);
  --dark-outline-border:color-mix(in srgb,var(--gray-50) 20%,transparent);
}
[data-theme="dark"]{
  /* roles inverted for dark theme: gray-400 (#ababab) as muted gives 9.2:1 on the black
     bg and on surface (#000000) — passes AA in both contexts. */
  --bg:var(--gray-800);
  --surface:var(--gray-900);
  --text:var(--gray-50);
  --muted:var(--gray-400);
  /* a step brighter than --muted for long-form reading on the pure black surface; --muted stays
     for labels/metadata so the hierarchy between them keeps its gap */
  --text-body-color:var(--gray-300);
  --border:color-mix(in srgb,var(--gray-50) 16%,transparent);
  --border-on-text:color-mix(in srgb,var(--gray-900) 14%,transparent);

  --shadow-resting:0 24px 48px -24px color-mix(in srgb,var(--gray-900) 60%,transparent);
  --shadow-hover:0 36px 64px -28px color-mix(in srgb,var(--gray-900) 80%,transparent);
  --shadow-pressed:0 14px 30px -20px color-mix(in srgb,var(--gray-900) 70%,transparent);
  --shadow-fab:0 4px 16px color-mix(in srgb,var(--gray-900) 60%,transparent);
}

/* Mobile Chrome shows a translucent blue highlight on any tap target (link, button, the
   switch...) by default — without this, it's gone. Normal :focus-visible (outline)
   still works for keyboard nav. */
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}

/* Locks out any unwanted horizontal scroll on the document as a whole. Some out-of-flow
   elements (logo-measure, skip-link) can have a natural content width that, on very
   narrow screens, exceeds the viewport even while off-screen or invisible — an
   absolute/fixed element with no overflow:hidden ancestor still counts toward the
   document's "scrollable" width. The site's intentional horizontal scrolls
   (.modal-scroll) already control their own overflow-x and aren't affected by this. */
html,body{overflow-x:hidden}
/* Permanently reserves the vertical scrollbar's space — without this, opening the case
   modal locks scroll (overflow:hidden in setScrollLocked, script.js), the scrollbar
   disappears, the page gets a few pixels wider, and everything (the navbar especially,
   being fixed full-width) jumps right at that instant. */
html{scrollbar-gutter:stable}
/* 20x20 is the default size for nearly every icon on the site — only the few that need
   a different value (.exp-logo svg, .copy-tooltip svg etc.) override it via a more
   specific selector. */
.icon-sprite{fill:currentColor;width:var(--icon-base);height:var(--icon-base)}
.fab-nav-icon{display:none}

/* Anyone with "reduce motion" on at the system level doesn't want any of the site's CSS
   transitions/animations (logo hover, theme swap, hero swap, modal transition, etc.) —
   zeroes out all their durations at once, instead of each component handling it alone.
   Smooth scroll on nav link clicks is done via JS (smoothScrollTo in script.js), which
   also checks this same preference before animating. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    transition-duration:0s !important;
    animation-duration:0s !important;
    scroll-behavior:auto !important;
  }
}

/* Sits off-screen until it receives focus via Tab — the page's first focusable element,
   letting keyboard/screen-reader users skip the sidebar and jump straight to content. */
.skip-link{
  position:absolute;top:-100px;left:16px;z-index:var(--z-skip-link);
  background:var(--text);color:var(--surface);
  padding:var(--space-12) var(--space-24);border-radius:var(--radius-md);
  font-size:var(--text-sm);font-weight:600;text-decoration:none;
  transition:top var(--duration-base) var(--ease-smooth);
}
.skip-link:focus-visible{top:16px}

/* Content present for DOM/screen readers but removed from visual rendering — used on
   the hero's real <h1> (the running-text version of the title), since what shows on
   screen there is the decorative marquee (aria-hidden, see .hero-marquee). */
.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;
}

/* Scrollbars follow the theme tokens (Firefox/new Chrome via scrollbar-color,
   Safari/Chrome via ::-webkit-scrollbar). scrollbar-color/width are inheritable, but
   some engines only apply them reliably when declared directly on the scrolling element
   — hence repeating it on every container with its own overflow (sidebar/topbar, mobile
   drawer, modal body) instead of relying on inheritance from html alone. */
html,
body > header,
.mobile-menu,
.modal-scroll,
.field textarea{scrollbar-width:thin;scrollbar-color:var(--muted) transparent}
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--muted);border-radius:var(--radius-full)}
::-webkit-scrollbar-thumb:hover{background:var(--text)}
/* smooth scroll is done via JS (smoothScrollTo in script.js) so it isn't forced instant
   by the system's "reduce motion"; keeping scroll-behavior:smooth here would make the
   browser try to animate on top of every scrollTo() call from the script itself, fighting it. */
.noscript-msg{padding:var(--space-64) var(--space-24);text-align:center;font-size:var(--text-md)}
body{
  font-family:var(--font-body);
  font-weight:400;
  line-height:1.5;
  color:var(--text);
  background:var(--bg);
  min-height:100vh;
  -webkit-font-smoothing:antialiased;
  transition:background-color var(--theme-transition),color var(--theme-transition);
  user-select:none;
}
/* prose — readable content that should remain selectable */
.card-desc,
.exp-desc,
.about-p,
.reference-quote,
.m-subtitle,
.mh-title,
.eyebrow,
.m-meta,
.m-body,
.m-p,
input,textarea{user-select:text}
/* heading reset: prose line-height (above, on body) is loose on purpose (1.5, an
   accessibility recommendation for text blocks) but too wide for a large/bold title —
   without this, any h1/h2/h3 that doesn't set its own line-height (several don't:
   .section-title, .card-title, .m-h...) would inherit 1.5 and open an odd visual gap
   between wrapped lines. Anything that already sets its own value (.exp-title:1.05,
   .mh-title:1.12 etc.) still wins via normal cascade order. */
h1,h2,h3{line-height:1.15}
.wrap{width:min(var(--wrap-max),100% - 2 * var(--wrap-gutter));margin:0 auto}
/* Sheet de sprites inline (logo depois do <body>, ver index.html): sem position:absolute ele é um
   elemento inline e abre uma linha de ~24px antes do <main>, empurrando o hero 24px pra fora
   da tela (o CTA no rodapé do hero ficava cortado). */
svg.hidden{position:absolute}
#mainContent{margin-top:var(--header-h)}
#sobre,#projetos,#historico,#referencias,#contato{scroll-margin-top:calc(var(--header-h) + var(--section-scroll-gap))}

/* =====================================================================
   SHARED PATTERNS
   The three effects below used to repeat almost identically across
   several components; centralized here once.
   ===================================================================== */

/* Small all-caps label (company/year, project role, reference author, etc.) */
.eyebrow,
.m-meta .k,
.exp-company,
.exp-time,
.reference-name,
.reference-role,
.section-number,
.exp-toggle span,
.m-nav a,
.resume-item-meta,
.resume-eyebrow,
.nav-links a,
.seg{
  font-size:var(--text-2xs);
  font-weight:600;
  text-transform:uppercase;
  letter-spacing:var(--tracking-caps);
  color:var(--muted);
  transition:color var(--theme-transition);
}

.exp-company,
.exp-toggle span{
  text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px;
  transition:color var(--theme-transition);
}
.exp-company:hover,
.exp-company:focus-visible{color:var(--text)}

/* .wrap inside the modal becomes the same 12-column grid as the rest of the site
   (repeat(12,1fr), column-gap:24px — see .projects/.references-grid/.contact-fields for
   other uses) instead of the calc() approximation .m-content uses today (only here —
   .wrap is a global utility everywhere else, hence scoping via .modal-scroll). 3 explicit
   rows: (1) just the title, full width minus the space reserved on the right for
   .m-side (same logic as .m-content below); (2) the back button (columns 1-2, the left
   gutter) beside .m-intro (full columns 1-12 — but .m-content inside it already jumps to
   column 3 via margin-left, so in practice it doesn't overlap the button, just occupies
   the same empty gutter space); (3) from there, .m-layout/.m-project-nav stack normally,
   each on its own row (auto-flow, no explicit grid-row needed). The meta/tags column
   (.m-side) stays as-is, outside this grid — fixed width inside .m-intro, not sticky
   (see comment further below on .m-content).
   CRITICAL: .modal-back-link must be a direct child of this grid (not a small wrapper
   dedicated to just the button) — position:sticky can only "stick" while scrolling
   within its OWN parent container's bounds; a small wrapper would break sticky almost
   immediately. As a direct child of .wrap (which spans the entire case page), the button
   has that whole space to stay stuck through the full scroll. */
/* DIRECT child (>), not any descendant: since the real footer gets reattached inside
   #modalScroll when a project opens (see renderModal in script.js), it also carries its
   own internal `.wrap`s (.skills-section, .site-footer-inner) — any descendant would
   catch those too and force the 12-column grid onto them, never the intended footer
   layout. The case content's `.wrap` is always a direct child of #modalScroll (see
   comment above), so > alone distinguishes it. Scoped by ID (not the .modal-scroll
   class, which #resumeModalScroll also carries): this 3-row grid (.mh-text/.m-intro/
   .m-layout) is specific to the project case layout — the resume (see .resume-head/
   .resume-grid below) has its own internal grid and shouldn't inherit this one. */
#modalScroll > .wrap{
  display:grid;
  grid-template-columns:repeat(12,1fr);
  column-gap:var(--space-24);
  align-items:start;
}
.mh-text{grid-row:1}
.modal-back-link,
.m-intro{grid-row:2}
.m-layout{grid-column:1 / -1}
/* same 8 center content columns (see .m-content/.m-side in @media (min-width:1101px)
   below) — stays aligned with the case's text/body instead of spanning full width. */
.m-project-nav{grid-column:3 / 11}
/* The project's single back button: a text pill, sitting in columns 1-2 (the left
   gutter), on the same grid row as .m-intro — aligned with the start of the content
   (cover image), not the title — and sticks to the top via position:sticky, same
   mechanic as the right-side menu (.m-nav, see .m-layout below), just on the opposite
   side. It's sticky from the first render (top:0 already meets .modal-scroll's
   padding-top edge), not only after scrolling. */
.modal-back-link{
  grid-column:1 / 3;justify-self:start;align-self:start;
  display:inline-flex;align-items:center;gap:var(--space-8);
  width:fit-content;
  height:var(--btn-h);
  padding:0 var(--space-24) 0 var(--space-16);
  border-radius:var(--radius-md);
  border:1px solid var(--border);
  background:none;color:var(--text);
  cursor:pointer;
  position:sticky;top:calc(var(--sticky-gap) - var(--section-scroll-gap));z-index:2;
  transition:background-color var(--duration-base) ease,color var(--theme-transition),border-color var(--theme-transition);
}
.modal-back-link svg{width:var(--icon-base);height:var(--icon-base);flex:none}
.modal-back-link span{
  font-size:var(--text-2xs);font-weight:600;text-transform:uppercase;letter-spacing:var(--tracking-caps);
}
/* Inverts text/background on hover. @media(hover:hover) prevents :hover from getting
   "stuck" after a touchscreen tap (the browser simulates hover on tap and only clears
   it on the next tap elsewhere, or never) — same pattern already used in .card:hover. */
@media (hover:hover){
  .modal-back-link:hover{background:var(--text);color:var(--surface)}
}

/* Sliding bar that tracks the active item (header nav underline, .nav-indicator;
   vertical bar in the project modal menu, .m-nav-indicator) — only the dimension/axis
   of movement differs between the two, set alongside each component. */
.nav-indicator,
.m-nav-indicator{
  position:absolute;top:0;left:0;
  border-radius:var(--radius-full);
  background:var(--text);
  opacity:0;pointer-events:none;
}
.nav-indicator.is-visible,
.m-nav-indicator.is-visible{opacity:1}

/* ---------- Header — fixed on desktop, static (scrolls with page) on mobile. */
header{
  position:fixed;top:0;left:0;right:0;z-index:var(--z-header);
  background:var(--bg);
  box-shadow:var(--shadow-resting);
  transition:background-color var(--theme-transition),box-shadow var(--theme-transition);
}
.header-inner{
  display:flex;flex-direction:row;align-items:center;
  height:var(--header-h);width:min(var(--wrap-max),100% - 96px);margin:0 auto;
  gap:var(--space-48);
}
.header-top{display:flex;align-items:center}
.mobile-menu{display:flex;align-items:center;flex:1}
.logo{
  display:inline-flex;align-items:baseline;flex:none;
  /* rem instead of px: every other text on the site scales with the browser's font-size
     preference (the entire type scale is in rem) — a fixed px would leave just the logo
     out of that scaling. */
  font-family:var(--font-heading);font-size:var(--text-logo);text-box:trim-both cap alphabetic;
  text-transform:uppercase;
  color:var(--text);text-decoration:none;transition:color var(--theme-transition);
}
/* width locked via JS (measured from the expanded text) so hover doesn't push the nav */
.logo-measure{position:absolute;left:0;top:0;visibility:hidden;pointer-events:none;white-space:nowrap}
.logo-fixed{display:inline-block}
.logo-grow{
  display:inline-grid;grid-template-columns:0fr;overflow:hidden;
  transition:grid-template-columns .5s var(--ease-smooth);
}
.logo:hover .logo-grow{grid-template-columns:1fr}
.logo-grow-inner{display:flex;overflow:hidden;min-width:0}
.lt{
  display:inline-block;opacity:0;transform:translateX(-8px);
  transition:opacity var(--duration-slow) ease,transform var(--duration-slow) ease;
  transition-delay:calc(var(--d) * 35ms);
}
.logo:hover .lt{opacity:1;transform:translateX(0)}
.nav-links{display:flex;flex-direction:row;align-items:center;gap:var(--space-24);position:relative;margin-left:auto}
/* Font/color/transition come from the shared selector up top (see ".eyebrow, ...
   .nav-links a, .seg" near the top of the file) — here only what diverges from the group. */
.nav-links a{text-decoration:none}
.nav-links a:hover,.nav-links a.active{color:var(--text)}
/* sliding 2px underline that follows the active section link */
.nav-indicator{
  height:2px;width:var(--ind-w,0px);
  transform:translate(var(--ind-x,0px),var(--ind-y,0px));
  transition:transform var(--duration-slower) var(--ease-smooth),width var(--duration-slower) var(--ease-smooth),opacity var(--duration-slow) ease,background-color var(--theme-transition);
}
.nav-top{display:none} /* Topo link: desktop nav hides it; mobile FAB shows it */
.fab-toggle{display:none} /* FAB button: hidden on desktop, shown on mobile */
.modal-progress{display:none} /* reading-progress bar: only mobile, with a project open — see @media max-width:880px */
.controls{display:flex;align-items:center;gap:var(--space-16);margin-left:auto}
/* Desktop: the navbar spans the full viewport width with a fixed 48px breathing room
   at the edges — unlike the rest of the site, it isn't pinned to the sections'
   --wrap-max. A 3-column grid (instead of the flex+margin-auto used on mobile) centers
   the nav links exactly in the middle of the bar, not relative to the leftover space
   between logo and controls — the two 1fr edge columns absorb any slack, so the center
   block (auto) always sits at the true center regardless of logo/controls width. */
@media (min-width:881px){
  .header-inner{
    width:100%;margin:0;padding-inline:var(--space-48);
    display:grid;grid-template-columns:1fr auto 1fr;gap:var(--space-24);
  }
}
.switch{
  position:relative;
  display:flex;align-items:center;
  background:var(--surface);
  border:1px solid var(--border);
  border-radius:var(--radius-full);padding:var(--space-4);gap:var(--gap);
  cursor:pointer;font-family:inherit;
  /* --seg-h sets the whole control's height (segment + thumb share the same value);
     36px + 4px padding top/bottom = 44px clickable area, within the recommended tap-target minimum */
  --seg-h:var(--switch-seg);--gap:2px;--active-index:0;
  transition:border-color var(--theme-transition),transform var(--duration-slow) ease,background-color var(--theme-transition);
}
.switch:hover{border-color:var(--text)}
/* pill that slides between options */
.switch .thumb{
  position:absolute;top:4px;left:4px;z-index:0;
  width:var(--seg-w,var(--switch-seg));height:var(--seg-h);border-radius:var(--radius-full);
  background:var(--text);pointer-events:none;
  transform:translateX(calc((var(--seg-w) + var(--gap)) * var(--active-index)));
  transition:transform var(--duration-slow) var(--ease-smooth),background-color var(--theme-transition);
}
/* Font/transition come from the shared selector up top (see ".eyebrow, ... .nav-links a,
   .seg" near the top of the file) — here only what diverges from the group: color
   (--text instead of the default --muted, since the text sits on top of .switch's
   --surface) and the segment layout. */
.seg{
  position:relative;z-index:1;
  pointer-events:none;            /* click always lands on the whole switch */
  width:var(--switch-seg);height:var(--seg-h);border-radius:var(--radius-full);
  display:grid;place-items:center;color:var(--text);
}
.seg.lang{width:var(--switch-seg)}
.seg.active{color:var(--surface)}
/* --icon-md-lg (18px), not the default --icon-base (24px) — inside .seg's 36px circle,
   the default icon size was too big, touching the edges. */
.switch .icon-sprite{width:var(--icon-md-lg);height:var(--icon-md-lg)}
#themeSwitch{--seg-w:var(--switch-seg);--active-index:0}
#langSwitch{--seg-w:var(--switch-seg);--active-index:1}

/* ---------- Hero ---------- */
/* Hero always fills the full viewport below the fixed header (min-height, not height:
   if content ever needs more space it grows, never cutting off or overlapping the
   header). Photo, marquee, and CTAs are absolutely positioned inside it — nothing here
   depends on normal flow/flex. */
.hero{
  position:relative;
  min-height:calc(100dvh - var(--header-h));
  text-align:center;
}

/* ---- Photo: anchored to the hero's bottom, with fixed breathing room to the header
   above ---- height:auto + max-height computed from the viewport (not % — height %
   doesn't resolve reliably against a parent whose size comes only from min-height)
   locks the rendered height to exactly the space available below the fixed top gap. */
.hero-photo{
  --hero-photo-top-gap:var(--space-64);
  position:absolute;
  bottom:0;left:50%;
  transform:translateX(-50%);
  z-index:1;
  height:auto;width:auto;
  max-height:calc(100dvh - var(--header-h) - var(--hero-photo-top-gap));
  max-width:92%;
  object-fit:contain;object-position:bottom;
  /* pointer-events:none so the photo never blocks hit-testing on its own — .hero-marquee
     underneath is the real interactive layer, and it draws the line between "the photo"
     and "the letreiro" itself, pixel by pixel via a canvas alpha sample (see
     isOverOpaqueHeroPhoto in script.js), instead of the photo's whole rectangular
     bounding box blocking everything under it regardless of transparency. -webkit-user-
     drag:none stops Safari from treating the img as a native drag source (own drag
     cursor/ghost) even though it's not a pointer target; draggable="false" on the element
     itself (see index.html) is the belt-and-suspenders HTML-level version. */
  pointer-events:none;user-select:none;-webkit-user-drag:none;user-drag:none;
}

/* ---- Marquee: decorative title vertically centered, behind the photo ----
   No z-index of its own (auto) — paints behind the photo's z-index:1 automatically,
   within .hero's same stacking context (position:relative). Also the drag-to-scroll
   surface itself (see initHeroMarquee in script.js): pointer-events/cursor only turn on
   for hover-capable, fine-pointer devices (i.e. desktop with a mouse) — touch never gets
   this interaction. .over-photo is toggled by JS on mousemove when the pointer sits over
   an opaque pixel of the photo (sampled from a canvas, not just its bounding box) — the
   grab cursor and the drag itself both stand down there, but the photo's own transparent
   areas still read as "the letreiro". */
.hero-marquee{
  position:absolute;top:50%;left:50%;
  width:100vw;
  transform:translate(-50%,-50%);
  overflow:hidden;
  pointer-events:none;
}
@media (hover:hover) and (pointer:fine){
  .hero-marquee{pointer-events:auto;cursor:grab}
  .hero-marquee.over-photo{cursor:default}
  .hero-marquee.is-dragging{cursor:grabbing}
}
@keyframes hero-marquee-scroll{
  from{transform:translateX(0)}
  to{transform:translateX(-50%)}
}
.hero-marquee-track{
  display:flex;width:max-content;
  /* for a seamless loop, the track has 2 identical content copies (see renderHeroMarquee
     in script.js) and moves only half its own width — same as .skills-track. Drag-to-scroll
     is handled on .hero-marquee, not this element (see initHeroMarquee in script.js), so
     this element carries no interaction styling of its own. */
  animation:hero-marquee-scroll 90s linear infinite;
}
@media(prefers-reduced-motion:reduce){.hero-marquee-track{animation:none}}
.hero-marquee-copy{display:flex;flex:none;align-items:baseline}
.hero-marquee-word{
  font-family:var(--font-heading);font-weight:700;text-transform:uppercase;
  letter-spacing:-0.02em;line-height:1;white-space:nowrap;
  font-size:clamp(56px,12vw,200px);
  padding-inline:.16em;
  color:var(--text);
  transition:color var(--theme-transition);
}
.hero-gap{display:none}
.text-outline{color:transparent;-webkit-text-stroke:1px var(--text);transition:-webkit-text-stroke-color var(--theme-transition)}

/* hero scroll cue — mobile only */
.hero-scroll-cue{
  cursor:pointer;
  display:none; /* shown only on mobile via @media below */
}

/* ---- CTAs: anchored to the hero's bottom, in front of the photo ----
   z-index:2 works even without position set: a flex item applies z-index normally even
   with static position (per the Flexbox spec) — compares directly against the photo's
   z-index:1, within .hero's same stacking context. */
.actions{
  position:absolute;left:0;right:0;bottom:0;
  padding-bottom:var(--hero-cta-bottom);
  display:flex;flex-wrap:wrap;justify-content:center;gap:var(--cta-gap);
  z-index:2;
  /* This box spans the full width of .hero and sits above .hero-marquee (z-index:2 vs
     auto) — without this, the empty flex space beside/around the button (there's plenty,
     since content is centered in a full-width box) would swallow clicks/hover meant for
     the marquee underneath wherever the two overlap, since a plain div is pointer-events:
     auto by default across its whole box, not just where its content visually is.
     Re-enabled only on the real button below. */
  pointer-events:none;
}
.actions > *{pointer-events:auto}
/* Moves left, same direction as the hero marquee (0 -> -50%, see @keyframes
   hero-marquee-scroll) — which is also the direction initDraggableTicker's resume math
   assumes for both tracks. */
@keyframes ticker-scroll{
  from{transform:translateX(0)}
  to{transform:translateX(-50%)}
}
.skills-ticker{
  width:100%;
  overflow:hidden;
  margin-top:var(--space-64);
  display:flex;flex-direction:column;gap:var(--space-8);
  -webkit-mask-image:linear-gradient(to right,transparent 0,#000 var(--marquee-fade),#000 calc(100% - var(--marquee-fade)),transparent 100%);
  mask-image:linear-gradient(to right,transparent 0,#000 var(--marquee-fade),#000 calc(100% - var(--marquee-fade)),transparent 100%);
}
.skills-track{
  display:flex;
  width:max-content;
  /* --ticker-duration is set by syncTickerSpeed in script.js so this carousel moves at the
     same px/s as the hero marquee (the 40s is only the fallback before that runs) */
  animation:ticker-scroll var(--ticker-duration,40s) linear infinite;
}
.skills-track-2{display:flex;animation-delay:calc(var(--ticker-duration,40s) / -2)}
.skills-ticker{cursor:grab}
.skills-ticker:hover .skills-track,
.skills-ticker:hover .skills-track-2{animation-play-state:paused}
.skills-ticker.is-dragging{cursor:grabbing}
@media(prefers-reduced-motion:reduce){.skills-track{animation:none}}
.skills-set{
  display:flex;
  gap:var(--space-8);
  flex:none;
  padding-right:var(--space-8);
}
.skill-pill{
  display:inline-flex;align-items:center;gap:var(--space-8);
  /* asymmetric padding: every chip here always has an icon on the left, so that edge
     uses the spec's "left/right padding with icon" (8dp); the right edge (text only, no
     icon) uses the default "left/right padding" (16dp). */
  height:var(--pill-h);padding:0 var(--space-16) 0 var(--space-8);
  border:1px solid var(--border);
  border-radius:var(--radius-sm);
  color:var(--muted);
  flex:none;
  transition:border-color var(--theme-transition),color var(--theme-transition);
}
.skill-pill svg{flex:none;width:var(--icon-md-lg);height:var(--icon-md-lg)}
/* text-only chip (no icon on the left): both edges get the plain 16dp padding */
.skill-pill--text{padding-inline:var(--space-16)}
.skill-pill span{
  font-size:var(--text-xs);font-weight:400;
  line-height:1.4;white-space:nowrap;
}
.btn{
  display:inline-flex;align-items:center;gap:var(--space-8);
  font-family:inherit;line-height:1;
  height:var(--btn-h);padding:0 var(--space-24);border-radius:var(--radius-md);cursor:pointer;
  text-decoration:none;border:1.6px solid transparent;
  transition:transform var(--duration-fast) ease,opacity var(--duration-base) ease,background-color var(--theme-transition),color var(--theme-transition),border-color var(--theme-transition);
}
/* Same typographic treatment as the site's links (.exp-company, .reference-name,
   .exp-toggle), minus the underline: compact all-caps instead of normal button text —
   the .btn's icon/background already signal the action affordance. */
.btn span{
  font-size:var(--text-2xs);font-weight:600;text-transform:uppercase;
  letter-spacing:var(--tracking-caps);
}
/* Icon slots: an optional icon before the label (leading), after it (trailing), both, or none — the
   gap comes from .btn's own `gap`, and the icon never shrinks (only the label may wrap). A dropdown
   trigger (aria-haspopup) uses a trailing chevron-down, which turns over while the menu is open. */
.btn .icon-sprite{flex:none}
.btn[aria-haspopup] > .icon-sprite:last-child{transition:transform var(--duration-md) ease}
.btn[aria-haspopup][aria-expanded="true"] > .icon-sprite:last-child{transform:rotate(180deg)}
.btn-dark{background:var(--text);color:var(--surface)}
.btn-dark:hover{opacity:.88}
.btn-ghost{background:transparent;color:var(--text);border-color:var(--text)}
.btn-ghost:hover{background:color-mix(in srgb,var(--text) 8%,transparent)}
.btn:disabled{opacity:.35;cursor:not-allowed;pointer-events:none}

/* ---------- History ---------- */
/* Section transition marker: a thin rule + sequential number before each title,
   signaling "a new context started" more clearly than spacing/title alone could — the
   rule uses --border (subtle, structural) instead of --text, so it doesn't visually
   compete with the number/title right below it. */
/* larger margin-bottom than before: History/References no longer have a large h2
   between the marker and the content (just the label itself, at the small eyebrow size)
   — without this extra breathing room, the grid/list would sit right up against the rule. */
.section-marker{display:flex;flex-direction:column;align-items:flex-start;gap:var(--space-8);margin-bottom:var(--section-content-gap)}
.section-rule{display:block;height:1px;width:100%;background:var(--border);transition:background-color var(--theme-transition)}

.section-title,.exp-title,.card-title,.mh-title,.m-h,.resume-title,.resume-h,.resume-section-h{
  font-weight:700;text-transform:uppercase;letter-spacing:-0.01em;
}

.section-title{
  font-family:var(--font-heading);
  font-size:var(--text-xl);text-box:trim-both cap alphabetic;
  margin-bottom:var(--stack-loose);transition:color var(--theme-transition);
}
.section-subtitle{
  font-size:var(--text-md);color:var(--muted);margin-top:var(--space-8);max-width:50ch;
  transition:color var(--theme-transition);
}
/* Main sections' vertical rhythm: 0 gap between them (no margin) — each carries its own
   bottom breathing room only (padding-top:0, padding-bottom:--section-pad-bottom, fixed 100px,
   no breakpoint variation), so two neighboring sections are 100px apart (it was 80 + 80
   with a top padding as well); the boundary belongs to the section itself — needed in case a section ever
   gets its own full-bleed background, since the "gap" between sections stops being
   page-background breathing room. Includes .about-section (previously outside this
   group — relied only on the hero's internal spacing before it, leaving the hero→about
   transition at a literal 0px). .skills-section is no longer part of this group: it
   became content of the <footer> itself (see .site-footer below), not a page section
   with this same 80px spacing. */
.about-section,
.history-section,
.projects-section,
.aipim-section,
.references-section,
.contact-section{padding-top:0;padding-bottom:var(--section-pad-bottom)}
/* here the ticker is no longer right below the hero (the original margin-top:64px,
   meant to sit under the CTAs) — it becomes the same 32px spacing used between the
   footer's other stacked rows (.socials above it, .site-footer-inner below — see
   .site-footer), since the ticker is the second of three. */
.skills-section .skills-ticker{margin-top:var(--space-32)}

/* ---- Experience: timeline line with company logo ---- */
.exp-list{position:relative}
.timeline-connector{
  position:absolute;top:0;width:0;border-left:1px dashed var(--muted-dim);z-index:0;
  transform:translateX(-.5px);transition:border-color var(--theme-transition),height var(--duration-slow) ease;
}
/* 12/8/4 scheme: desktop uses the 12 columns (1-col margin on each side, logo gets 2,
   content gets 8 — see .exp-logo/.exp-content); tablet/mobile drop to 8/4 below. */
.exp-row{
  position:relative;z-index:1;
  display:grid;grid-template-columns:repeat(12,1fr);column-gap:var(--grid-gap);
  padding-bottom:var(--space-64);
}
.exp-logo{
  grid-column:2 / 4;justify-self:center;align-self:start;
  width:var(--exp-logo-size);height:var(--exp-logo-size);border-radius:var(--radius-full);
  display:grid;place-items:center;
  /* no solid fill on purpose — but the icons are now outline (stroke, not fill), so they
     don't have enough opaque area to hide the .timeline-connector line passing behind
     each badge on their own. --bg (the same color as the page) solves this: opaque
     enough to hide the line, but identical to the surrounding background, so it doesn't
     read as a "badge with a background". The dashed outline is decorative, echoing
     .timeline-connector itself (same --muted-dim color, same dashed style) — together
     they form a single motif. */
  background:var(--bg);color:var(--muted-dim);
  border:1px dashed var(--muted-dim);
  transition:background-color var(--theme-transition),color var(--theme-transition),border-color var(--theme-transition);
}
/* .timeline-connector is always #experienceTimeline's first child (see renderHistory in
   script.js), immediately followed by the first .exp-row — so this combinator only
   targets the most recent job's badge, no JS/class flag needed. A solid --text outline
   (instead of dashed --muted) signals "current position", setting it apart from the
   rest (past) — --text already inverts per theme on its own (gray-50 in dark, gray-900
   in light), so the outline comes out lighter in dark and darker in light with no extra rule. */
.timeline-connector + .exp-row .exp-logo{border-style:solid;border-color:var(--text);color:var(--text)}
.exp-logo svg{width:var(--icon-lg);height:var(--icon-lg)}
.exp-content{grid-column:4 / 12}
/* same scale as section-title (--text-xl): a repeated list item shouldn't look visually
   bigger than the title of the section containing it */
/* --text on every item, not just the most recent role (unlike the --muted-dim of "past"
   badges/line, see .exp-logo above) — the role name always stays equally prominent;
   "current vs. past" hierarchy still comes from the badge/outline/line, not the title. */
.exp-title{
  /* --history-title-scale: set by fitHistoryTitles() in script.js only when the longest role
     wouldn't fit on one line; the same factor on every row keeps the titles equal */
  font-family:var(--font-heading);font-size:calc(var(--text-xl) * var(--history-title-scale,1));text-box:trim-both cap alphabetic;
  line-height:1.05;color:var(--text);
  transition:color var(--theme-transition);
}
.exp-meta{display:flex;align-items:center;gap:var(--space-12);margin-top:var(--space-8)}
.exp-desc{
  font-size:var(--text-body);line-height:1.6;color:var(--text-body-color);max-width:68ch;
  margin-top:var(--stack-normal);transition:color var(--theme-transition);
}

/* Accordion: height in px computed via JS (scrollHeight), not "auto" — it's the only
   way to animate max-height with pure CSS; see renderHistory/toggle in script.js. */
.exp-more{overflow:hidden;max-height:0;transition:max-height var(--duration-slow) ease}
.exp-toggle-row{
  display:grid;grid-template-columns:repeat(12,1fr);column-gap:var(--grid-gap);
}
.exp-toggle{
  grid-column:4 / 12;justify-self:start;
  display:inline-flex;align-items:center;gap:var(--space-8);
  background:transparent;border:none;padding:0;cursor:pointer;
  font-family:inherit;
}
.exp-toggle svg{width:var(--icon-md-lg);height:var(--icon-md-lg);color:var(--muted);transition:transform var(--duration-md) ease,color var(--theme-transition)}
.exp-toggle[aria-expanded="true"] svg{transform:rotate(180deg)}
@media (hover:hover){
  .exp-toggle:hover span,
  .exp-toggle:hover svg{color:var(--text)}
}
.exp-toggle:focus-visible span{color:var(--text)}

/* ---------- About ---------- */
.about-body{
  max-width:calc(100% * 7 / 12);
  margin-inline:auto;
  display:flex;flex-direction:column;gap:var(--stack-loose);
}
.about-p{
  font-size:var(--text-body);line-height:1.7;
  color:var(--text-body-color);
  transition:color var(--theme-transition);
}
/* resume buttons (download + view), now below the text (moved from the hero) — same
   centered column as .about-body above them */
.about-actions{display:flex;justify-content:center;gap:var(--cta-gap);margin-top:var(--cta-offset)}

/* ---------- Projects ---------- */
/* auto-fit/minmax instead of repeat(3,1fr): the fixed 248px sidebar shrinks .wrap's
   usable area well before the mobile breakpoint (880px) — between ~880-1180px viewport,
   3 fixed columns felt cramped even on common laptop screens. With minmax, the grid
   decides on its own how many columns fit comfortably (drops to 2, then 1 — which the
   mobile override below already guarantees explicitly). */
/* explicit 12-column grid (same 12/8/4 desktop/tablet/mobile scheme used in
   .exp-row/.references-grid) — each .card always spans 4 columns (see .card), so 3 fit
   per row on desktop (12/4), 2 on tablet (8/4), and 1 on mobile (4/4). */
.projects{
  display:grid;grid-template-columns:repeat(12,1fr);
  gap:var(--grid-gap);
}
/* ---- Resume dropdown (PT/EN) ---- */
.resume-dropdown{position:relative;display:inline-flex}
.resume-menu{
  position:absolute;top:calc(100% + var(--space-8));left:0;min-width:100%;z-index:20;
  background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);
  box-shadow:var(--shadow-resting);padding:var(--space-8);
  opacity:0;visibility:hidden;transform:translateY(-6px);
  transition:opacity var(--duration-base) ease,transform var(--duration-base) ease,visibility var(--duration-base) ease,background-color var(--theme-transition),border-color var(--theme-transition),box-shadow var(--theme-transition);
}
.resume-menu.is-open{opacity:1;visibility:visible;transform:translateY(0)}
.resume-menu a{
  display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-8);
  padding:var(--space-12) var(--space-16);border-radius:var(--radius-md);
  font-size:var(--text-sm);font-weight:400;color:var(--text);text-decoration:none;
  transition:background-color var(--duration-fast) ease,color var(--theme-transition);
}
.resume-menu a:hover{background:var(--bg)}
.resume-meta{
  font-size:var(--text-2xs);font-weight:400;color:var(--muted);white-space:nowrap;
  transition:color var(--theme-transition);
}

/* display:flex column: .projects' grid stretches every card in a row to the tallest
   one's height (align-items:stretch is the grid default), because of titles that wrap
   to 2 lines on some projects and not others. Without the card itself being a flex
   container with .card-body growing (flex:1), that extra height just sat unused below
   .card-body (which stayed at its OWN content's height) — opening a gap between the
   visible white card and the .card's real border/shadow underneath. */
.card{
  grid-column:span 4;
  display:flex;flex-direction:column;
  background:var(--surface);
  border-radius:var(--radius-xl);box-shadow:var(--shadow-resting);
  cursor:pointer;
  transition:transform var(--duration-slow) var(--ease-smooth),box-shadow var(--duration-slow) ease,background-color var(--theme-transition);
}
/* .card-title-link is the card's only focusable element (see cardHtml in script.js) —
   a click anywhere on .card delegates to the link (see renderCards), so .card no longer
   needs its own tabindex/role. :has() puts the visual feedback (elevation + shadow) on
   the outer .card, same as the old role="button" on the card itself, even though the
   real focus lives only on the link. */
.card-title-link{color:inherit;text-decoration:none;outline:none}
.card:has(.card-title-link:focus-visible){
  transform:translateY(-6px);
  box-shadow:var(--shadow-hover);
}
@media (hover:hover){
  .card:hover{
    transform:translateY(-6px);
    box-shadow:var(--shadow-hover);
  }
}
.card:active{
  transform:translateY(-2px) scale(.99);
  box-shadow:var(--shadow-pressed);
}
.card-media{
  flex:none;
  width:100%;aspect-ratio:4/3;
  /* an <img> (lazy-loaded; aspect-ratio + intrinsic width/height reserve its space so the card doesn't jump). No
     background of its own on purpose: a light one under the image peeks through the anti-aliased rounded corners */
  display:block;object-fit:cover;
  /* the width/height attributes are presentational hints: without this, height=480 wins over aspect-ratio */
  height:auto;
  /* Rounded corners on all 4 sides (not just the top): since .card-body has no color
     of its own (the .card behind it paints the background), the image's bottom corner
     "opens up" and reveals the same color as the text panel — no gradient, no
     pseudo-element, no real box overlap. Just native border-radius doing the work. */
  border-radius:var(--radius-xl);
}
.card-body{
  flex:1 1 auto;
  /* 24px on top (the image above already gives the eyebrow room), 36px on the other sides */
  padding:var(--space-24) var(--card-inset) var(--card-inset);
}
.card-head{display:flex;align-items:flex-start;gap:var(--space-16)}
/* optional icon on the right of the header (cardHtml's `icon`): pushed to the edge, in --muted like other card metadata */
.card-icon{flex:none;margin-left:auto;color:var(--muted);transition:color var(--theme-transition)}
.eyebrow{margin-top:var(--space-8);display:flex;align-items:center;gap:var(--space-8)}
.card-title{
  /* --card-title-scale: set by fitCardTitles() in script.js only when the longest title
     wouldn't fit on one line; the same factor on every card keeps the titles equal */
  font-family:var(--font-heading);font-size:calc(var(--text-xl) * var(--card-title-scale,1));text-box:trim-both cap alphabetic;
  transition:color var(--theme-transition);
}
.card-desc{
  margin-top:var(--stack-normal);color:var(--text-body-color);
  font-size:var(--text-body);line-height:1.5;font-weight:400;
  transition:color var(--theme-transition);
}

/* ---------- Expertise ---------- */
/* .references-grid and .reference: same 12/8/4 grid as .projects.
   .reference is a "static panel" card (surface+radius-xl+shadow-resting+flex column) fixed at
   "span 4" across every breakpoint, only the grid's total column count changes (12→8→4), giving
   3/2/1 card(s) per row without rewriting the span on every media query. */
.references-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--grid-gap)}
.reference{
  grid-column:span 4;
  background:var(--surface);border-radius:var(--radius-xl);
  box-shadow:var(--shadow-resting);
  display:flex;flex-direction:column;
  transition:background-color var(--theme-transition),box-shadow var(--theme-transition);
}
.m-tags{display:flex;flex-wrap:wrap;gap:var(--space-8)}

/* ---------- Design system (Aipim) ----------
   No title block of its own: a two-tone paragraph (.aipim-statement, body text: --text-body at 1.6, the lead in
   --text and the rest in --text-body-color, 6 columns wide; the chips use the full width) over the
   .skill-pill--text chips, --stack-loose apart; then, --section-content-gap below, one card per destination
   (Figma, Storybook, GitHub). The cards are the project card itself (cardHtml() in script.js, on the same
   .projects grid), so nothing below styles them. */
.aipim-head{
  display:flex;flex-direction:column;gap:var(--stack-loose);
  margin-bottom:var(--section-content-gap);
}
.aipim-statement{
  width:calc((100% - 11 * var(--grid-gap)) / 12 * 6 + 5 * var(--grid-gap));
  margin:0;font-size:var(--text-body);line-height:1.6;
  color:var(--text);transition:color var(--theme-transition);
}
.aipim-statement span{color:var(--text-body-color);transition:color var(--theme-transition)}
.aipim-tags{display:flex;flex-wrap:wrap;gap:var(--space-8);list-style:none;margin:0;padding:0}
@media (max-width:880px){
  .aipim-statement{width:auto}
}

/* ---------- Case: prototype ---------- */
/* End of the case's sections, before the sign-off: just the button that opens the viewer. The prototype
   is not an image of the carousel, so it has no thumbnail/poster of its own. */
.m-proto-end{margin:var(--space-48) 0 0;display:flex;justify-content:center}

/* Viewer: a layer under the navbar (z-index 48 < header's 50; above the project modal's 40 and its
   reading-progress bar's 45). It has no bar: while it is open the site's own <header> gets
   .is-proto and swaps its contents — project title + "Prototype" eyebrow where the logo is, the
   Desktop/Mobile tabs where the nav is, a close button where the theme/language switches are.
   The header's grid (desktop: 1fr auto 1fr, same alignment as always) does the layout, so nothing
   is duplicated. The originals are hidden without leaving the layout (absolute + visibility) instead
   of display:none: fitLogoWidth() and positionNavIndicator() in script.js measure them on resize,
   and a display:none parent would measure 0. Both tabs resize the same iframe, so switching
   doesn't reload the prototype or lose where you were in it. */
.proto-viewer{
  position:fixed;top:var(--header-h);left:0;right:0;bottom:0;z-index:var(--z-viewer);display:flex;flex-direction:column;
  background:var(--bg);
  opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity var(--duration-md) ease,visibility var(--duration-md) ease,background-color var(--theme-transition);
}
.proto-viewer.is-open{opacity:1;visibility:visible;pointer-events:auto}
.proto-heading,.proto-switch,.proto-close{display:none}
header.is-proto .header-top,
header.is-proto .mobile-menu,
header.is-proto .controls{position:absolute;left:0;top:0;visibility:hidden;pointer-events:none}
header.is-proto .proto-heading{display:block;min-width:0;flex:1}
header.is-proto .proto-switch{display:flex}
header.is-proto .proto-close{display:grid}
/* phone-only prototype (`device:"mobile"`): no switch, and the close button keeps the last grid column instead of sliding into the middle one */
header.is-proto-single .proto-switch{display:none}
header.is-proto-single .proto-close{grid-column:3}
.proto-title{
  margin:0;font-family:var(--font-heading);font-size:var(--text-lg);line-height:1;text-box:trim-both cap alphabetic;text-transform:uppercase;
  color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  transition:color var(--theme-transition);
}
/* the eyebrow's box is trimmed to its caps like the title's (text-box), so the pair's box is exactly the visible
   text and the header's align-items:center puts it dead center — untrimmed, its line-height left ~4px of
   empty space under the text and the pair sat 2px high. display:block because text-box doesn't apply to
   the flex container that .eyebrow is elsewhere. */
.proto-heading .eyebrow{display:block;margin-top:var(--space-8);text-box:trim-both cap alphabetic}
/* Desktop/Mobile is the site's .switch atom, unchanged (thumb, hover, press feedback, 44px height, click
   toggles); only the segments are wider than the 36px icon circles, to fit the words. */
.proto-switch{--seg-w:calc(var(--dim-44) * 2px);--active-index:0}
.proto-switch .seg{width:var(--seg-w)}
.proto-close{
  justify-self:end;margin-left:auto;place-items:center;flex:none;
  width:var(--tap-size);height:var(--tap-size);padding:0;
  border-radius:var(--radius-full);border:1px solid var(--border);background:none;color:var(--text);cursor:pointer;
  transition:background-color var(--duration-base) ease,color var(--duration-base) ease,border-color var(--theme-transition);
}
@media (hover:hover){
  .proto-close:hover{background:var(--text);color:var(--surface)}
}
/* the stage is a size container so the phone below can be sized from its own area (cqh/cqw) */
.proto-stage{flex:1;min-height:0;overflow:hidden;display:flex;align-items:center;justify-content:center;padding:var(--space-8)}
/* Desktop tab: the prototype takes all of the area under the bar, leaving 8px of the viewer's
   background (the stage's padding) between it and each edge, with 8px rounded corners. */
/* The 8px rounding is on the iframe itself, with no overflow:hidden wrapper around it: an
   overflow:hidden + border-radius ancestor is a known cause of broken touch scrolling inside iframes
   in WebKit. */
/* Switching Desktop/Mobile cross-fades: setProtoMode() adds .is-swapping (the frame fades out), changes the
   mode and size while it is invisible, and removes it (fades back in). No size animation: the prototype
   would re-lay itself out at every frame and flash its tablet layout on the way. */
.proto-device{
  flex:none;width:100%;height:100%;box-sizing:border-box;display:flex;flex-direction:column;
  /* same elevation as the cards; the radius is here (not only on the iframe) because a shadow follows its own box */
  border-radius:var(--radius-sm);box-shadow:var(--shadow-resting);
  transition:opacity var(--duration-base) var(--ease-smooth);
}
.proto-stage.is-swapping .proto-device{opacity:0}
.proto-device iframe{display:block;width:100%;flex:1 1 0;min-height:0;border:0;border-radius:var(--radius-sm);background:var(--gray-50)}
/* the generic Android-style status bar (assets/components/phone-status-bar.js) only shows on the phone (see .psb below) */
/* Mobile: a 1:2 phone (same 8px corners; the status bar counts inside the 1:2). Its LOGICAL size is what the
   prototype lays itself out at — between --phone-w-min and --phone-w-max wide, so it always renders the phone
   layout (a wider box would cross the prototype's own 880px breakpoint into the desktop layout, a narrower one
   squeezes it below a real phone). fitProtoPhone() in script.js picks that width from the stage and, when the
   stage is too small (or the screen huge), scales the whole phone visually with --phone-k instead of resizing it:
   it is centered by the stage, so it keeps 16px of breathing room above and below. */
.proto-stage[data-mode="mobile"] .proto-device{
  width:var(--phone-w,var(--phone-w-max));
  height:calc(var(--phone-w,var(--phone-w-max)) * 2);
  transform:scale(var(--phone-k,1));
}
/* the bar follows the phone's rounded top corners; the iframe below it is square on top. In Desktop it is
   collapsed (--psb-h:0), swapped while the frame is faded out. */
.proto-stage[data-mode="mobile"] .psb{--psb-radius:var(--radius-sm) var(--radius-sm) 0 0}
.proto-stage[data-mode="mobile"] .proto-device iframe{border-radius:0 0 var(--radius-sm) var(--radius-sm)}
.psb{overflow:hidden}
.proto-stage[data-mode="desktop"] .psb{--psb-h:0px;opacity:0}

/* Phones (narrow, or touch + short = held sideways): no tabs (the device already is the phone) and
   the prototype fills the screen edge to edge, without the 8px spacing. Tablets keep the tabs.
   Mirrors protoMobileMq in script.js. */
@media (max-width:880px),(pointer:coarse) and (max-height:600px){
  header.is-proto .proto-switch{display:none}
  /* no size containment here (the phone sizing isn't used) and no scroll chaining into the page behind */
  .proto-viewer{overscroll-behavior:contain}
  .proto-stage,
  .proto-stage[data-mode="mobile"]{padding:0;container-type:normal}
  .proto-stage .proto-device,
  .proto-stage[data-mode="mobile"] .proto-device{width:100%;height:100%;transform:none}
  .proto-device{border-radius:0;box-shadow:none}
  .proto-device iframe{border-radius:0}
}
/* phone held sideways: header is still the desktop 3-column grid, so with the switch hidden the
   close button would slide into the middle column — same fix as .is-proto-single */
@media (min-width:881px) and (pointer:coarse) and (max-height:600px){
  header.is-proto .proto-close{grid-column:3}
}

/* ---------- References ---------- */
/* .reference: only this card's padding and gap, on top of the base rule above. */
.reference{padding:var(--card-inset);gap:var(--stack-loose)}
.reference-avatar{
  width:var(--tap-size);height:var(--tap-size);border-radius:var(--radius-full);
  object-fit:cover;flex:none;
}
.reference-quote{
  font-family:var(--font-body);font-weight:400;font-style:italic;
  font-size:var(--text-body);line-height:1.5;
  flex:1;margin:0;transition:color var(--theme-transition);
}
/* Footer: photo on the left, name with the relationship ("My Leader at Wellhub") stacked on the right */
.reference-author{
  display:flex;align-items:center;gap:var(--space-16);
  border-top:1px solid var(--border);padding-top:var(--stack-loose);
  transition:border-color var(--theme-transition);
}
.reference-id{display:flex;flex-direction:column;align-items:flex-start;gap:var(--space-4);min-width:0}
.reference-name{
  text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px;
  white-space:nowrap;
}
.reference-role{white-space:nowrap}
.reference-name:hover{color:var(--text)}

/* small dot used to separate short text pairs (company/year, author/role, etc.) — a
   single reused component instead of a font character (•, ·), which varies in
   alignment/weight between fonts */
.sep-dot{
  width:4px;height:4px;border-radius:var(--radius-full);flex:none;
  background:var(--muted);transition:background-color var(--theme-transition);
}

/* ---------- Contact ---------- */

.contact-form-col{display:flex;flex-direction:column;gap:var(--space-32);max-width:calc(100% * 6 / 12);margin:0 auto;width:100%}
.contact-form-intro{text-align:center}
.contact-form-intro .section-title{margin-bottom:0}

/* 12/8/4 scheme: 12 columns on desktop — .field spans 6 (half, email/phone side by
   side), .field-wide spans everything (1/-1, works the same at any column count) */
.contact-fields{display:grid;grid-template-columns:repeat(12,1fr);row-gap:var(--grid-gap);column-gap:var(--grid-gap)}
/* square corner at the bottom (where the underline sits) and rounded only on top — an
   "inset" field look */
.field{
  position:relative;display:block;grid-column:span 6;
  /* --surface: white in light mode (contrasts with bg #f0f0f0), black in dark mode
     (contrasts with bg #262626) */
  background:var(--surface);border-radius:var(--radius-md) var(--radius-md) 0 0;
  padding:var(--space-24) var(--space-16) var(--space-12);
  box-shadow:inset 0 -1.5px 0 0 var(--text);
  cursor:text;
  transition:background-color var(--theme-transition),box-shadow var(--theme-transition);
}
.field.field-wide{grid-column:1 / -1}

/* Material-style "floating" label: sits centered over the field by default (standing
   in for a placeholder); on focus or once there's content, it rises and shrinks.
   That's why the input/textarea comes BEFORE the <span> in HTML — the sibling
   selector (~) only looks forward. */
.field-label{
  position:absolute;left:16px;top:50%;transform:translateY(-50%);
  font-size:var(--text-base);font-weight:400;color:var(--muted);
  pointer-events:none;
  transition:top var(--duration-base) var(--ease-smooth),font-size var(--duration-base) var(--ease-smooth),
             transform var(--duration-base) var(--ease-smooth),color var(--theme-transition);
}
/* textarea is multi-line: the idle label aligns to the first line instead of centering */
.field-area .field-label{top:var(--space-24);transform:none}

.field input:focus ~ .field-label,
.field input:not(:placeholder-shown) ~ .field-label,
.field textarea:focus ~ .field-label,
.field textarea:not(:placeholder-shown) ~ .field-label{
  top:var(--space-8);transform:translateY(0);font-size:var(--text-2xs);
}

.field input,
.field textarea{
  display:block;width:100%;
  font:inherit;font-size:var(--text-base);font-weight:400;color:var(--text);
  background:none;border:none;outline:none;padding:0;resize:none;
}
.field textarea{min-height:84px;line-height:1.5}
.field input::placeholder,
.field textarea::placeholder{
  color:var(--muted);opacity:0;transition:opacity var(--duration-fast) ease;
}
.field input:focus::placeholder,
.field textarea:focus::placeholder{opacity:.6}
.contact-submit-row{display:flex;justify-content:center}
/* submission feedback (the form isn't connected to an email service yet) */
.form-status{
  display:none;
  font-size:var(--text-sm);color:var(--muted);text-align:right;
  transition:color var(--theme-transition);
}
.form-status.is-visible{display:block}

.socials{display:flex;flex-direction:row;justify-content:center;gap:var(--space-16);max-width:600px;margin:0 auto}
.social-link{
  position:relative;
  display:flex;align-items:center;justify-content:center;
  width:var(--btn-h);height:var(--btn-h);
  border-radius:var(--radius-full);
  border:1px solid var(--border);background:none;color:var(--text);
  text-decoration:none;
  transition:background-color var(--duration-base) ease,color var(--duration-base) ease,border-color var(--theme-transition);
}
.social-link:hover{background:var(--text);color:var(--surface)}
.social-link svg{flex:none}

/* confirmation tooltip when copying the email to clipboard: same visual language as
   resume-menu (surface + border + shadow) instead of the old hard inverted block */
/* Desktop: tooltip opens to the link's right side, vertically centered — the contact
   column is narrow (part of the card's 2-column grid), and the tooltip text is long, so
   max-width+wrap keeps it from spilling out of the column/card when it opens sideways.
   280px allows a balanced 2-line wrap (the original 220px wrapped into 4 short, ugly
   lines). align-items:flex-start (instead of center) because with 2+ lines of text,
   centering the icon against the whole block left it floating with no relation to the
   first line — aligned to the top, it follows the text's start.
   Mobile: repositioned below the link (see @media below), where there's already enough
   width to fit on one line. */
.copy-tooltip{
  position:absolute;bottom:calc(100% + var(--space-8));left:50%;z-index:5;
  display:flex;align-items:flex-start;gap:var(--space-8);
  width:max-content;max-width:240px;
  background:var(--surface);color:var(--text);
  border:1px solid var(--border);box-shadow:var(--shadow-resting);
  padding:var(--space-8) var(--space-16);border-radius:var(--radius-md);
  opacity:0;visibility:hidden;transform:translateX(-50%) translateY(var(--space-4));
  transition:opacity var(--duration-base) ease,transform var(--duration-base) ease,visibility var(--duration-base) ease,
             background-color var(--theme-transition),border-color var(--theme-transition),box-shadow var(--theme-transition);
  pointer-events:none;
}
.copy-tooltip svg{width:var(--icon-md-lg);height:var(--icon-md-lg);flex:none;margin-top:2px}
.copy-tooltip-text{
  font-size:var(--text-2xs);font-weight:400;color:var(--text);
  text-decoration:none;transition:color var(--theme-transition);
}
.copy-tooltip.is-visible{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0)}

/* ---------- Responsive: mobile (≤880px) ---------- */
@media (max-width:880px){
  /* --icon-lg shrinks (the timeline badge is smaller on mobile, see .exp-logo).
     --wrap-gutter stays at 24px here too (same as desktop), so the side padding doesn't
     step down on tablet/mobile. */
  /* body copy drops to 16px on mobile/tablet (--text-body is a role token, so this one line resizes every consumer) */
  :root{--wrap-gutter:var(--space-24);--card-inset:var(--space-24);--icon-lg:calc(var(--dim-34) * 1px);--text-body:var(--text-base)}
  /* On mobile the photo gets much wider than the top gap would otherwise allow —
     max-width:none lets it bleed off both sides (clipped by the viewport, same as the
     marquee) instead of shrinking to fit; only the height (fixed top gap) limits its size. */
  .hero-photo{--hero-photo-top-gap:var(--space-24);max-width:none}
  /* CTA a 36px da borda de baixo do hero; o letreiro ancora no CTA, --space-64 acima dele, em vez
     de centralizado. z-index acima da foto (z-index:1): no mobile o
     letreiro passa por cima de mim, pra ler melhor. */
  .hero{--hero-cta-bottom:calc(var(--dim-36) * 1px);--hero-marquee-gap:var(--space-64)}
  .hero-marquee{top:auto;bottom:calc(var(--hero-cta-bottom) + var(--btn-h) + var(--hero-marquee-gap));transform:translateX(-50%);z-index:2;
    /* clip só na horizontal: com overflow:hidden o margin-bottom negativo da palavra (abaixo) deixa
       a caixa mais baixa que a base das letras redondas, e o overshoot de G/S/O era cortado em ~1px */
    overflow-x:clip;overflow-y:visible}
  .actions{padding-bottom:var(--hero-cta-bottom)}
  /* margin-bottom negativo come o espaço de descendente da linha (~.1em em Barlow Condensed
     caixa-alta, medido): sem ele os 36px acima ficam ~50px visíveis até a base das letras. */
  .hero-marquee-word{font-size:clamp(48px,28vw,120px);padding-inline:.12em;margin-bottom:-.1em}
  .hero-marquee-track{animation-duration:38s}
  /* the "scroll down" arrow would sit on top of the CTAs now that .actions already
     anchors them to the bottom of the full screen — the "SEE PROJECTS" button already
     fills that role, so the cue stays hidden (element/JS remain in the DOM, it just
     doesn't show). */
  .hero-scroll-cue{
    position:absolute;bottom:var(--fab-offset);left:50%;transform:translateX(-50%);
    display:none;flex:none;
    width:var(--btn-h);height:var(--btn-h);border-radius:var(--radius-full);
    border:1px solid var(--border);background:none;color:var(--text);
    align-items:center;justify-content:center;cursor:pointer;
    transition:background-color var(--duration-base) ease,color var(--theme-transition),border-color var(--theme-transition);
  }
  .hero-scroll-cue:hover{background:var(--text);color:var(--surface)}

  .about-body{max-width:100%}
  .about-p,.exp-desc,.card-desc{font-size:var(--text-body)}
  .card-title{font-size:var(--text-2xl)}
  .exp-title{font-size:calc(var(--text-2xl) * var(--history-title-scale,1))}

  /* CTAs (not icon-only buttons) span full width on mobile — bigger tap target */
  .actions,.about-actions,.resume-bottom-cta{flex-direction:column;align-items:stretch;width:100%}
  .btn{width:100%;justify-content:center}
  .resume-dropdown{width:100%}
  .resume-menu{left:0;right:0;min-width:0}

  .header-inner{width:min(var(--wrap-max),100% - 48px)}
  /* header-top: logo only — no hamburger visible */
  .header-top{flex:none;justify-content:flex-start}
  /* on touch the logo doesn't expand "tg." -> "talis galhardi." (desktop-only effect) */
  .logo:hover .logo-grow{grid-template-columns:0fr}
  .logo:hover .lt{opacity:0;transform:translateX(-8px)}

  /* FAB floating panel — replaces the full-screen drawer */
  .mobile-menu{
    display:flex;flex-direction:column;align-items:flex-end;
    position:fixed;top:auto;left:auto;
    right:var(--fab-offset);bottom:calc(var(--fab-offset) + var(--fab-size) + 16px);
    width:auto;min-width:0;
    background:transparent;padding:0;gap:var(--space-16);
    transform:translateY(12px);
    opacity:0;visibility:hidden;
    transition:transform var(--duration-md) var(--ease-smooth),opacity var(--duration-base) ease,visibility var(--duration-md) ease;
    z-index:var(--z-mobile-menu);overflow:visible;
    flex:none;margin-left:0;
  }
  .mobile-menu.is-open{transform:translateY(0);opacity:1;visibility:visible}
  /* nav links stacked as right-aligned pills */
  .mobile-menu .nav-links{
    flex:none;flex-direction:column;align-items:flex-end;
    justify-content:flex-start;gap:var(--space-8);margin-left:0;position:static;
  }
  .mobile-menu .nav-links a{
    display:inline-flex;align-items:center;gap:var(--space-8);
    background:var(--text);color:var(--surface);
    height:36px;padding:0 var(--space-12);border-radius:var(--radius-full);
    border:.5px solid var(--border-on-text);
    font-weight:600;box-shadow:var(--shadow-fab);
    transition:opacity var(--duration-fast) ease,background-color var(--theme-transition),color var(--theme-transition),border-color var(--theme-transition);
  }
  .mobile-menu .nav-links a:hover{opacity:.75}
  .mobile-menu .fab-nav-icon{display:block;flex:none}
  /* Topo link: pill in FAB only */
  .nav-top{display:inline-flex}
  /* indicator not used in FAB layout */
  .nav-indicator{display:none}
  /* Reading-progress bar replaces the FAB while a project page is open — see
     updateModalProgress() in script.js. */
  .modal-progress{
    display:block;position:fixed;top:var(--header-h);left:0;right:0;z-index:var(--z-modal-progress);
    height:3px;background:var(--border);
  }
  .modal-progress-fill{height:100%;width:0;background:var(--text)}
  #projectModal.is-open ~ .fab-toggle{display:none}
  /* FAB toggle button */
  .fab-toggle{
    display:flex;align-items:center;justify-content:center;
    position:fixed;bottom:var(--fab-offset);right:var(--fab-offset);
    width:var(--fab-size);height:var(--fab-size);
    border-radius:var(--radius-full);
    background:var(--text);color:var(--surface);
    border:.5px solid var(--border-on-text);cursor:pointer;z-index:var(--z-fab);
    box-shadow:var(--shadow-fab);
    transform-origin:top right;
    /* Escondido por padrão: só aparece (.is-shown, ver updateFabVisibility em script.js)
       quando o CTA do hero já saiu da área do FAB. visibility atrasa até o fade-out acabar,
       pra o botão invisível também sair da ordem de foco/toque. */
    opacity:0;visibility:hidden;pointer-events:none;
    transition:background-color var(--theme-transition),color var(--theme-transition),border-color var(--theme-transition),transform var(--duration-slow) var(--ease-smooth),box-shadow var(--theme-transition),opacity var(--duration-slow) ease,visibility 0s var(--duration-slow);
  }
  .fab-toggle.is-shown{opacity:1;visibility:visible;pointer-events:auto;transition-delay:0s,0s,0s,0s,0s,0s,0s}
  .fab-toggle[aria-expanded="true"]{transform:scale(.7)}
  .fab-toggle[aria-expanded="true"]:active{transform:scale(.65)}
  .fab-toggle .fab-icon{transition:transform var(--duration-slow) var(--ease-smooth)}
  .fab-toggle[aria-expanded="true"] .fab-icon{transform:scale(1.43)}
  .fab-toggle .bar{
    transform-box:fill-box;transform-origin:center;
    transition:transform var(--duration-slow) var(--ease-smooth);
  }
  .fab-toggle[aria-expanded="true"] .bar-1{transform:translateY(4px) rotate(45deg)}
  .fab-toggle[aria-expanded="true"] .bar-2{transform:translateY(-4px) rotate(-45deg)}

  .timeline-connector{display:none}
  /* 12/8/4 scheme: 4 columns on mobile — logo and content each span all 4 (1/5),
     stacking on separate rows instead of side by side like desktop/tablet */
  .exp-row{grid-template-columns:repeat(4,1fr);row-gap:var(--stack-normal);padding-bottom:var(--space-48)}
  .exp-logo{grid-column:1 / 5;justify-self:start;--exp-logo-size:var(--btn-h)}
  .exp-content{grid-column:1 / 5}
  .exp-toggle-row{grid-template-columns:repeat(4,1fr)}
  .exp-toggle{grid-column:1 / 5}
  /* "company · period" doesn't fit on one line on narrow screens — without flex-wrap,
     each item's text squeezed/wrapped on its own inside the row (instead of the row
     wrapping as a whole), crowding into the title above. With wrap, the period drops to
     its own line when needed, just like normal text would. */
  .exp-meta{flex-wrap:wrap;row-gap:var(--space-4)}

  /* 12/8/4 scheme: 4 columns on mobile — .card still spans 4 (4/4 = 1 per row), so this
     is really a single column and the gap (--grid-gap) becomes the vertical
     space between stacked cards. max-width keeps the single card from stretching too
     wide on a still-fairly-wide screen. */
  .projects{grid-template-columns:repeat(4,1fr);max-width:420px;margin-left:auto;margin-right:auto}

  /* 12/8/4 scheme: 4 columns on mobile — .reference is already span 4 in the base rule (1 per row) */
  .references-grid{grid-template-columns:repeat(4,1fr)}
  .reference-quote{font-size:var(--text-md)}

  /* 4 columns on mobile: every card is full width (span 4) and its list a single column */

  .contact-form-col{max-width:100%}
  /* 12/8/4 scheme: 4 columns on mobile, .field spans all 4 (stacks, no side-by-side pair) */
  .contact-fields{grid-template-columns:repeat(4,1fr)}
  .field{grid-column:span 4}

  /* Narrower carousel edge fades on mobile */
  .skills-ticker{
    -webkit-mask-image:linear-gradient(to right,transparent 0,#000 calc(var(--marquee-fade) / 2),#000 calc(100% - var(--marquee-fade) / 2),transparent 100%);
    mask-image:linear-gradient(to right,transparent 0,#000 calc(var(--marquee-fade) / 2),#000 calc(100% - var(--marquee-fade) / 2),transparent 100%);
  }

  /* Tighter hero gaps on mobile */
  .skills-ticker{margin-top:var(--space-48)}
  .skills-section .skills-ticker{margin-top:var(--space-32)}
  .site-footer{margin-top:var(--space-48)}
}

/* ---- Tablet: 621–880px (iPad mini/Air portrait, mid-size tablets)
   The mobile layout (horizontal header, no sidebar) already applies in this range via
   @media max-width:880px above — this block only fixes what feels too spread out on
   screens wider than a phone but still below the desktop breakpoint. Coming later in
   the file, it overrides the mobile block's conflicting declarations. ---- */
@media (min-width:621px) and (max-width:880px){
  /* CTAs go back to side-by-side (tablet has room) */
  .actions,.about-actions,.resume-bottom-cta{flex-direction:row;align-items:center}
  .btn{width:auto}
  .resume-dropdown{width:auto}
  .resume-menu{right:auto}

  /* 12/8/4 scheme: 8 columns on tablet — .card still spans 4 (8/4 = 2 per row; 1
     column on a 768px screen would waste too much space) */
  .projects{grid-template-columns:repeat(8,1fr);max-width:none}

  /* 12/8/4 scheme: 8 columns on tablet — .reference is already span 4 in the base rule (2 per row) */
  .references-grid{grid-template-columns:repeat(8,1fr)}

  /* 8 columns on tablet (621-880px): one full-width card per row (span 8), every list in two columns */

  /* 12/8/4 scheme: 8 columns on tablet — logo drops to 2 (was 2 of 12), content to 6
     (was 8 of 12); no 1-column margin on each side like desktop, which this tighter
     width doesn't call for. */
  .exp-row{grid-template-columns:repeat(8,1fr)}
  .exp-logo{grid-column:1 / 3}
  .exp-content{grid-column:3 / 9}
  .exp-toggle-row{grid-template-columns:repeat(8,1fr)}
  .exp-toggle{grid-column:3 / 9}

  /* 12/8/4 scheme: 8 columns on tablet, .field spans 4 (half — email/phone side by side again) */
  .contact-fields{grid-template-columns:repeat(8,1fr)}
  .field{grid-column:span 4}
}

:focus-visible{outline:2.5px solid var(--text);outline-offset:3px}

/* Normalized pressed state for every BUTTON on the site (not links, not .card — .card
   keeps its own elevation/shadow feedback, see .card:active, which is about "settling"
   a raised surface, a different problem from a flat button). Each button used to have a
   different scale value (.btn had none, just a translateY(1px); .switch/.modal-back-link
   used .96; .fab-toggle/.lightbox-btn used .94) or no feedback at all — .exp-toggle had
   nothing. One rule, reading from --press-scale (defined in :root), keeps this tactile
   "click" consistent for any new button that just needs to join this selector list.
   .fab-toggle[aria-expanded="true"]:active keeps its own scale (.65, see there) as a
   combined/deliberately more dramatic state, not the normal pressed one — its higher
   specificity already wins over this rule without needing :not() here. */
.btn:active,
.switch:active,
.modal-back-link:active,
.fab-toggle:active,
.lightbox-btn:active,
.exp-toggle:active{transform:scale(var(--press-scale))}

/* ---------- Site footer ---------- */
/* Three stacked blocks: social icons (.socials), skills ticker (.skills-section), and
   the legal text line (.site-footer-inner), in that order — the first two used to live
   outside the footer (socials inside .contact-section, ticker in its own section); see
   the comments on each. padding-top:24px is the gap between the top border and the
   social icons; padding-bottom:24px, between the legal text and the page's end. */
.site-footer{
  border-top:1px solid var(--border);
  padding:var(--space-24) 0;
  margin-top:var(--space-80);
  transition:border-color var(--theme-transition);
}
/* same 32px spacing .socials already uses above it (used to be the gap between the
   contact form and the social icons; now it separates the social icons, which moved
   into the footer along with the ticker, from the legal text line) — symmetric between
   the two stacked rows in here. */
.site-footer-inner{
  display:flex;flex-wrap:wrap;align-items:center;justify-content:center;
  gap:var(--space-8) var(--space-12);
  margin-top:var(--space-32);
  font-size:var(--text-2xs);color:var(--muted);line-height:1.5;text-align:center;
  transition:color var(--theme-transition);
}
.site-footer-sep{color:var(--border)}

/* ---------- Project page (used to be "modal") ----------
   Full-bleed, starting below the navbar (top:var(--header-h)) instead of covering the
   whole screen: the navbar stays visible and usable (see openModal/closeModal in
   script.js, which no longer make siteHeader/fabToggle inert). Title and back button
   are no longer their own fixed header — they became regular content at the top of
   #modalScroll (see renderModal), inside the same .wrap (width/column) the rest of the
   site uses. Underneath, it's still a JS overlay with no real navigation — only the
   look and what's accessible changed. */
.modal-overlay{
  position:fixed;top:var(--header-h);left:0;right:0;bottom:0;z-index:var(--z-modal);
  background:var(--bg);
  opacity:0;visibility:hidden;pointer-events:none;
  overflow:hidden;
  transition:opacity var(--duration-slow) ease,visibility var(--duration-slow) ease,background-color var(--theme-transition);
}
/* pointer-events:none above is what really matters: visibility:hidden hides it
   visually, but without this the closed overlay still sits "in the way" of mouse
   scroll over that screen area — this was exactly the bug where the floating back
   button reappeared on its own after closing a project and scrolling the page behind it. */
.modal-overlay.is-open{opacity:1;visibility:visible;pointer-events:auto}
.modal{
  position:relative;
  display:flex;flex-direction:column;
  width:100%;height:100%;
  background:var(--bg);color:var(--text);
  transform:translateY(18px);
  transition:transform var(--duration-md) var(--ease-smooth),opacity var(--duration-base) var(--ease-smooth),background-color var(--theme-transition),color var(--theme-transition);
}
.modal-overlay.is-open .modal{transform:none}
/* The back button lives in columns 1-2 of .wrap's grid (the left gutter, see
   .modal-scroll .wrap above) — sticky, not floating. See .m-project-nav below for
   previous/next project navigation, which also moved from the top to the end of the
   content, like a traditional page. */
/* .mh-text spans the same 8 center content columns (3 to 10 — see .m-content/.m-side
   in @media (min-width:1101px) below), with symmetric 2-column gutters on each side
   (back button left, meta/nav right) — instead of the px calculation that only lined
   up by coincidence at one specific viewport width. */
.mh-text{grid-column:3 / 11;margin-bottom:var(--case-intro-gap)}
.mh-title{
  font-family:var(--font-heading);font-size:var(--text-2xl);line-height:1.12;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  /* trimmed to its caps: the title's box is its visible height, so --case-intro-gap below it is the real distance */
  text-box:trim-both cap alphabetic;
}
/* same rhythm as the page sections: the content starts --section-scroll-gap under the navbar (what a section jump lands
   on) and carries --section-pad-bottom below, before the footer */
.modal-scroll{flex:1 1 auto;min-height:0;overflow-y:auto;padding:var(--section-scroll-gap) 0 var(--section-pad-bottom);outline:none}
/* Reuses the same .card from the project grid (see renderCards/cardHtml in script.js)
   instead of duplicating the pattern in a text-only button — the card itself takes the
   person to the previous/next project, with the same cover/title/description as the
   home page. */
.m-project-nav{
  display:grid;grid-template-columns:1fr 1fr;gap:var(--grid-gap);
  margin-top:var(--space-64);padding-top:var(--space-32);
  border-top:1px solid var(--border);
  transition:border-color var(--theme-transition);
}
.m-project-nav-item{display:flex;flex-direction:column;gap:var(--space-8);min-width:0}
/* .card/.card-media normally carry --radius-xl (24px), sized for the home project
   grid's large cards. Here this instance always stays 2 per row (never stacks, see
   .m-project-nav above) and can shrink much more than a normal card on narrow screens —
   with --radius-xl the corner starts reading as disproportionate (almost 1/4 of the
   card's height on mobile). One step down (--radius-lg) keeps the rounded corner
   proportional at any width, without needing a responsive value just for this. */
.m-project-nav-item .card,
.m-project-nav-item .card-media{border-radius:var(--radius-lg)}
.m-project-nav-item .eyebrow{margin:0}

/* Project transition: the whole card slides in the navigation direction and comes back
   from the opposite side already showing the new content. overflow:hidden on the
   overlay clips it at the screen's edge. */
.modal-overlay.is-open .modal.modal-anim-out{opacity:0}
.modal-overlay.is-open .modal.modal-anim-out.dir-next{transform:translateX(-72px)}
.modal-overlay.is-open .modal.modal-anim-out.dir-prev{transform:translateX(72px)}
.modal-overlay.is-open .modal.modal-anim-in{opacity:0;transition:none}
.modal-overlay.is-open .modal.modal-anim-in.dir-next{transform:translateX(72px)}
.modal-overlay.is-open .modal.modal-anim-in.dir-prev{transform:translateX(-72px)}

/* ---------- Resume page (#resumeModal) ----------
   Same overlay/.modal/.modal-scroll as a project case (see
   openResumeModal/closeResumeModal in script.js) — only the internal content changes:
   no side TOC, no prev/next between projects, no lightbox, and no sticky
   .modal-back-link like a case has (here "back" is just another button, see
   .resume-bottom-cta below). Title + download button (ghost) at the top, then its own
   2-column layout (wide Experience + stacked About/Skills/Contact/Education), each
   block in an outlined card — same pattern as .m-intro-card, just reused as a generic
   container instead of a single case-summary card. */
.resume-head{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-24);margin-bottom:var(--section-content-gap)}
.resume-head-text{display:flex;flex-direction:column;gap:var(--space-8)}
.resume-eyebrow{margin:0}
.resume-title{font-family:var(--font-heading);font-size:var(--text-xl);line-height:1.12;text-box:trim-both cap alphabetic}
/* 12 columns, same scheme as .modal-scroll .wrap — Experience spans 7, the rest
   (stacked About/Skills/Contact/Education) spans the final 5, not a 1:1 split.
   align-items:stretch (not start): without it .resume-aside is only as tall as the sum
   of its own cards, so its bottom edge almost never lines up with the Experience card
   beside it (taller, holds 6 jobs). Stretched, the same dead space is left at the
   bottom of both columns — what's missing is distributing that slack only to
   .resume-aside (see :last-child right below), since .resume-experience is already the
   taller of the two columns on its own. */
.resume-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--grid-gap);align-items:stretch}
.resume-card{
  display:flex;flex-direction:column;gap:var(--stack-loose);
  background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-xl);padding:var(--card-inset);
  transition:background-color var(--theme-transition),border-color var(--theme-transition);
}
.resume-experience{grid-column:span 7;min-width:0}
.resume-aside{grid-column:span 5;min-width:0;display:flex;flex-direction:column;gap:var(--grid-gap)}
/* The aside's last card (currently Publications & Speaking — generic via :last-child,
   so it still works if the section order changes again) grows to absorb the extra
   height .resume-grid now stretches into it (see align-items:stretch above) — that's
   what makes the two columns' bottom edges line up: without flex:1 here, the whole
   column would be taller, but each card inside it would keep only its own content
   height, leaving an empty gap below the last one instead of the card itself
   stretching to the edge. */
.resume-aside > .resume-card:last-child{flex:1 0 auto}
/* Back + download resume, both centered as a pair at the end of the page — same
   pattern (flex + justify-content:center + --cta-gap) as .about-actions in the home's
   About section, not a "back at one end, download centered across the whole row" layout. */
.resume-bottom-cta{display:flex;justify-content:center;gap:var(--cta-gap);margin-top:var(--cta-offset)}
/* same size (--text-lg, 24px) for every job title AND every education entry — a
   repeated list item shouldn't vary in size depending on which card it's in. */
.resume-h{font-family:var(--font-heading);font-size:var(--text-lg);line-height:1}
/* Card title (Experience/About/Skills/Contact/Education) one step below the title of
   each item inside it (--text-md-lg, 20px, vs. --text-lg, 24px of .resume-h above) —
   clear hierarchy between the card's header and what's listed in it. */
.resume-section-h{font-family:var(--font-heading);font-size:var(--text-md-lg);line-height:1}
.resume-entry{display:flex;flex-direction:column;gap:var(--stack-normal)}
.resume-entry-head{display:flex;flex-direction:column;gap:var(--space-8)}
/* .resume-item-meta, not .resume-meta: the latter already exists and is something else
   — the PDF file-size text next to "Português"/"English" in the download-resume
   dropdown (see .resume-item above). flex, not running text: .sep-dot (see above) only
   gets sized/visible inside a flex container (same pattern as .exp-meta/.eyebrow) — as
   loose text it's an inline <span> with no dimensions at all. flex-wrap: unlike
   .exp-meta (which lives in a wide column in the home's History and only wraps on
   mobile), this column is already narrow at every breakpoint (5/12 of .resume-grid) —
   longer company/institution + period pairs (e.g. "Fatec - Faculdade de Tecnologia ·
   June 2010 to December 2015") would overflow the card's width without wrapping. */
.resume-item-meta{display:flex;flex-wrap:wrap;align-items:center;row-gap:var(--space-4);column-gap:var(--space-8)}
/* Class-less paragraph/list — any bare <p>/<ul> inside a card inherits the same
   body-text treatment as .m-p/.m-list (see above), just scoped here instead of
   repeating the class on every renderResumeModal() call. :where() zeroes out the
   selector's specificity (the combined selector, ".resume-card p", would otherwise beat
   .resume-item-meta alone and win the cascade even coming earlier in the file) —
   .resume-item-meta (the company/period <p>) needs to stay at the small eyebrow size
   defined alongside .eyebrow/.exp-company above, not this body-text size. */
:where(.resume-card p, .resume-card ul){font-size:var(--text-body);line-height:1.6;color:var(--text-body-color)}
.resume-card ul{list-style:disc;padding-left:var(--space-24)}
.resume-card li + li{margin-top:var(--space-4)}
.resume-card li::marker{color:var(--text)}

@media (max-width:880px){
  .resume-head{flex-direction:column;align-items:stretch}
  .resume-grid{grid-template-columns:1fr}
  .resume-experience,.resume-aside{grid-column:1 / -1}
}

/* ---------- Case image lightbox (cover + section images, see .m-img above) ----------
   Background is always dark and buttons always light, regardless of the site's theme —
   unlike the rest of the modal, the goal here is to isolate the image, not blend with
   the page chrome, so it skips the --surface/--text/--border tokens (which invert per
   theme). z-index above everything else on the site (the previous highest use was 60,
   on the mobile fab-toggle).
   .lightbox-stage is the image's own box (not a padded wrapper; its surrounding overlay is what a
   click-outside lands on): shaped after --lightbox-ratio (set inline per image in openLightbox,
   from the thumbnail's own rendered aspect ratio — defaults to 9/5 here, same as every .m-img
   thumbnail, since those are already cropped to that ratio; a differently-shaped thumbnail, like
   the Aipim banner image, gets a stage shaped like itself instead of being letterboxed into 9/5.
   This also keeps the FLIP scale in openLightbox/closeLightbox correct: it assumes the stage and
   the thumbnail share an aspect ratio, which only holds once the stage follows the thumbnail).
   Max-width of --wrap-max — the same 12 columns that bound any content on the site; the
   fullscreen image doesn't escape that. The min()'s third term derives the width that would fit
   exactly within the available height, so it never overflows the viewport on short screens. */
.lightbox{
  position:fixed;inset:0;z-index:var(--z-lightbox);
  display:flex;align-items:center;justify-content:center;
  background:var(--lightbox-scrim);
  opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity var(--duration-md) ease,visibility var(--duration-md) ease;
}
.lightbox.is-open{opacity:1;visibility:visible;pointer-events:auto;transition:opacity var(--duration-md) ease,visibility 0s}
.lightbox-stage{
  position:relative;
  aspect-ratio:var(--lightbox-ratio,1.8);
  width:min(var(--wrap-max),100% - 200px,calc((100dvh - 240px) * var(--lightbox-ratio,1.8)));
}
.lightbox-img{
  width:100%;height:100%;
  object-fit:contain;
  opacity:0;transition:opacity var(--duration-base) ease;
}
.lightbox-img.is-loaded{opacity:1}
.lightbox-btn{
  display:grid;place-items:center;flex:none;
  width:var(--tap-size);height:var(--tap-size);
  border-radius:var(--radius-full);
  border:1px solid var(--lightbox-border);
  background:var(--lightbox-btn-bg);color:var(--lightbox-fg);
  cursor:pointer;
  transition:background-color var(--duration-base) ease,transform var(--duration-fast) ease;
}
@media (hover:hover){
  .lightbox-btn:hover{background:var(--lightbox-btn-bg-hover)}
}
.lightbox-close{position:fixed;top:var(--space-24);right:var(--space-24)}
/* while the image is in flight the stage is transformed, which would drag the fixed/absolute close button along: keep it hidden until the animation ends */
.lightbox.is-flying .lightbox-close{opacity:0;transition:none}
.lightbox-close{transition:background-color var(--duration-base) ease,transform var(--duration-fast) ease,opacity var(--duration-base) ease}
@media (max-width:620px){
  /* mobile: the image takes the full screen width (no side gutter, no rounded corners, object-fit
     contain so nothing is cropped); close moves above the image itself (bottom:100%, aligned to its
     right edge), without overlapping the photo. The overlay above/below stays tappable to close. */
  .lightbox-stage{width:100%}
  .lightbox-close{position:absolute;bottom:100%;margin-bottom:var(--space-16);right:0;top:auto;transform:none}
}

.m-subtitle{font-size:var(--text-md);line-height:1.5;color:var(--muted);margin-bottom:var(--stack-loose);transition:color var(--theme-transition);
  /* top only: the opening line starts at its cap line, --case-intro-gap under the cover image */
  text-box:trim-start cap alphabetic}
/* On desktop this trailing space is invisible (nothing below it in the .m-content
   column — .m-side sits beside it, not under it). On mobile .m-layout stacks into one
   column (see @media max-width:1100px below), so without this the meta block (company/
   year/role/...) butted right up against this line with zero gap. */
#modalReadMeta{margin-bottom:var(--space-24)}
.m-meta{display:flex;flex-direction:column;gap:var(--space-12)}
.m-meta>div{display:flex;flex-direction:column;gap:0px}
.m-meta .v{font-size:var(--text-base);font-weight:600}
/* aspect-ratio (not a fixed height): the modal column goes from ~880px wide on desktop
   to ~354px full-screen on mobile — with a fixed height, that width change alone
   already distorted the box's effective ratio (from ~2.4:1 to nearly square), forcing
   background-size:cover to crop much more off the image's sides on mobile. With
   aspect-ratio, height scales proportionally with width and the framing stays the same
   on any screen. */
/* no background color/placeholder under the image (set inline as background-image): a light one peeks through the
   anti-aliased rounded corners as a thin outline; aspect-ratio already reserves the space while it loads */
.m-img{background:center/cover no-repeat;border-radius:var(--radius-lg);width:100%;aspect-ratio:9/5;cursor:pointer}
.m-img-cover{aspect-ratio:9/5;margin-bottom:var(--case-intro-gap)}
/* image for each case section (see renderCaseBody in script.js) — same ratio across
   every section, including the final closing image (with no heading/text of its own) */
.m-img-wide{aspect-ratio:9/5;margin-top:var(--stack-loose)}
/* When the closing text is split across two sections (see renderCaseBody), the first
   half's image is followed directly by a paragraph with no heading between them —
   without this rule the paragraph would sit flush against the image's edge (a heading
   normally handles this on its own, with its own margin-top). */
.m-img-wide + .m-p{margin-top:var(--stack-loose)}
.m-h{font-family:var(--font-heading);font-size:var(--text-lg);text-box:trim-both cap alphabetic;margin:var(--section-pad-y) 0 var(--stack-loose);transition:color var(--theme-transition)}
.m-p{font-size:var(--text-body);line-height:1.6;color:var(--text-body-color);margin-bottom:var(--stack-loose);transition:color var(--theme-transition)}
/* Sign-off at the end of the case (see m_caseClosing/renderCaseBody) — comes after the
   last image, with no heading, so it needs its own spacing and to visually stand apart
   from a normal content paragraph. */
.m-outro{text-align:center;font-weight:600;color:var(--text);margin:var(--stack-loose) 0 0}
/* list inside the case body (findings, metrics, what was structured) — same typography
   as .m-p, just with a marker; used sparingly, only where the content is already a list. */
.m-list{font-size:var(--text-body);line-height:1.6;color:var(--text-body-color);margin:calc(var(--space-8) * -1) 0 var(--stack-loose);padding-left:var(--space-24);transition:color var(--theme-transition)}
.m-list li{margin-bottom:var(--space-4)}
.m-list li:last-child{margin-bottom:0}
.m-list li::marker{color:var(--text)}
.m-intro-card .m-list:last-child{margin-bottom:0}

/* TLDR + About the company stacked, no fill — only the border sets these two opening
   blocks apart from the running text of the numbered sections below. Each card's last
   .m-p shouldn't leave extra space below the text. */
.m-intro-cards{display:flex;flex-direction:column;gap:var(--grid-gap);margin-bottom:var(--section-pad-y)}
.m-intro-card{border:1px solid var(--border);border-radius:var(--radius-lg);padding:var(--card-inset);transition:border-color var(--theme-transition)}
.m-intro-card .m-h{margin-top:0}
.m-intro-card .m-p:last-child{margin-bottom:0}

/* Case body layout, desktop only (on mobile it becomes a single column and the menu
   disappears — see @media below — since the fullscreen modal has no room left over for
   a fixed nav column). On desktop (@media (min-width:1101px) below), .m-layout becomes
   the same 12-column grid as .modal-scroll .wrap (repeat(12,1fr), column-gap:24px) —
   .m-content spans exactly the 8 center columns (3 to 10) and .m-side/.m-nav the last 2
   (11-12), symmetric to the back button's 2-column gutter on the left (columns 1-2).
   Same scheme reused for the case's opening (subtitle+cover on the left, tags+meta on
   the right, see .m-intro/.m-side). */
.m-layout{display:flex;align-items:flex-start}
.m-content{flex:1 1 auto;min-width:0}
.m-intro{margin-bottom:var(--space-48)}
.m-side,
.m-nav{flex:none;min-width:0}
.m-side{display:flex;flex-direction:column;gap:var(--space-32);padding-left:var(--space-16)}
/* sticky instead of fixed: follows .modal-scroll's scroll (its overflow-y:auto
   ancestor) without needing to measure the modal's on-screen position via JS */
.m-nav{position:sticky;top:calc(var(--sticky-gap) - var(--section-scroll-gap) - var(--space-8));display:flex;flex-direction:column;gap:var(--space-12);padding-top:var(--space-8);padding-left:var(--space-16)}
/* vertical bar that tracks the visible section (same idea as the header's
   .nav-indicator, just vertical and to the left of each link, instead of below) */
.m-nav-indicator{
  width:2px;height:var(--ind-h,0px);
  transform:translateY(var(--ind-y,0px));
  transition:transform var(--duration-slower) var(--ease-smooth),height var(--duration-slower) var(--ease-smooth),opacity var(--duration-slow) ease,background-color var(--theme-transition);
}
.m-nav a{text-decoration:none}
.m-nav a.active{color:var(--text)}
.m-nav a:hover,
.m-nav a:focus-visible{color:var(--text)}

/* ---- Case body skeleton (see loadProjectDetail in script.js): title/subtitle/meta
   already ship ready (part of the summary preloaded for the cards), only the long body
   + images are fetched on demand the first time the modal opens. ---- */
.m-skeleton{display:flex;flex-direction:column;gap:var(--space-12);padding-top:var(--space-8)}
.sk-line{
  display:block;height:14px;border-radius:var(--radius-md);max-width:68ch;
  background:linear-gradient(90deg,var(--border) 25%,var(--surface) 50%,var(--border) 75%);
  background-size:200% 100%;
  animation:sk-shimmer 1.4s ease-in-out infinite;
}
.sk-h{height:20px;width:55%;margin-bottom:var(--space-4)}
.sk-short{width:75%}
.sk-img{
  display:block;height:360px;border-radius:var(--radius-lg);width:100%;margin-top:var(--space-8);
  background:linear-gradient(90deg,var(--border) 25%,var(--surface) 50%,var(--border) 75%);
  background-size:200% 100%;
  animation:sk-shimmer 1.4s ease-in-out infinite;
}
@keyframes sk-shimmer{
  0%{background-position:200% 0}
  100%{background-position:-200% 0}
}
/* Desktop: .m-layout becomes a 12-column grid identical to .modal-scroll .wrap's (same
   total width, same column-gap) — since .m-layout already spans 1/-1 inside the outer
   grid, the column lines line up, so .m-content (3/11, the 8 center ones) and
   .m-side/.m-nav (11/13, the last 2) end up truly aligned with the rest of the page,
   symmetric to the back button's 2-column gutter on the left — instead of the old
   fixed px width. */
@media (min-width:1101px){
  .m-layout{
    display:grid;grid-template-columns:repeat(12,1fr);column-gap:var(--space-24);align-items:start;
  }
  .m-content{grid-column:3 / 11}
  .m-side,
  .m-nav{grid-column:11 / 13}
}
@media (max-width:1100px){
  /* no gutter column — a single column, each piece on its own row: title, then button,
     then content (.m-intro). On desktop, .modal-back-link and .m-intro deliberately
     share the same grid row (the button only occupies the empty gutter .m-content
     leaves free); here, since both become full width in the same column, they need
     separate rows — otherwise one draws on top of the other. */
  #modalScroll > .wrap{grid-template-columns:1fr}
  .modal-back-link,
  .mh-text,
  .m-project-nav{grid-column:1}
  /* Without the 2-column gutter at this breakpoint, the back button would lose its own
     empty space to sit in and would float (sticky) on top of the full-width content
     below — an outline pill with a transparent background in that situation leaves the
     image/text behind it visible through it, unreadable. It becomes an icon-only
     instance of the dark-outline look (--dark-outline-* tokens) instead of
     desktop's labeled outline pill: fixed dark background + subtle light outline, not
     --text/--surface, which in dark theme would give a bright white fill — competing
     for attention with the FAB itself (which already uses a full --text/--surface
     fill). Combined with --shadow-fab (same elevation as the FAB), the button stays
     subdued over the content behind it without "shouting" as loud. The label stays in
     the DOM (aria-label already covers the accessible name), just visually hidden. It
     also moves up to grid-row:1 (ahead of the title, which drops to grid-row:2) and the
     sticky gains --space-24 of distance from the navbar instead of sitting flush (0px).
     The sticky top value is NOT a plain 24px: the sticky containing block uses
     .modal-scroll's own padding-box, which already INCLUDES its --section-scroll-gap
     padding-top — since this button is now the grid's first child (grid-row:1), with
     nothing above it to "consume" that padding first, top:var(--space-24) alone would pin it
     at padding + 24px below the navbar, never scrolling with the content.
     calc(var(--space-24) - var(--section-scroll-gap)) cancels that padding out, leaving exactly
     24px of free "run" before the point where it sticks 24px from the navbar. */
  .modal-back-link{
    grid-row:1;
    margin-bottom:var(--stack-loose);
    top:calc(var(--space-24) - var(--section-scroll-gap));
    width:var(--tap-size);height:var(--tap-size);
    padding:0;justify-content:center;
    border-radius:var(--radius-full);
    border-color:var(--dark-outline-border);
    background:var(--dark-outline-bg);color:var(--gray-50);
    box-shadow:var(--shadow-fab);
  }
  .modal-back-link:hover,
  .modal-back-link:active{background:var(--dark-outline-bg-hover);color:var(--gray-50)}
  .mh-text{grid-row:2}
  .modal-back-link span{
    position:absolute;width:1px;height:1px;padding:0;margin:-1px;
    overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;
  }
  .m-intro{grid-row:3}
  /* side nav menu is a desktop feature — the fullscreen mobile modal has no column
     left for it, so it disappears and the content takes the full width */
  .m-layout{flex-direction:column}
  .m-content{width:100%}
  .m-side{width:100%}
  .m-nav{display:none}
  .m-side{padding-left:0}
  /* metadata side by side with wrapping (company/year/role/team/duration) instead of a
     single column — there's plenty of width to spare on fullscreen mobile for this, and
     it saves a good amount of height before the tags. each item has a comfortable
     minimum width so the value doesn't break mid-word; "Team" alone is usually the
     longest text and ends up taking the whole row (flex-wrap handles that on its own,
     no special rule needed). */
  .m-meta{flex-direction:row;flex-wrap:wrap;column-gap:var(--space-24);row-gap:var(--space-16)}
  .m-meta>div{flex:1 1 140px}
}
