/* src/runtime/gallery.css */
/* Elastic obra gallery (homepage "Obras recientes").
   ALL of its styling lives here, in the raw userStyles bundle, on purpose:
   half these selectors are runtime state (.is-active moves, container hover
   rules) and the import prunes CSS for class states absent from markup. One
   authoritative file beats three diverging copies. site.css carries only
   one-declaration stubs so the class registry keeps the classes on elements.

   Accordion physics: flex 1 to 4 with the reference cubic-bezier(.25,1,.5,1).
   No-JS behaviour: .is-active (first item, in the markup) is the resting
   expanded card and :hover rules drive the accordion; gallery.js only makes
   the last hovered card stay expanded. Nothing is ever invisible: inactive
   cards are dimmed with brightness, not opacity. */
.eg { display: flex; gap: 16px; height: 600px; margin-top: 40px; }
.eg__item {
  position: relative; overflow: hidden; display: block; min-width: 0;
  border-radius: var(--radius-lg); background: var(--navy);
  flex: 1 1 0%; filter: brightness(0.55); text-decoration: none;
  transition: flex 0.7s cubic-bezier(0.25, 1, 0.5, 1), filter 0.7s cubic-bezier(0.25, 1, 0.5, 1);
}
.eg__item.is-active { flex: 4 1 0%; filter: brightness(1); }
.eg__item:not(.is-active):hover { filter: brightness(0.8); }
.eg:hover .eg__item { flex: 1 1 0%; filter: brightness(0.55); }
.eg:hover .eg__item:hover { flex: 4 1 0%; filter: brightness(1); }
.eg__media {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transform: scale(1.1); filter: saturate(0.6) contrast(1.04);
  transition: transform 1s cubic-bezier(0.25, 1, 0.5, 1);
}
.eg__item.is-active .eg__media, .eg:hover .eg__item:hover .eg__media { transform: scale(1); }
.eg__scrim {
  position: absolute; inset: 0; opacity: 0; transition: opacity 0.5s;
  background: linear-gradient(180deg, rgba(7,18,35,0) 40%, rgba(7,18,35,0.65) 75%, rgba(7,18,35,0.95) 100%);
}
.eg__item.is-active .eg__scrim, .eg:hover .eg__item:hover .eg__scrim { opacity: 1; }
.eg__body {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; flex-direction: column; align-items: flex-start; gap: 10px;
  padding: 28px; transform: translateY(48px); opacity: 0;
  transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1) 0.15s, opacity 0.5s 0.15s;
}
.eg__item.is-active .eg__body, .eg:hover .eg__item:hover .eg__body { transform: translateY(0); opacity: 1; }
.eg__tag {
  padding: 5px 12px; border: 1px solid rgba(255,255,255,0.3);
  background: rgba(255,255,255,0.12); backdrop-filter: blur(6px);
  border-radius: var(--radius-pill); color: var(--white);
  font-family: var(--font-heading); font-size: 11px; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase;
}
.eg__title {
  color: var(--white); font-family: var(--font-display); font-stretch: expanded;
  font-weight: 900; text-transform: uppercase;
  font-size: clamp(22px, 2.6vw, 40px); line-height: 1.04; letter-spacing: -0.02em;
}
.eg__cta {
  display: inline-flex; align-items: center; gap: 8px; margin-top: 6px;
  color: var(--celeste); font-family: var(--font-heading);
  font-size: 12.5px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase;
}
.eg__label {
  position: absolute; left: 50%; bottom: 28px; transform: translateX(-50%);
  writing-mode: vertical-rl; white-space: nowrap; color: var(--white);
  font-family: var(--font-heading); font-weight: 700; font-size: 15px;
  letter-spacing: 0.12em; text-transform: uppercase;
  transition: opacity 0.4s, transform 0.4s;
}
.eg__item.is-active .eg__label, .eg:hover .eg__item:hover .eg__label {
  opacity: 0; transform: translateX(-50%) scale(0.6);
}
@media (max-width: 900px) {
  .eg { flex-direction: column; height: auto; }
  .eg__item { min-height: 72px; }
  .eg__item.is-active { min-height: 340px; }
  /* Vertical text is a desktop flourish; stacked rows read horizontally. */
  .eg__label { writing-mode: horizontal-tb; left: 20px; bottom: 18px; transform: none; }
  .eg__item.is-active .eg__label { transform: scale(0.6); }
}
@media (prefers-reduced-motion: reduce) {
  .eg__item, .eg__media, .eg__body, .eg__scrim, .eg__label { transition: none !important; }
}


/* src/runtime/i18n.css */
/* Lenovo Construccion SRL - Chinese typography layer (/zh/ pages)
   ================================================================

   PROBLEM
   Archivo (--font-display / --font-heading / --font-body, see
   design-kit/tokens/typography.css) is self-hosted by the CMS at
   /uploads/fonts/archivo/ with LATIN-ONLY subsets. Chinese copy on /zh/
   pages currently has no matching glyphs in that font and silently falls
   back to system-ui, which reads as noticeably lighter/thinner than the
   brand's Archivo voice on every other page.

   THIS FILE
   Adds same-origin @font-face rules for a SUBSETTED Noto Sans SC (built by
   tools/i18n/fonts/subset-noto-sans-sc.py, see that script's header for the
   full pipeline) and repoints the shared design tokens to it, scoped to
   Chinese pages only. It is pushed to the CMS runtime bucket the same way
   as motion.css / menu.css (see tools/runtime/README.md) and never touches
   any file the Spanish/English pages depend on.

   CSP / SAME-ORIGIN
   instatic serves `default-src 'self'`, so every src: url() below is a
   root-relative same-origin path, no @import, no fonts.googleapis.com.
   The four woff2 files this CSS references
   (tools/i18n/fonts/noto-sans-sc-{400,600,700}.woff2) need to be uploaded
   to:

       /uploads/fsheONfWUnvpDvwK16qaD-noto-sans-sc-400.woff2
       /uploads/eTkQbZAXMXZ07OIZ9Jkqp-noto-sans-sc-600.woff2
       /uploads/BmwISGechfiU7_H4_9Jqc-noto-sans-sc-700.woff2

   mirroring the existing /uploads/fonts/archivo/ convention. Update the
   url()s below if the CMS ends up hosting them anywhere else.

   SCOPE: WHY A CLASS AND NOT :lang() ALONE
   <html lang> is not reliably set by the prerender/CMS pipeline, so every
   rule below is gated on ".lc-zh" - a class a runtime script sets on
   <html> for /zh/ pages - with ":lang(zh)" added as a secondary selector
   so this also activates correctly on its own if/when lang ever does get
   set (":lang(zh)" matches "zh", "zh-CN", "zh-Hans", etc. per the CSS
   BCP-47 prefix match). Neither selector touches component CSS files -
   every override here rides purely on the custom-property cascade already
   set up by design-kit/tokens/typography.css.

   font-display: swap on every face: this site's progressive-enhancement
   contract (see tools/runtime/README.md) never hides content waiting on a
   resource. Chinese text renders immediately in the fallback stack below
   and swaps to Noto Sans SC the instant it loads - it is never invisible.

   THE WEIGHT/STRETCH COMPROMISE (read this before touching font-weight
   here or in a component file)
   Archivo's display treatment is `font-weight: 900` (--fw-black) at
   `font-stretch: expanded` - a wide, ultra-heavy face. Noto Sans SC has
   neither ingredient:
     - No width axis at all (fvar only exposes `wght`), so
       `font-stretch: expanded` has nothing to select. Per the CSS Fonts
       spec this is not an error - the browser just renders the nearest
       (normal-width) instance - so the `font-stretch: expanded` already
       written into .ds-display / .project-card__year / .stat__value /
       .marquee__word etc. is left completely alone. It is inert for
       Chinese text and correct as-is; no override needed or possible.
     - A real weight-900 instance exists in the upstream variable font,
       but CJK strokes at 900 clot together at heading/display sizes -
       Chinese glyphs carry far more ink per character than Latin letters,
       so the same numeric weight reads noticeably heavier and less legible
       in Hans than in Latin. Shipping a 4th static weight file for a
       weight that actively hurts legibility isn't a trade worth making.
   COMPROMISE: only 400 / 600 / 700 are subsetted (they cover every
   font-weight value actually used against --font-body/--font-heading
   site-wide - see the subsetting script's WEIGHTS comment). Weight 700 is
   also the ceiling: --fw-extrabold (800) and --fw-black (900) are
   redefined to 700 *inside this scope only*, so every component that
   consumes those tokens (.ds-display, h1-h6, .project-card__title,
   .stat__value, .marquee__word, ...) automatically renders at Noto's
   boldest legible weight without any of those files being touched.
   --ls-tighter (the negative tracking .ds-display/.stat__value/
   .marquee__word use to read "tight and dynamic" on Latin) is reset to
   normal for the same reason letter-spacing on already-full-width,
   already-tightly-set Hans glyphs, negative tracking crowds strokes
   instead of adding energy. --ls-tight (used by h1-h6, -0.01em) is small
   enough to leave alone.
   Net effect: Chinese display/heading text is bold (700), full-width,
   normally tracked Noto Sans SC - as close to Archivo's visual weight as
   the two type systems' actual capabilities allow, not a literal
   weight-for-weight, stretch-for-stretch clone. */

/* Las rutas llevan el hash que asigna el CMS al subir el archivo: no se
   puede elegir la ruta destino. Si se regeneran las fuentes hay que
   volver a subirlas y actualizar estas URLs (tools/i18n/upload-fonts).
   Todas son same-origin, requisito de la CSP default-src self. */
@font-face {
  font-family: "Noto Sans SC";
  src: url("/uploads/fsheONfWUnvpDvwK16qaD-noto-sans-sc-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0020-007E, U+00A9, U+00B7, U+00F3, U+2014, U+2018-2019, U+201C-201D, U+2026, U+25BE, U+3000-3002, U+3008-300F, U+4E00, U+4E0D-4E0E, U+4E13, U+4E1A, U+4E2A, U+4E2D, U+4E3A, U+4E48, U+4E5F, U+4E86, U+4E8E, U+4EA4, U+4EA7, U+4EBA, U+4EC0, U+4ECE, U+4ED6, U+4ED8, U+4EE5, U+4EEC, U+4EF6-4EF7, U+4F01, U+4F18, U+4F1A, U+4F30, U+4F34, U+4F3C, U+4F4D, U+4F4F, U+4F53, U+4F5C, U+4F7F, U+4F8B, U+4F9B, U+4F9D, U+4FDD, U+4FE1, U+4FEE, U+503C, U+505A, U+5065, U+5148-5149, U+5165, U+5168, U+516C, U+5171, U+5173, U+5176-5177, U+5185, U+518D, U+51B3, U+51C6, U+51CF, U+51FA, U+5206, U+5212, U+521B, U+5229, U+522B, U+5230, U+5236, U+529E, U+52A0-52A1, U+5316, U+5319, U+533A, U+534F, U+5355, U+5382, U+5385, U+53A8, U+53CA, U+53D1, U+53EA, U+53EF-53F0, U+53F8, U+5404, U+5408, U+540A, U+540C-540D, U+5411, U+5417, U+5427, U+542B, U+544A, U+5468, U+547D, U+548C, U+54C1, U+54CD, U+54EA, U+5546, U+5584, U+56DB, U+56DE, U+56E0, U+56E2, U+56F4, U+56FA, U+56FD-56FE, U+5723, U+5728, U+5730, U+573A, U+5747, U+575A, U+578B, U+5904, U+5907, U+5916, U+591A, U+5927, U+5939, U+59CB, U+59D3, U+5B85, U+5B88-5B89, U+5B8C, U+5B9A, U+5B9E, U+5BA2, U+5BA4, U+5BB6, U+5BF9, U+5BFC, U+5C0A, U+5C0F, U+5C11, U+5C31, U+5C3C-5C3E, U+5C40, U+5C42, U+5C4B, U+5C55, U+5C5E, U+5DE5, U+5DF2, U+5E08, U+5E38, U+5E72-5E74, U+5E76, U+5E7F, U+5EA6, U+5EFA, U+5F00, U+5F0F, U+5F53, U+5F71, U+5FB7, U+5FEB, U+6001, U+6027, U+60A8, U+60F3, U+610F, U+611F, U+613F, U+6210-6211, U+6237, U+623F-6240, U+624B, U+6270, U+627F-6280, U+628A, U+6295, U+62A5, U+62C5, U+62DB, U+62FC, U+6301, U+6309, U+636E, U+63A5, U+63A7-63A8, U+63CF-63D0, U+643A, U+652F, U+6536, U+6539, U+653E, U+6548, U+6574, U+65AD, U+65B0, U+65B9, U+65BD, U+65E2, U+660E, U+666F-6670, U+6700, U+6709, U+670D, U+671F, U+6728, U+672A, U+672C, U+672F, U+6743, U+6750, U+6761, U+6765, U+677F, U+6784, U+679C, U+67E5, U+67F1, U+6805, U+6807, U+683C, U+6848, U+6881, U+697C, U+6B63-6B64, U+6BB5, U+6BCF, U+6C42, U+6CA1, U+6CD5, U+6CE8, U+6D3B, U+6D41, U+6D82, U+6DF1, U+6E05, U+6EE1, U+70B9, U+7167, U+724C, U+72B6, U+7387, U+73B0, U+7406, U+751F, U+7528, U+7531, U+7535, U+7559, U+7684, U+76D1, U+76D6, U+76EE, U+76F4, U+770B, U+77E5, U+786E, U+7891, U+79EF, U+7A0B, U+7A33, U+7A7A, U+7ACB, U+7AD9, U+7AEF, U+7B51, U+7B79, U+7B97, U+7BA1, U+7BB1, U+7C73, U+7C7B, U+7CBE, U+7CFB, U+7EBF, U+7EC4, U+7EC6-7EC8, U+7ECF, U+7ED3, U+7ED9, U+7EDF, U+7F16, U+7F51, U+7FFB, U+804A, U+804C, U+8054, U+822A, U+8282, U+82F1, U+8303, U+83DC, U+8425, U+884C, U+88C5, U+8981, U+8986, U+89C1-89C2, U+89C4, U+89C6, U+89E3, U+8A00, U+8BA1, U+8BA9, U+8BAE, U+8BBE, U+8BC1, U+8BC4, U+8BC6, U+8BC9, U+8BDD, U+8BF7, U+8C03, U+8C22, U+8D1F, U+8D23, U+8D28, U+8D39, U+8DB3, U+8DDF, U+8DE8, U+8E2A, U+8FC7, U+8FD1, U+8FD4, U+8FDB, U+8FF0, U+9001, U+900F, U+901A, U+9020, U+9053, U+9075, U+90AE, U+90E8, U+91C7, U+91CC-91CD, U+91CF, U+91D1, U+94A2, U+94A5, U+94FA, U+95E8, U+95EE, U+95F4, U+961F, U+9636, U+9650, U+966A, U+9700, U+9732, U+9760, U+9762, U+9875, U+9879, U+987E, U+9884, U+9891, U+9910, U+996E, U+9970, U+9996, U+9AD8, U+FE4F, U+FF01, U+FF08-FF09, U+FF0C-FF0D, U+FF1A-FF1B, U+FF1F, U+FF5E;
}

@font-face {
  font-family: "Noto Sans SC";
  src: url("/uploads/eTkQbZAXMXZ07OIZ9Jkqp-noto-sans-sc-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0020-007E, U+00A9, U+00B7, U+00F3, U+2014, U+2018-2019, U+201C-201D, U+2026, U+25BE, U+3000-3002, U+3008-300F, U+4E00, U+4E0D-4E0E, U+4E13, U+4E1A, U+4E2A, U+4E2D, U+4E3A, U+4E48, U+4E5F, U+4E86, U+4E8E, U+4EA4, U+4EA7, U+4EBA, U+4EC0, U+4ECE, U+4ED6, U+4ED8, U+4EE5, U+4EEC, U+4EF6-4EF7, U+4F01, U+4F18, U+4F1A, U+4F30, U+4F34, U+4F3C, U+4F4D, U+4F4F, U+4F53, U+4F5C, U+4F7F, U+4F8B, U+4F9B, U+4F9D, U+4FDD, U+4FE1, U+4FEE, U+503C, U+505A, U+5065, U+5148-5149, U+5165, U+5168, U+516C, U+5171, U+5173, U+5176-5177, U+5185, U+518D, U+51B3, U+51C6, U+51CF, U+51FA, U+5206, U+5212, U+521B, U+5229, U+522B, U+5230, U+5236, U+529E, U+52A0-52A1, U+5316, U+5319, U+533A, U+534F, U+5355, U+5382, U+5385, U+53A8, U+53CA, U+53D1, U+53EA, U+53EF-53F0, U+53F8, U+5404, U+5408, U+540A, U+540C-540D, U+5411, U+5417, U+5427, U+542B, U+544A, U+5468, U+547D, U+548C, U+54C1, U+54CD, U+54EA, U+5546, U+5584, U+56DB, U+56DE, U+56E0, U+56E2, U+56F4, U+56FA, U+56FD-56FE, U+5723, U+5728, U+5730, U+573A, U+5747, U+575A, U+578B, U+5904, U+5907, U+5916, U+591A, U+5927, U+5939, U+59CB, U+59D3, U+5B85, U+5B88-5B89, U+5B8C, U+5B9A, U+5B9E, U+5BA2, U+5BA4, U+5BB6, U+5BF9, U+5BFC, U+5C0A, U+5C0F, U+5C11, U+5C31, U+5C3C-5C3E, U+5C40, U+5C42, U+5C4B, U+5C55, U+5C5E, U+5DE5, U+5DF2, U+5E08, U+5E38, U+5E72-5E74, U+5E76, U+5E7F, U+5EA6, U+5EFA, U+5F00, U+5F0F, U+5F53, U+5F71, U+5FB7, U+5FEB, U+6001, U+6027, U+60A8, U+60F3, U+610F, U+611F, U+613F, U+6210-6211, U+6237, U+623F-6240, U+624B, U+6270, U+627F-6280, U+628A, U+6295, U+62A5, U+62C5, U+62DB, U+62FC, U+6301, U+6309, U+636E, U+63A5, U+63A7-63A8, U+63CF-63D0, U+643A, U+652F, U+6536, U+6539, U+653E, U+6548, U+6574, U+65AD, U+65B0, U+65B9, U+65BD, U+65E2, U+660E, U+666F-6670, U+6700, U+6709, U+670D, U+671F, U+6728, U+672A, U+672C, U+672F, U+6743, U+6750, U+6761, U+6765, U+677F, U+6784, U+679C, U+67E5, U+67F1, U+6805, U+6807, U+683C, U+6848, U+6881, U+697C, U+6B63-6B64, U+6BB5, U+6BCF, U+6C42, U+6CA1, U+6CD5, U+6CE8, U+6D3B, U+6D41, U+6D82, U+6DF1, U+6E05, U+6EE1, U+70B9, U+7167, U+724C, U+72B6, U+7387, U+73B0, U+7406, U+751F, U+7528, U+7531, U+7535, U+7559, U+7684, U+76D1, U+76D6, U+76EE, U+76F4, U+770B, U+77E5, U+786E, U+7891, U+79EF, U+7A0B, U+7A33, U+7A7A, U+7ACB, U+7AD9, U+7AEF, U+7B51, U+7B79, U+7B97, U+7BA1, U+7BB1, U+7C73, U+7C7B, U+7CBE, U+7CFB, U+7EBF, U+7EC4, U+7EC6-7EC8, U+7ECF, U+7ED3, U+7ED9, U+7EDF, U+7F16, U+7F51, U+7FFB, U+804A, U+804C, U+8054, U+822A, U+8282, U+82F1, U+8303, U+83DC, U+8425, U+884C, U+88C5, U+8981, U+8986, U+89C1-89C2, U+89C4, U+89C6, U+89E3, U+8A00, U+8BA1, U+8BA9, U+8BAE, U+8BBE, U+8BC1, U+8BC4, U+8BC6, U+8BC9, U+8BDD, U+8BF7, U+8C03, U+8C22, U+8D1F, U+8D23, U+8D28, U+8D39, U+8DB3, U+8DDF, U+8DE8, U+8E2A, U+8FC7, U+8FD1, U+8FD4, U+8FDB, U+8FF0, U+9001, U+900F, U+901A, U+9020, U+9053, U+9075, U+90AE, U+90E8, U+91C7, U+91CC-91CD, U+91CF, U+91D1, U+94A2, U+94A5, U+94FA, U+95E8, U+95EE, U+95F4, U+961F, U+9636, U+9650, U+966A, U+9700, U+9732, U+9760, U+9762, U+9875, U+9879, U+987E, U+9884, U+9891, U+9910, U+996E, U+9970, U+9996, U+9AD8, U+FE4F, U+FF01, U+FF08-FF09, U+FF0C-FF0D, U+FF1A-FF1B, U+FF1F, U+FF5E;
}

@font-face {
  font-family: "Noto Sans SC";
  src: url("/uploads/BmwISGechfiU7_H4_9Jqc-noto-sans-sc-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0020-007E, U+00A9, U+00B7, U+00F3, U+2014, U+2018-2019, U+201C-201D, U+2026, U+25BE, U+3000-3002, U+3008-300F, U+4E00, U+4E0D-4E0E, U+4E13, U+4E1A, U+4E2A, U+4E2D, U+4E3A, U+4E48, U+4E5F, U+4E86, U+4E8E, U+4EA4, U+4EA7, U+4EBA, U+4EC0, U+4ECE, U+4ED6, U+4ED8, U+4EE5, U+4EEC, U+4EF6-4EF7, U+4F01, U+4F18, U+4F1A, U+4F30, U+4F34, U+4F3C, U+4F4D, U+4F4F, U+4F53, U+4F5C, U+4F7F, U+4F8B, U+4F9B, U+4F9D, U+4FDD, U+4FE1, U+4FEE, U+503C, U+505A, U+5065, U+5148-5149, U+5165, U+5168, U+516C, U+5171, U+5173, U+5176-5177, U+5185, U+518D, U+51B3, U+51C6, U+51CF, U+51FA, U+5206, U+5212, U+521B, U+5229, U+522B, U+5230, U+5236, U+529E, U+52A0-52A1, U+5316, U+5319, U+533A, U+534F, U+5355, U+5382, U+5385, U+53A8, U+53CA, U+53D1, U+53EA, U+53EF-53F0, U+53F8, U+5404, U+5408, U+540A, U+540C-540D, U+5411, U+5417, U+5427, U+542B, U+544A, U+5468, U+547D, U+548C, U+54C1, U+54CD, U+54EA, U+5546, U+5584, U+56DB, U+56DE, U+56E0, U+56E2, U+56F4, U+56FA, U+56FD-56FE, U+5723, U+5728, U+5730, U+573A, U+5747, U+575A, U+578B, U+5904, U+5907, U+5916, U+591A, U+5927, U+5939, U+59CB, U+59D3, U+5B85, U+5B88-5B89, U+5B8C, U+5B9A, U+5B9E, U+5BA2, U+5BA4, U+5BB6, U+5BF9, U+5BFC, U+5C0A, U+5C0F, U+5C11, U+5C31, U+5C3C-5C3E, U+5C40, U+5C42, U+5C4B, U+5C55, U+5C5E, U+5DE5, U+5DF2, U+5E08, U+5E38, U+5E72-5E74, U+5E76, U+5E7F, U+5EA6, U+5EFA, U+5F00, U+5F0F, U+5F53, U+5F71, U+5FB7, U+5FEB, U+6001, U+6027, U+60A8, U+60F3, U+610F, U+611F, U+613F, U+6210-6211, U+6237, U+623F-6240, U+624B, U+6270, U+627F-6280, U+628A, U+6295, U+62A5, U+62C5, U+62DB, U+62FC, U+6301, U+6309, U+636E, U+63A5, U+63A7-63A8, U+63CF-63D0, U+643A, U+652F, U+6536, U+6539, U+653E, U+6548, U+6574, U+65AD, U+65B0, U+65B9, U+65BD, U+65E2, U+660E, U+666F-6670, U+6700, U+6709, U+670D, U+671F, U+6728, U+672A, U+672C, U+672F, U+6743, U+6750, U+6761, U+6765, U+677F, U+6784, U+679C, U+67E5, U+67F1, U+6805, U+6807, U+683C, U+6848, U+6881, U+697C, U+6B63-6B64, U+6BB5, U+6BCF, U+6C42, U+6CA1, U+6CD5, U+6CE8, U+6D3B, U+6D41, U+6D82, U+6DF1, U+6E05, U+6EE1, U+70B9, U+7167, U+724C, U+72B6, U+7387, U+73B0, U+7406, U+751F, U+7528, U+7531, U+7535, U+7559, U+7684, U+76D1, U+76D6, U+76EE, U+76F4, U+770B, U+77E5, U+786E, U+7891, U+79EF, U+7A0B, U+7A33, U+7A7A, U+7ACB, U+7AD9, U+7AEF, U+7B51, U+7B79, U+7B97, U+7BA1, U+7BB1, U+7C73, U+7C7B, U+7CBE, U+7CFB, U+7EBF, U+7EC4, U+7EC6-7EC8, U+7ECF, U+7ED3, U+7ED9, U+7EDF, U+7F16, U+7F51, U+7FFB, U+804A, U+804C, U+8054, U+822A, U+8282, U+82F1, U+8303, U+83DC, U+8425, U+884C, U+88C5, U+8981, U+8986, U+89C1-89C2, U+89C4, U+89C6, U+89E3, U+8A00, U+8BA1, U+8BA9, U+8BAE, U+8BBE, U+8BC1, U+8BC4, U+8BC6, U+8BC9, U+8BDD, U+8BF7, U+8C03, U+8C22, U+8D1F, U+8D23, U+8D28, U+8D39, U+8DB3, U+8DDF, U+8DE8, U+8E2A, U+8FC7, U+8FD1, U+8FD4, U+8FDB, U+8FF0, U+9001, U+900F, U+901A, U+9020, U+9053, U+9075, U+90AE, U+90E8, U+91C7, U+91CC-91CD, U+91CF, U+91D1, U+94A2, U+94A5, U+94FA, U+95E8, U+95EE, U+95F4, U+961F, U+9636, U+9650, U+966A, U+9700, U+9732, U+9760, U+9762, U+9875, U+9879, U+987E, U+9884, U+9891, U+9910, U+996E, U+9970, U+9996, U+9AD8, U+FE4F, U+FF01, U+FF08-FF09, U+FF0C-FF0D, U+FF1A-FF1B, U+FF1F, U+FF5E;
}

/* Re-point the shared design tokens for /zh/ pages only. Nothing outside
   this selector's scope is touched, so es/en pages are byte-for-byte
   unaffected - see design-kit/tokens/typography.css for the tokens being
   overridden. */
:root.lc-zh,
:root:lang(zh) {
  --font-display: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-heading: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-body: "Noto Sans SC", "Archivo", system-ui, sans-serif;

  /* Compromise: Noto Sans SC is not subsetted above weight 700 (see file
     header). Any component reading these two tokens (.ds-display, h1-h6,
     .project-card__title, .stat__value, .marquee__word, ...) is
     automatically capped at Noto's boldest legible weight instead of
     falling through to a missing 800/900 face. */
  --fw-extrabold: 700;
  --fw-black: 700;

  /* Negative tracking that reads as "tight, dynamic" on Latin display type
     crowds already full-width Hans glyphs instead. Reset to neutral for
     the same display-role elements that lost font-stretch: expanded. */
  --ls-tighter: 0;
}

/* --------------------------------------------------------------------------
   Variantes numeradas de los tokens de fuente.

   Cada Site Import agrega una copia numerada de TODOS los tokens del tema:
   hoy existen --font-display, --font-display-2 ... --font-display-6, y lo
   mismo para heading y body. 18 en total. Los elementos del sitio apuntan a
   la variante que existia cuando se importaron, no al token base: el h1 del
   hero usa var(--font-display-6) en su estilo inline.

   Redefinir solo los tres tokens base dejaba 15 variantes en Archivo y el
   chino seguia cayendo a system-ui pese a tener la fuente cargada. Como el
   estilo inline referencia la variable y no un valor literal, redefinir la
   variable alcanza: el var() se resuelve al nuevo stack.
   -------------------------------------------------------------------------- */
:root.lc-zh, :root:lang(zh) {
  --font-display-2: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-heading-2: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-body-2: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-display-3: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-heading-3: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-body-3: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-display-4: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-heading-4: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-body-4: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-display-5: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-heading-5: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-body-5: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-display-6: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-heading-6: "Noto Sans SC", "Archivo", system-ui, sans-serif;
  --font-body-6: "Noto Sans SC", "Archivo", system-ui, sans-serif;
}

/* Peso y tracking: aca SI hace falta !important, y no es un atajo.
   El h1 trae font-weight: 900 y letter-spacing negativos como estilos INLINE
   del CMS, no como variables, asi que ninguna redefinicion de token los
   alcanza. Noto Sans SC subsetada solo trae 400/600/700: pedir 900 hace que
   el navegador sintetice negrita sobre el 700 y los trazos CJK se empastan.
   El tracking negativo de Archivo aprieta glifos de ancho completo en vez de
   dar energia. Ambos se corrigen solo dentro del alcance chino. */
.lc-zh h1, .lc-zh h2, .lc-zh h3,
.lc-zh .stat__value, .lc-zh .eg__title, .lc-zh .nav-link,
.lc-zh .marquee__word, .lc-zh .svc-card__title {
  font-weight: 700 !important;
  letter-spacing: 0 !important;
  font-stretch: normal !important;
}


/* src/runtime/menu.css */
/* Menu panel styling, shipped as a raw style file rather than in
   tools/prerender/site.css.

   instatic's importer keeps a CSS rule only when its class token appears in
   the imported markup. Every class below is created or applied by
   tools/runtime/menu.js at runtime, so on import all of them were silently
   dropped and the panel rendered completely unstyled. Raw style files are
   concatenated into the userStyles bundle without passing through the class
   registry, so nothing here is pruned.

   Kept verbatim and contiguous from site.css. An earlier line-based extract
   lifted rules out of their @media wrappers, which left a stray selector and
   applied `transition: none !important` unconditionally instead of only under
   prefers-reduced-motion. */

/* --------------------------------------------------------------------------
   Navigation menu - vanilla port of React Bits' StaggeredMenu
   (docs: task brief "Build a navigation menu"). The React original drives
   everything through useState + gsap timelines; there is no React runtime on
   the published pages, so the behaviour is reproduced here as: a resting
   state that needs no JS at all, plus modifier classes that
   tools/runtime/menu.js applies at runtime to turn `#site-nav` into an
   off-canvas panel and drive it with gsap. Nothing in this file depends on
   JS having run - `.site-header__nav--panel` and `.is-open` are only ever
   added by menu.js, so a page where that script never loads renders none of
   this and falls straight back to the plain flex row below.
   -------------------------------------------------------------------------- */

/* Resting state - identical in spirit to the pre-existing inline nav: a
   plain, always-visible row of real links. This is what every page shows
   before menu.js (if it ever loads) rewrites the element. */
.site-header__nav { display: flex; align-items: center; gap: 2px; }
.nav-link {
  padding: 8px 14px; font-family: var(--font-heading); font-weight: 700;
  font-size: 13.5px; letter-spacing: 0.02em; white-space: nowrap; color: var(--navy);
}
.nav-link:hover { color: var(--celeste); }
.nav-link.is-active { color: var(--celeste); }

/* Panel state - modifier class added by menu.js only. Off-canvas by default
   (translateX(100%)); menu.js flips `.is-open` and drives the actual motion
   with a gsap timeline, but the CSS transition below is a real fallback path
   for prefers-reduced-motion (skip the timeline, just toggle the class) and
   for the brief instant between menu.js attaching the class and building its
   timeline. */
.site-header__nav--panel {
  position: fixed; inset: 0 0 0 auto; z-index: var(--z-modal);
  width: min(440px, 92vw); height: 100dvh;
  display: flex; flex-direction: column; justify-content: center; align-items: flex-start;
  gap: 6px; padding: var(--space-8) var(--space-7);
  background: var(--navy-900); box-shadow: var(--shadow-lg);
  transform: translateX(100%);
  transition: transform var(--dur-slow) var(--ease-out);
}
.site-header__nav--panel.is-open { transform: translateX(0); }
.site-header__nav--panel .nav-link {
  display: inline-flex; align-items: baseline; gap: 16px;
  padding: 10px 0; color: var(--white);
  font-family: var(--font-display); font-stretch: expanded; font-weight: var(--fw-black);
  font-size: clamp(30px, 5.4vw, 52px); line-height: 1.05; letter-spacing: var(--ls-tighter);
  text-transform: uppercase;
}
.site-header__nav--panel .nav-link:hover,
.site-header__nav--panel .nav-link.is-active { color: var(--celeste); }
/* Numbering - injected by menu.js next to each link (01, 02, ...) so gsap can
   fade/stagger it independently from the link's own rise+rotate. Not present
   at all without JS, which is correct: it is pure decoration. */
.nav-link__index {
  font-family: var(--font-heading); font-weight: 700; font-size: 13px;
  letter-spacing: var(--ls-wide); color: var(--celeste);
}

/* Coloured pre-layers - the staggered bands that slide in behind the panel a
   beat before it arrives. Elements only exist because menu.js injects them;
   nothing here renders without JS. Brand ramp, not the reference component's
   purple: celeste first, navy behind it. */
.menu-prelayer {
  position: fixed; inset: 0 0 0 auto; z-index: var(--z-overlay);
  width: min(440px, 92vw); height: 100dvh;
  transform: translateX(100%); pointer-events: none;
}
.menu-prelayer--1 { background: var(--celeste); }
.menu-prelayer--2 { background: var(--navy); }

/* Click-away scrim, injected alongside the pre-layers. */
.menu-backdrop {
  position: fixed; inset: 0; z-index: var(--z-overlay);
  background: var(--navy-900); opacity: 0;
  /* pointer-events none while closed is load-bearing: this element covers
     the whole viewport at opacity 0, and with pointer events on it silently
     intercepted every hover and click on all page content below the header.
     It only exists to catch click-away while the panel is open. */
  pointer-events: none;
  transition: opacity var(--dur-slow) var(--ease-standard);
}
.menu-backdrop.is-open { opacity: 0.5; pointer-events: auto; }

/* Body scroll lock while the panel is open - class toggled by menu.js. */
.has-menu-open { overflow: hidden; }

/* Toggle button - `plus` glyph built from two bars, rotates 45deg into an
   `X` on open. Ships `hidden` in the markup; menu.js is the only thing that
   ever un-hides it (see Header.jsx banner). */
.menu-toggle {
  display: inline-flex; align-items: center; gap: 10px;
  background: none; border: none; padding: 8px 4px; cursor: pointer;
  font-family: var(--font-heading); font-weight: 700; font-size: 13.5px;
  letter-spacing: 0.02em; color: var(--navy);
  transition: color var(--dur-fast) var(--ease-standard);
}
.menu-toggle:hover { color: var(--celeste); }
.menu-toggle__icon {
  position: relative; width: 18px; height: 18px; flex: none;
  transition: transform var(--dur-base) var(--ease-out);
}
.menu-toggle__bar {
  position: absolute; top: 50%; left: 50%; background: currentColor;
  transform: translate(-50%, -50%);
  transition: opacity var(--dur-fast) var(--ease-standard);
}
.menu-toggle__bar--h { width: 18px; height: 2px; }
.menu-toggle__bar--v { width: 2px; height: 18px; }
.menu-toggle.is-open .menu-toggle__icon { transform: rotate(45deg); }
/* Abierto en celeste, NO en blanco. El blanco era correcto mientras el panel
   tapaba el toggle: el texto "Cerrar" caia sobre navy. Al reparentar el panel
   al body, el header (z-toast) quedo por encima del panel (z-modal), asi que
   el toggle ahora se ve sobre el fondo blanco del header y en blanco era
   invisible. En mobile no aplica: ahi el toggle se oculta con el panel
   abierto y la X del panel es el unico control. */
.menu-toggle.is-open { color: var(--celeste); }

/* `.site-header` already establishes a stacking context (position: sticky +
   z-index), which means none of its descendants, however high their own
   z-index, can paint above the panel/backdrop by themselves - a child's
   z-index is only ever compared against siblings inside its nearest ancestor
   stacking context, so the whole header paints as one unit at whatever
   z-index the header itself has. The panel/backdrop live at z-modal/z-overlay
   as page-level fixed elements, so the header has to out-rank both here, or
   the open panel would visually and interactively bury the toggle that is
   supposed to close it. */
.site-header { z-index: var(--z-toast); }

@media (prefers-reduced-motion: reduce) {
  .site-header__nav--panel,
  .menu-prelayer,
  .menu-backdrop,
  .menu-toggle__icon { transition: none !important; }
}

/* Narrow viewports: only affects the resting (no-JS or pre-enhancement) row -
   once menu.js applies `--panel` the element is position: fixed and this rule
   no longer matches, so it never fights the panel layout. */
@media (max-width: 860px) {
  .site-header { height: auto; }
  .site-header__inner { flex-wrap: wrap; padding: 14px 20px; row-gap: 10px; }
  .site-header__nav:not(.site-header__nav--panel) {
    order: 3; width: 100%; justify-content: center; flex-wrap: wrap; gap: 4px 14px;
  }
}

/* Hide-on-scroll. The transform only ever exists while hidden or animating;
   at rest the header has none, so the fixed-position panel inside it keeps
   the viewport as its containing block (menu.js never hides while open). */
.site-header { transition: transform 0.3s var(--ease-out); }
.site-header--hidden { transform: translateY(-100%); }
@media (prefers-reduced-motion: reduce) {
  .site-header { transition: none !important; }
}

/* Mobile: burger de 3 barras sin etiqueta, panel a pantalla completa.
   La barra del medio solo existe en mobile; en desktop el icono sigue
   siendo el plus de 2 barras que rota a X. */
.menu-toggle__bar--m { display: none; }
@media (max-width: 640px) {
  .menu-toggle__label { display: none; }
  .menu-toggle { padding: 10px; }
  .menu-toggle__icon { width: 22px; height: 16px; transition: none; }
  .menu-toggle__bar--h { width: 22px; height: 2px; top: 0; transform: translate(-50%, 0); transition: top 0.2s var(--ease-out), transform 0.2s var(--ease-out); }
  .menu-toggle__bar--m { display: block; width: 22px; height: 2px; top: 50%; transform: translate(-50%, -50%); transition: opacity 0.15s; }
  .menu-toggle__bar--v { width: 22px; height: 2px; top: auto; bottom: 0; transform: translate(-50%, 0); transition: bottom 0.2s var(--ease-out), transform 0.2s var(--ease-out); }
  /* abierto: las 3 barras colapsan en una X */
  .menu-toggle.is-open .menu-toggle__icon { transform: none; }
  .menu-toggle.is-open .menu-toggle__bar--h { top: 50%; transform: translate(-50%, -50%) rotate(45deg); }
  .menu-toggle.is-open .menu-toggle__bar--m { opacity: 0; }
  .menu-toggle.is-open .menu-toggle__bar--v { bottom: auto; top: 50%; transform: translate(-50%, -50%) rotate(-45deg); }
  /* panel y precapas cubren toda la pantalla */
  .site-header__nav--panel, .menu-prelayer { width: 100vw; }
  .site-header__nav--panel { padding: var(--space-8) var(--space-6); }
  .site-header__nav--panel .nav-link { font-size: clamp(34px, 9vw, 44px); }
}

/* ---- Close X inside the panel -------------------------------------------
   Required, not decorative. The panel is z-modal and the toggle inherits
   z-auto, both inside the header's stacking context, so an open panel paints
   over the toggle (verified: elementFromPoint at the toggle centre returns
   the panel). Desktop still had Escape and click-away; mobile has a 100vw
   panel and no keyboard, so this was a dead end. */
.menu-close {
  position: absolute; top: 20px; right: 20px;
  width: 46px; height: 46px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; cursor: pointer;
  color: var(--white); z-index: 2;
}
.menu-close:focus-visible { outline: 2px solid var(--celeste); outline-offset: 3px; border-radius: 4px; }
.menu-close__bar {
  position: absolute; top: 50%; left: 50%;
  width: 24px; height: 2px; background: currentColor; border-radius: 2px;
}
.menu-close__bar--a { transform: translate(-50%, -50%) rotate(45deg); }
.menu-close__bar--b { transform: translate(-50%, -50%) rotate(-45deg); }

/* CTA mirrored into the panel; only shown where the header hides its own. */
.nav-cta { display: none; }

/* ---- Mobile navbar -------------------------------------------------------
   The header was wrapping onto two rows (logo on one, CTA + burger on the
   next). One row only: logo left, burger right. The CTA moves into the panel
   rather than being dropped, so the conversion path survives. */
@media (max-width: 640px) {
  .site-header__inner { flex-wrap: nowrap; gap: 12px; padding: 10px 18px; }
  .site-header__cta { display: none; }
  /* The logo height is an inline style from the CMS image node, so this is
     the one place !important is the correct tool rather than a shortcut. */
  .site-header__logo .logo { height: 38px !important; }
  .site-header__actions { gap: 0; }

  /* The full-screen panel covers the header, so the burger would sit on top
     of the panel's own X. One close control, not two. */
  .has-menu-open .menu-toggle { opacity: 0; pointer-events: none; }

  .site-header__nav--panel { padding: 88px var(--space-6) var(--space-8); justify-content: center; }
  .nav-cta {
    display: inline-flex; margin-top: 28px; align-self: flex-start;
  }
}

/* Safety net for horizontal overflow. `clip` rather than `hidden` on purpose:
   `hidden` turns the element into a scroll container, which silently breaks
   `position: sticky` on the header. `clip` does not. The real fix is that
   menu.js reparents the panel to the body, so nothing here is load-bearing;
   this only stops any future stray wide element from unlocking sideways
   scrolling on phones. */
body { overflow-x: clip; }


/* src/runtime/motion.css */
/* Pushed to the CMS as site.files["src/styles/motion.css"] (type: style),
   which instatic concatenates into the page-scoped userStyles bundle.
   Kept separate from tools/prerender/site.css because the CMS splits our
   stylesheet on import: selector rules become styleRules, @keyframes rides
   on a rule's rawCss, and everything else at-rule-shaped has no structured
   representation at all. This file is that remainder. */
@keyframes lc-blur-in {
  from { opacity: 0; filter: blur(7px); transform: translateY(10px); }
  to   { opacity: 1; filter: blur(0);   transform: translateY(0); }
}
@keyframes lc-fade-in { from { opacity: 0; } to { opacity: 1; } }
/* Word marquee (Home, between Services and Featured). Two identical
   `.marquee__group`s sit back to back in `.marquee__track` (width:
   max-content), so the track is exactly twice the width of one group.
   Translating it by -50% carries the first group fully out of view at the
   exact instant the second group reaches the start position, so the loop
   point is invisible - no JS needed to sync anything. The second row plays
   this same keyframe with animation-direction: reverse
   (.marquee__track--reverse in site.css), no second keyframe needed.
   tools/runtime/marquee.js optionally takes over both tracks at runtime to
   couple speed/direction to scroll velocity - see that file's header
   comment and the .marquee comment in site.css. */
@keyframes lc-marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
/* Hero photo fades in from the white ground behind it, timed with the copy
   reveal (same 640ms/ease). Declared together with the drift inside @supports
   because a second animation declaration replaces the first. */
.lc-parallax { animation: lc-fade-in 640ms var(--ease-out) both; }
@supports (animation-timeline: view()) {
  @keyframes lc-hero-drift {
    from { transform: translate3d(0, -7%, 0) scale(1.16); }
    to   { transform: translate3d(0,  7%, 0) scale(1.16); }
  }
  .lc-parallax {
    animation: lc-hero-drift linear both;
    animation-timeline: view();
    will-change: transform;
  }
}
@media (prefers-reduced-motion: reduce) {
  .lc-blur, .lc-parallax, .lc-fade, .marquee__track { animation: none !important; }
}


/* src/runtime/stats.css */
/* Stats card en mobile.
   Espejo del bloque equivalente en tools/prerender/site.css: vive aca ademas
   para que llegue al sitio publicado sin depender de un re-import, igual que
   menu.css y gallery.css.

   Tres problemas que arregla, visibles en 375px:

   1. Sin separadores. La media query de 900px en site.css hace
      `.g-stats > * { border-left: none !important }` para desarmar la grilla
      de 4 columnas, pero no pone nada en su lugar, asi que las cuatro cifras
      quedaban flotando en un bloque continuo sin jerarquia. El separador
      correcto al apilar es horizontal, no vertical: border-top entre celdas.

   2. `100,000+` desbordaba. `.stat__value` usa `var(--fs-display-lg)`, un
      token FIJO, asi que el numero mas largo se renderizaba al mismo tamano
      que en desktop y casi tocaba los bordes de la card. Pasa a clamp con
      unidad de viewport.

   3. Padding horizontal. Esa misma regla fuerza `padding-left: 0 !important`
      sobre las celdas, asi que el aire lateral tiene que vivir en la card,
      no en la celda. Pelear con el !important seria mas fragil. */
@media (max-width: 900px) {
  /* La banda: menos aire vertical en telefono (72px es mucho) y 20px
     laterales, que es exactamente el ancho contra el que se midio la escala
     tipografica de abajo. Sin esto la card quedaba borde a borde, porque la
     <section class="stats-band"> habia quedado VACIA con la card como
     hermana en vez de hija: su padding se aplicaba a una seccion sin
     contenido. En desktop no se notaba porque la card tiene max-width y
     margin auto. Corregido en los nodos del CMS; esto es el ajuste fino. */
  .stats-band { padding: 44px 20px; }
  .stats-card { padding: 6px 24px; }

  /* Vertical solamente: el horizontal lo pone la card (ver nota 3). */
  .stats-card__cell { padding: 26px 0; }

  .stats-card__cell + .stats-card__cell {
    border-left: none;
    border-top: 1px solid rgba(20, 40, 74, 0.28);
  }

  /* Escala con el viewport para que el valor mas largo entre siempre. */
  .stats-card .stat__value { font-size: clamp(38px, 12vw, 58px); }
  .stats-card .stat__label { font-size: 12px; letter-spacing: 0.1em; }
}

/* Telefonos angostos: la card se pega mas al borde y el numero baja otro
   escalon, porque 100,000+ es el que manda el ancho minimo. */
@media (max-width: 380px) {
  .stats-card { padding: 4px 18px; }
  .stats-card__cell { padding: 22px 0; }
  .stats-card .stat__value { font-size: clamp(34px, 11.5vw, 44px); }
}


/* src/runtime/switcher.css */
/* Language switcher, dropdown form.
   Lives here and not in tools/prerender/site.css because every class below is
   created by tools/runtime/switcher.js at runtime, and instatic's importer
   keeps a CSS rule only when its class appears in the imported markup. Rules
   for runtime-only classes are silently discarded on import.

   Closed state hides the list with visibility + opacity, NOT display:none or
   removal from the DOM. The options are real <a href> and stay crawlable and
   present at all times; only pointer and tab access are gated. */

.lang-switch {
  position: relative;
  display: inline-flex;
  flex: none;
}

/* ---- trigger ------------------------------------------------------------ */
.lang-switch__trigger {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 12px;
  background: none;
  border: 1px solid rgba(20, 40, 74, 0.22);
  border-radius: var(--radius-pill);
  cursor: pointer;
  color: var(--navy);
  font-family: var(--font-heading);
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  line-height: 1;
  transition: color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard),
              background var(--dur-fast) var(--ease-standard);
}
.lang-switch__trigger:hover {
  color: var(--celeste);
  border-color: var(--celeste);
}
.lang-switch__trigger:focus-visible {
  outline: 2px solid var(--celeste);
  outline-offset: 3px;
}

.lang-switch__caret {
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
  transition: transform var(--dur-fast) var(--ease-out);
}
.lang-switch.is-open .lang-switch__trigger { color: var(--celeste); border-color: var(--celeste); }
.lang-switch.is-open .lang-switch__caret { transform: rotate(180deg); }

/* ---- list --------------------------------------------------------------- */
.lang-switch__list {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  min-width: 158px;
  display: flex;
  flex-direction: column;
  padding: 6px;
  background: var(--white);
  border: 1px solid rgba(20, 40, 74, 0.14);
  border-radius: var(--radius-md, 10px);
  box-shadow: var(--shadow-md);
  /* z-index only has to beat siblings inside the header's own stacking
     context; the header already sits above the page. */
  z-index: 5;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
  transition: opacity var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out),
              visibility 0s linear var(--dur-fast);
}
.lang-switch.is-open .lang-switch__list {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition-delay: 0s;
}

.lang-switch__item {
  display: block;
  padding: 9px 12px;
  border-radius: 7px;
  color: var(--navy);
  font-family: var(--font-heading);
  font-size: 13.5px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-standard),
              color var(--dur-fast) var(--ease-standard);
}
.lang-switch__item:hover { background: rgba(0, 174, 239, 0.1); color: var(--celeste); }
.lang-switch__item:focus-visible { outline: 2px solid var(--celeste); outline-offset: -2px; }
.lang-switch__item.is-active { background: var(--navy); color: var(--white); }
.lang-switch__item.is-active:hover { background: var(--navy); color: var(--white); }

/* Closed: keep the links in the DOM for crawlers but out of the tab order. */
.lang-switch:not(.is-open) .lang-switch__list { pointer-events: none; }

/* ---- panel variant (navy background) ------------------------------------ */
.site-header__nav--panel .lang-switch { margin-top: 26px; }
.site-header__nav--panel .lang-switch__trigger {
  color: var(--white);
  border-color: rgba(255, 255, 255, 0.4);
  font-size: 14px;
  padding: 10px 16px;
}
.site-header__nav--panel .lang-switch__trigger:hover { color: var(--celeste); border-color: var(--celeste); }
/* The list opens UPWARD in the panel: the switcher sits low in a full-height
   column, so a downward menu would run off the bottom on short viewports. */
.site-header__nav--panel .lang-switch__list {
  top: auto;
  bottom: calc(100% + 8px);
  transform: translateY(4px);
}
.site-header__nav--panel .lang-switch.is-open .lang-switch__list { transform: translateY(0); }

/* ---- mobile -------------------------------------------------------------
   Under 640px the header is one tight row (logo left, burger right) and the
   CTA is already hidden there, so the header copy would push it to two rows.
   The in-panel copy is the mobile path. */
@media (max-width: 640px) {
  .lang-switch--header { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .lang-switch__trigger,
  .lang-switch__caret,
  .lang-switch__list,
  .lang-switch__item { transition: none !important; }
}
