/* =============================================================================
   site.css — the portfolio page

   Layers: layout, components, pages. Slot in after tokens.css and base.css,
   both of which declare the cascade order.

   Provenance note, stated honestly: this is NOT a port of an official design
   system. It is a bespoke editorial aesthetic built on the token layer in
   tokens/tokens.css. That layer carries the palette, type scale, and motion
   scale; this file only arranges it. Every value below resolves from a token.
   ============================================================================= */

/* -----------------------------------------------------------------------------
   LAYOUT
   -------------------------------------------------------------------------- */
@layer layout {
  /* Sticky footer. body carries min-block-size: 100dvh from base.css, so the
     main column absorbs the slack and the footer sits on the bottom edge even
     when the content is shorter than the viewport. Without this the footer
     floats mid-page on a tall screen, which reads as a broken page. */
  body {
    display: flex;
    flex-direction: column;
  }

  main {
    flex: 1 0 auto;
  }

  .container {
    width: min(100% - (var(--page-pad-inline) * 2), var(--page-max));
    margin-inline: auto;
  }

  .container--wide {
    width: min(100% - (var(--page-pad-inline) * 2), var(--page-max-wide));
    margin-inline: auto;
  }

  .section {
    margin-block-start: var(--space-3xl);
    padding-block-start: var(--space-2xl);
  }

  .section--ruled {
    border-top: 1px solid var(--rule);
  }

  /* The footer's rule spans the viewport so the page bottom reads as a base,
     not as a floating box. */
  .site-footer {
    margin-block-start: var(--space-4xl);
    border-top: 1px solid var(--rule);
    background: var(--surface-chrome);
  }

  /* Two-column split. Stacks to strict single column below 52rem, per the
     skill's mobile override rule for DESIGN_VARIANCE >= 4. */
  .split {
    display: grid;
    gap: var(--space-xl);
    grid-template-columns: 1fr;
  }

  @media (min-width: 52rem) {
    .split {
      gap: var(--col-gap);
      align-items: start;
    }

    .split--prose-facts {
      grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
    }
  }

  /* Paragraphs need their own rhythm. Without a gap the About prose reads as
     one undifferentiated block, which is the failure mode of a margin reset. */
  @media (min-width: 52rem) {
    .split--prose-facts > :first-child {
      display: grid;
      gap: var(--space-md);
      align-content: start;
    }

  /* Entry internals: a narrow ordinal column and the content beside it. The
     hanging number gives a three-row list a spine, which a single flat column
     of text does not have. */
  @media (min-width: 52rem) {
    .entry {
      grid-template-columns: 3.25rem minmax(0, 1fr);
      gap: var(--space-xl);
    }
  }
}

/* -----------------------------------------------------------------------------
   COMPONENTS
   -------------------------------------------------------------------------- */
@layer components {
  /* --- skip link ---------------------------------------------------------- */
  .skip-link {
    position: absolute;
    inset-inline-start: var(--space-md);
    inset-block-start: -4rem;
    z-index: 40;
    padding: var(--space-xs) var(--space-sm);
    border: 1px solid var(--rule-control);
    border-radius: var(--radius);
    background: var(--surface-page);
    color: var(--ink-strong);
    font-family: var(--accent-mono);
    font-size: var(--fs-label);
    letter-spacing: var(--ls-label);
    text-transform: uppercase;
    text-decoration: none;
    transition: inset-block-start var(--dur-micro) ease;
  }

  .skip-link:focus {
    inset-block-start: var(--space-md);
    text-decoration: none;
  }

  /* --- header -------------------------------------------------------------
     One line, 3.5rem tall. Nav links are chrome: mono, uppercase, tracked.
     ---------------------------------------------------------------------- */
  .site-header {
    position: sticky;
    inset-block-start: 0;
    z-index: 20;
    background: var(--chrome-surface);
    border-bottom: 1px solid var(--chrome-border);
    backdrop-filter: blur(8px);
  }

  .site-header__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    block-size: var(--chrome-height);
  }

  .wordmark {
    display: inline-flex;
    align-items: baseline;
    color: var(--ink-display);
    font-family: var(--accent-mono);
    font-size: var(--fs-nav);
    letter-spacing: var(--ls-nav);
    text-transform: uppercase;
    text-decoration: none;
    white-space: nowrap;
  }

  .wordmark:hover {
    text-decoration: none;
  }

  /* The one recurring accent mark in the chrome. It is a text glyph, not an
     image and not a decorative dot: it carries the same weight as a period. */
  .wordmark__mark {
    color: var(--accent);
  }

  .site-nav {
    display: flex;
    align-items: center;
    gap: var(--space-lg);
  }

  /* Controls, as distinct from navigation. Separating them lets the mobile
     header put the controls beside the wordmark and drop the nav to a second
     row, which is impossible while all three share one flex parent. */
  .site-utility {
    display: flex;
    align-items: center;
    gap: var(--space-md);
  }

  /* Below 34rem the header becomes two rows: wordmark and controls, then the
     navigation underneath.

     The previous version simply hid the nav links here. That kept the header
     one line and made the page unnavigable on a phone, which is a much worse
     trade than 40 extra pixels of sticky header. */
  @media (max-width: 33.999rem) {
    .site-header__inner {
      display: grid;
      grid-template-columns: 1fr auto;
      grid-template-areas:
        "mark utility"
        "nav nav";
      block-size: auto;
      padding-block: var(--space-sm);
      gap: var(--space-sm) var(--space-md);
    }

    .wordmark {
      grid-area: mark;
    }

    .site-utility {
      grid-area: utility;
      gap: var(--space-sm);
    }

    .site-nav {
      grid-area: nav;
      gap: var(--space-lg);
    }
  }

  .site-nav__link {
    font-family: var(--accent-mono);
    font-size: var(--fs-nav);
    letter-spacing: var(--ls-nav);
    text-transform: uppercase;
    text-decoration: none;
    color: var(--chrome-link);
    transition: color var(--dur-micro) ease;
  }

  .site-nav__link:hover,
  .site-nav__link:focus-visible {
    color: var(--chrome-link-hover);
    text-decoration: none;
  }


  .theme-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 2rem;
    block-size: 2rem;
    border: 1px solid transparent;
    border-radius: var(--radius);
    color: var(--chrome-link);
    cursor: pointer;
    transition:
      color var(--dur-micro) ease,
      border-color var(--dur-micro) ease;
  }

  .theme-toggle:hover,
  .theme-toggle:focus-visible {
    color: var(--ink-display);
    border-color: var(--rule-control);
  }

  /* --- buttons ------------------------------------------------------------
     Two intents only. The bracket form is the system's signature control: the
     brackets are literal label text and the affordance, not decoration around
     a border.
     ---------------------------------------------------------------------- */
  .btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    min-block-size: var(--hit-min);
    padding-inline: var(--space-md);
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    background: var(--accent);
    color: var(--selection-ink);
    font-family: var(--accent-mono);
    font-size: var(--fs-button);
    font-weight: 500;
    letter-spacing: var(--ls-button);
    text-transform: uppercase;
    text-decoration: none;
    white-space: nowrap;
    transition:
      background var(--dur-micro) ease,
      border-color var(--dur-micro) ease;
  }

  .btn:hover,
  .btn:focus-visible {
    background: var(--accent-strong);
    border-color: var(--accent-strong);
    color: var(--selection-ink);
    text-decoration: none;
  }

  .link-bracket {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--hit-min);
    font-family: var(--accent-mono);
    font-size: var(--fs-button);
    font-weight: 500;
    letter-spacing: var(--ls-button);
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--ink-secondary);
    text-decoration: none;
    transition: color var(--dur-micro) ease;
  }

  .link-bracket:hover,
  .link-bracket:focus-visible {
    color: var(--ink-display);
    text-decoration: none;
  }

  /* --- hero ---------------------------------------------------------------
     Left-weighted, not centred. DESIGN_VARIANCE 6 means the composition is
     offset from symmetry rather than perfectly mirrored.
     ---------------------------------------------------------------------- */
  .hero {
    padding-block: var(--space-2xl) var(--space-xl);
  }

  .hero__eyebrow {
    display: block;
    margin-block-end: var(--space-md);
    color: var(--ink-muted);
  }

  .hero__title {
    margin-block-end: var(--space-lg);
    font-size: var(--fs-display);
    line-height: var(--lh-display);
    letter-spacing: var(--ls-display);
    text-wrap: pretty;
  }

  /* --- typed headline ------------------------------------------------------
     The H1 IS the animated phrase, with no static prefix. Two consequences:

     1. It is sized with --fs-h1 rather than --fs-display, because the longest
        term ("especialista en inteligencia artificial", 39 characters) has to
        fit on one line at desktop. At --fs-display it wrapped.
     2. The height must be reserved for the WORST case, not the average. As the
        phrase types and erases it repeatedly crosses the wrap threshold, so a
        one-line reserve would oscillate the whole hero on every cycle.

        The threshold sits near 640px: above it the longest term fits on one
        line, below it takes two. The reserve follows the same breakpoint. */
  .hero__title--typed {
    max-width: none;
    min-block-size: 1.15em;
    font-size: var(--fs-h1);
    line-height: var(--lh-h1);
    letter-spacing: var(--ls-h1);
  }

  /* 44rem, not 40rem. At exactly 640px the longest Spanish term measured 566px
     of text against 576px of container, which is too close to trust the wrap
     threshold. The reserve moves up to leave real margin. */
  @media (max-width: 43.99rem) {
    .hero__title--typed {
      min-block-size: 2.3em;
    }
  }

  /* Italic descender clearance: the display face is italic, so any word with
     y g j p q needs vertical room or the tail clips against the next line. */
  .hero__title em {
    font-style: italic;
  }

  /* --- typed headline ------------------------------------------------------
     Two stacked lines rather than a <br>, because the skill bans broken
     headlines as a default design move and a block-level span is the honest
     way to express a real structural break.

     The role line reserves exactly one line of height. Without that reserve the
     H1 reflows on every character, which is both a CLS source and nauseating. */
  .hero__line {
    display: block;
  }

  /* The one accent in the hero. It marks the insertion point, so it carries
     meaning rather than decorating. */
  .caret {
    display: inline-block;
    inline-size: 0.055em;
    block-size: 0.78em;
    margin-inline-start: 0.07em;
    background: var(--accent);
    vertical-align: baseline;
    translate: 0 0.02em;
    animation: caret-blink 1.06s steps(1, end) infinite;
  }

  @keyframes caret-blink {
    0%,
    49.9% {
      opacity: 1;
    }

    50%,
    100% {
      opacity: 0;
    }
  }

  /* Once the phrase has settled the caret goes away. A blinking cursor on
     finished text reads as "still typing" when nothing is typing. */
  .is-typed .caret {
    display: none;
  }

  /* Reduced motion never starts the loop, so the caret never appears at all. */
  @media (prefers-reduced-motion: reduce) {
    .caret {
      display: none;
    }

  }

  .hero__sub {
    max-width: 46ch;
    margin-block-end: var(--space-xl);
    color: var(--ink-body);
  }

  .hero__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-lg);
  }

  /* --- section headings ---------------------------------------------------
     Section labels are h2, not uppercase eyebrows. The skill caps eyebrow
     count at ceil(sections / 3); using zero is better here because the
     editorial italic already carries the hierarchy, and a tracked micro-label
     above each section would read as a template.
     ---------------------------------------------------------------------- */
  .section__title {
    margin-block-end: var(--space-sm);
  }

  .section__intro {
    max-width: 58ch;
    margin-block-end: var(--space-xl);
    color: var(--ink-secondary);
  }

  /* --- work entries -------------------------------------------------------
     Rows separated by a top rule per entry. No bottom borders: the skill bans
     border-t + border-b on every row.
     ---------------------------------------------------------------------- */
  .entry {
    display: grid;
    gap: var(--space-lg);
    padding-block: var(--space-xl);
    border-top: 1px solid var(--rule);
  }

  .entry:first-of-type {
    border-top: 0;
    padding-block-start: 0;
  }

  .entry__index {
    display: block;
    color: var(--ink-faint);
  }

  @media (max-width: 51.999rem) {
    .entry__index {
      margin-block-end: var(--space-2xs);
    }
  }

  .entry__title {
    /* Constrained so a long title wraps to two lines instead of running the
       full container width while the prose beside it stays at 54ch. An
       unconstrained title is what makes a row look unfinished. */
    max-width: 30ch;
    margin-block-end: var(--space-sm);
    font-family: var(--mono);
    font-size: clamp(1.4rem, 2.4vw, 1.75rem);
    font-weight: 500;
    font-style: normal;
    letter-spacing: -0.01em;
    color: var(--ink-display);
  }

  .entry__desc {
    max-width: 54ch;
    margin-block-end: var(--space-md);
    font-size: var(--fs-copy);
    line-height: var(--lh-copy);
    color: var(--ink-body);
  }

  /* Employer and period. Mono and muted, because it is a fact rather than a
     claim, and it should not compete with the title.

     This was previously an outcome line holding a fabricated metric. Metrics
     invented on someone's behalf are the worst thing a portfolio can carry, so
     the slot now holds only what the source documents state.
     ---------------------------------------------------------------------- */
  .entry__meta {
    display: block;
    margin-block-end: var(--space-md);
    color: var(--ink-muted);
  }

  .entry__stack {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2xs) var(--space-sm);
    margin-block-end: var(--space-md);
    color: var(--ink-muted);
  }

  .entry__stack li {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
  }

  /* Hairline separators between stack items, rather than a middle-dot run.
     The skill rations the middle-dot to one per line in metadata strips. */
  .entry__stack li + li::before {
    content: "";
    inline-size: 1px;
    block-size: 0.75em;
    background: var(--rule-strong);
  }

  .entry__link {
    font-family: var(--accent-mono);
    font-size: var(--fs-button);
    font-weight: 500;
    letter-spacing: var(--ls-button);
    text-transform: uppercase;
    color: var(--ink-strong);
    text-decoration-line: underline;
    text-decoration-thickness: 1px;
    text-decoration-color: var(--accent-underline);
    text-underline-offset: 0.22em;
    transition:
      color var(--dur-micro) ease,
      text-decoration-color var(--dur-micro) ease;
  }

  .entry__link:hover,
  .entry__link:focus-visible {
    color: var(--accent-strong);
    text-decoration-color: var(--accent);
  }

  /* --- artifact ------------------------------------------------------------
     The evidence slot for a project. It holds REAL output, not a picture of
     output.

     This was a photograph slot first, filled from a placeholder image service.
     That was wrong twice over: generic stock next to a Postgres linter is a
     lie by juxtaposition, and building a fake terminal window to frame it is
     the single most common tell in machine-generated pages. A plain <pre> is
     neither. It is a code block, it is the actual text, and it is the thing a
     reader came to see.

     To use a real screenshot instead, swap the <pre> for:
       <img src="..." width="1280" height="800" alt="" loading="lazy">
     and give it `aspect-ratio` here so the box cannot shift.
     ---------------------------------------------------------------------- */
  .artifact {
    padding: var(--space-md);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    background: var(--surface-card);
    color: var(--ink-body);
    font-family: var(--mono);
    font-size: 0.75rem;
    line-height: 1.75;
    overflow-x: auto;
    tab-size: 2;
    /* Long lines scroll inside the block rather than widening the layout. */
    overscroll-behavior-x: contain;
  }

  .artifact code {
    font-size: inherit;
    white-space: pre;
  }

  /* The one accent inside an artifact marks the line that matters. Used on a
     single line per block, never as a syntax theme. */
  .artifact .is-flagged {
    color: var(--accent-strong);
  }

  .artifact .is-dim {
    color: var(--ink-faint);
  }

  /* --- credentials ---------------------------------------------------------
     A degree gets more visual weight than a job did: the title takes the serif
     italic display treatment at a size between h3 and h2, where a role title is
     upright and smaller. That is the promotion, achieved without adding colour
     or a box.
     ---------------------------------------------------------------------- */
  .credentials {
    display: grid;
    gap: 0;
  }

  .credential {
    display: grid;
    gap: var(--space-3xs);
    padding-block: var(--space-lg);
    border-top: 1px solid var(--rule);
  }

  .credential:first-of-type {
    border-top: 0;
    padding-block-start: 0;
  }

  .credential__degree {
    font-family: var(--serif);
    font-style: italic;
    font-weight: var(--display-weight);
    font-size: clamp(1.3rem, 2.4vw, 1.7rem);
    line-height: 1.25;
    letter-spacing: var(--display-tracking);
    color: var(--ink-display);
    max-width: 32ch;
  }

  /* The leading credential takes a larger title. Size is the whole mechanism:
     no accent colour, no badge, no border, because the accent in this system is
     reserved for links, focus and the headline caret. */
  .credential--primary .credential__degree {
    font-size: clamp(1.5rem, 3.2vw, 2.1rem);
    max-width: 26ch;
    line-height: 1.2;
  }

  .credential__school {
    color: var(--ink-strong);
    font-weight: 600;
  }

  .credential__meta {
    color: var(--ink-faint);
  }

  /* --- about -------------------------------------------------------------- */
  .facts {
    display: grid;
    gap: var(--space-lg);
  }

  .facts__group dt {
    margin-block-end: var(--space-2xs);
    color: var(--ink-muted);
  }

  .facts__group dd {
    color: var(--ink-body);
  }

  .facts__group dd + dd {
    margin-block-start: var(--space-2xs);
  }

  .roles {
    display: grid;
    gap: var(--space-md);
  }

  .role {
    display: grid;
    gap: var(--space-3xs);
  }

  .role__title {
    color: var(--ink-strong);
    font-weight: 600;
  }

  .role__meta {
    color: var(--ink-faint);
  }

  .role__scope {
    color: var(--ink-secondary);
    font-size: 0.875rem;
    line-height: 1.6;
  }

  /* --- contact ------------------------------------------------------------ */
  .contact__email {
    display: inline-block;
    margin-block: var(--space-sm) var(--space-lg);
    font-family: var(--serif);
    font-style: italic;
    font-size: clamp(1.5rem, 4vw, 2.25rem);
    line-height: 1.25;
    letter-spacing: -0.015em;
    color: var(--ink-display);
    text-decoration-line: underline;
    text-decoration-thickness: 1px;
    text-decoration-color: var(--accent-underline);
    text-underline-offset: 0.18em;
    /* Descender reserve: italic g and y sit low at this size. */
    padding-block-end: 0.08em;
    overflow-wrap: anywhere;
    transition: text-decoration-color var(--dur-micro) ease;
  }

  .contact__email:hover,
  .contact__email:focus-visible {
    text-decoration-color: var(--accent);
  }

  .contact__links {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-lg);
  }

  /* --- footer ------------------------------------------------------------- */
  .site-footer__inner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm) var(--space-lg);
    padding-block: var(--space-lg);
    color: var(--ink-muted);
  }

  .site-footer a {
    color: var(--ink-muted);
  }

  .site-footer a:hover {
    color: var(--ink-strong);
  }
}

/* -----------------------------------------------------------------------------
   LANGUAGE SWITCH

   Two real links, not a JavaScript text swap. A swap would put both languages
   in the DOM at once, which search engines index as duplicate content and
   screen readers announce twice. Each page also carries the correct <html
   lang>, so hyphenation, spellcheck, and screen-reader pronunciation are right
   without any script running.
   -------------------------------------------------------------------------- */
@layer components {
  .lang-switch {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
  }

  .lang-switch__item {
    font-family: var(--accent-mono);
    font-size: var(--fs-nav);
    letter-spacing: var(--ls-nav);
    text-transform: uppercase;
    text-decoration: none;
    padding-inline: var(--space-3xs);
    color: var(--ink-faint);
    transition: color var(--dur-micro) ease;
  }

  .lang-switch__item:hover,
  .lang-switch__item:focus-visible {
    color: var(--chrome-link-hover);
    text-decoration: none;
  }

  /* The current language reads as selected, not as a link. */
  .lang-switch__item[aria-current="true"] {
    color: var(--ink-display);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-decoration-color: var(--accent-underline);
    text-underline-offset: 0.28em;
  }

  .lang-switch__item[aria-current="true"]:hover {
    text-decoration-color: var(--accent);
  }

  .lang-switch__sep {
    color: var(--rule-control);
    font-family: var(--mono);
    font-size: var(--fs-nav);
  }
}

/* -----------------------------------------------------------------------------
   UTILITIES
   -------------------------------------------------------------------------- */
@layer utilities {
  /* Keeps a hyphenated compound from breaking across lines. "Postgres-heavy"
     split into "Postgres-" / "heavy" is a real typographic defect in display
     type, and CSS breaks at hyphens by default. */
  .nowrap {
    white-space: nowrap;
  }
}

/* -----------------------------------------------------------------------------
   PAGES
   -------------------------------------------------------------------------- */
@layer pages {
  /* Scroll-in reveal. MOTION_INTENSITY 5 means fluid CSS on transform and
     opacity only, with no scroll listener anywhere.

     `animation-timeline: view()` is a progressive enhancement: browsers that
     do not support it simply never apply the animation, and the element stays
     at its natural, fully visible state. That is why the base rule is the
     FINAL state and the animation only moves it INTO place. If the timeline
     is unsupported the content is still completely legible. */
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .reveal {
        animation: reveal-in linear both;
        animation-timeline: view();
        animation-range: entry 5% cover 26%;
      }
    }
  }

  @keyframes reveal-in {
    from {
      opacity: 0;
      transform: translateY(0.75rem);
    }

    to {
      opacity: 1;
      transform: translateY(0);
    }
  }

  /* Deliberately no scroll cue at the bottom of the hero. The skill bans
     them, and it is right: if the reader has not scrolled, they are looking
     at the hero, and they know what scrolling is. */
}
