/* MADV — CSS que as utilidades Tailwind não exprimem.
   ===================================================================
   FICHEIRO GERADO. Não editar aqui — editar prototype/assets/site.css
   e correr:

       python wp-content/mu-plugins/madv/tools/sync_site_css.py

   O conteúdo é cópia integral do site.css do protótipo, embrulhado em
   `@layer utilities`, mais o apêndice específico do WordPress no fim.
   O porquê de cada uma das três coisas está no cabeçalho do script.
   =================================================================== */

@layer utilities {
/* ── Radius scale (design system) ────────────────────────────────────
   Radius is opt-in and lives on a 3-step scale. Keep these values in sync
   with the Tailwind `borderRadius` tokens in build.py (control/card/panel).
     --r-sm  controls        — inputs, buttons
     --r-md  attached surface — the router results popover, small cards
     --r-lg  large surface    — hero strip, menu panel
   Non-control elements are square by default, so nothing rounds unless a
   component rule below or a `rounded-*` utility asks for it. Native form
   controls carry an intrinsic radius on some platforms — zero it here. */
  :root{ --r-sm:.5rem; --r-md:.75rem; --r-lg:1.5rem;
         /* ── palette ──────────────────────────────────────────
            Mirrors the Tailwind config in build.py so the SAME six values are
            reachable from plain CSS. Before this existed, site.css referenced
            var(--rule) which was never defined anywhere: the fallback was doing
            all the work and the file only looked tokenised.
            Keep in sync with build.py `colors:` until the Oxygen port makes
            one of the two the single source. */
         --paper:  #FAFAFA;
         --ink:    #111110;
         --muted:  #6B6763;
         --rule:   #DDDAD5;
         --maroon: #6A1312;
         --maroonD:#511110;
         /* the "recede" value shared by #area-cycle and the overlay menu */
         --recede: #C9C4BF;
         /* ── motion tokens (the whole system tunes from here) ──
            --ease-out is an expo-out decel: strong start, long soft settle.
            That long tail is what reads as "expensive" vs a plain ease. */
         --ease-out:  cubic-bezier(.16,1,.3,1);
         --ease-line: cubic-bezier(.22,1,.32,1);
         --dur-hero:  .85s;
         --dur-sec:   .72s;
         --dur-child: .58s;
         --dur-line:  .78s;
         --rise:      clamp(20px, 2.6vw, 34px);   /* section-level travel */
         --stagger:   80ms; }
  button, input, select, textarea { border-radius: 0; }
  html { scroll-behavior: smooth; }
  body { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
  .u-link { background-image: linear-gradient(currentColor, currentColor);
            background-size: 0% 1px; background-repeat: no-repeat;
            background-position: 0 100%; transition: background-size .25s ease; }
  .u-link:hover, .u-link:focus-visible { background-size: 100% 1px; }
  :focus-visible { outline: 2px solid #6A1312; outline-offset: 3px; }

  /* Last-resort flush. The inline head script adds this when site.js never
     reported for duty, so a failed script degrades to a fully visible page
     instead of a blank one. Must out-specify `.js-on .reveal{opacity:0}`. */
  html.reveal-flush .reveal,
  html.reveal-flush .reveal-sm,
  html.reveal-flush .reveal-stagger > *,
  html.reveal-flush .reveal-lines .rl-in { opacity:1 !important; transform:none !important; }
  html.reveal-flush [data-type-q]        { visibility:visible !important; }
  /* Timeline spines. `.reveal-stagger > *` above already catches .spine-draw
     because it is a direct child of the <ol>, but .spine-conn and .spine-seg sit
     INSIDE a .reveal rather than being one, so the flush never reached them:
     with site.js dead the connector stayed at scaleY(0) and the timeline line
     broke above the MADV disc. Verified 2026-07-30 by removing site.js and
     leaving the inline failsafe intact. */
  html.reveal-flush .spine-conn,
  html.reveal-flush .spine-seg           { transform:none !important; }
  /* the equipa profile panels wait on .is-open, which only site.js ever sets */
  html.reveal-flush .team-panel-in,
  html.reveal-flush .team-panel-in > div > div > div,
  html.reveal-flush .team-panel-in > div > div { opacity:1 !important; transform:none !important; }

  /* ══ MOTION LAYER ══════════════════════════════════════════════════
     Principle: animation is ENHANCEMENT, never a gate.
     Every element's base state is visible + correct. If JS is off, if
     animation-timeline is unsupported, or if the user prefers reduced
     motion, the page renders complete and static. Nothing sits at
     opacity:0 waiting to be rescued (the flaw in all 16 peer sites).
     ════════════════════════════════════════════════════════════════ */

  @keyframes rise      { from { opacity:0; transform: translateY(14px) } to { opacity:1; transform:none } }
  @keyframes riseSmall { from { opacity:0; transform: translateY(8px)  } to { opacity:1; transform:none } }
  @keyframes wipeIn    { from { clip-path: inset(0 100% 0 0) }          to { clip-path: inset(0 0 0 0) } }
  @keyframes ruleGrow  { from { transform: scaleY(0) }                   to { transform: scaleY(1) } }

  @media (prefers-reduced-motion: no-preference) {

    /* ── On-load hero entrance ──────────────────────────────────────
       Uses a transition toggled by a class, NOT a CSS animation.
       A running CSS animation on an element overlaying the WebGL canvas
       promotes it to a compositing layer and produces a black artifact
       over the canvas (reproduced: disabling the animations removed it).
       A transition that has finished leaves no persistent layer.        */
    .anim-load      { opacity: 0; transform: translateY(16px);
                      transition: opacity var(--dur-hero) var(--ease-out),
                                  transform var(--dur-hero) var(--ease-out); }
    html.ready .anim-load { opacity: 1; transform: none; }
    /* .anim-load is a TRANSITION (see note above), so the stagger must use
       transition-delay — animation-delay is inert here and left it un-cascaded. */
    .anim-load-d1   { transition-delay: .06s }
    .anim-load-d2   { transition-delay: .14s }
    .anim-load-d3   { transition-delay: .22s }
    .anim-load-d4   { transition-delay: .30s }
    .anim-load-d5   { transition-delay: .40s }

    /* Router question types in as the last hero beat. Hidden until the
       typewriter runs (js-on set synchronously in <head>, so no flash);
       min-height reserves the line so typing causes no layout shift. No JS ->
       full question shown. Reduced motion -> shown in full, no typing. */
    [data-type-q]        { min-height: 1.5em; }
    html.js-on [data-type-q]         { visibility: hidden; }
    html.js-on [data-type-q].type-on { visibility: visible; }
    .type-cursor         { display: inline-block; width: .5ch; margin-left: 1px;
                           font-weight: 300; opacity: .8; }

    .anim-wipe      { animation: wipeIn 1.05s cubic-bezier(.22,.9,.3,1) both; }
    .anim-rule      { animation: ruleGrow .9s cubic-bezier(.22,.9,.3,1) .25s both; transform-origin: top; }

    /* ── Scroll reveal: ONE-SHOT, JS-gated.
       The hidden state is added BY JS (.js-on). No JS -> never hidden.
       Reduced motion -> JS opts out. Once revealed, stays revealed
       (no re-animating on every scroll pass, which reads as fidgety). ── */
    .js-on .reveal         { opacity: 0; transform: translateY(var(--rise));
                             transition: opacity var(--dur-sec) var(--ease-out),
                                         transform var(--dur-sec) var(--ease-out); }
    .js-on .reveal-sm      { opacity: 0; transform: translateY(14px);
                             transition: opacity var(--dur-sec) var(--ease-out),
                                         transform var(--dur-sec) var(--ease-out); }
    .js-on .reveal-stagger > * { opacity: 0; transform: translateY(16px);
                             transition: opacity var(--dur-child) var(--ease-out),
                                         transform var(--dur-child) var(--ease-out); }
    .js-on .is-in,
    .js-on .is-in > *      { opacity: 1 !important; transform: none !important; }

    /* ── Masked line-reveal (the award-site signature) ──────────────────
       Each line is clipped by an overflow-hidden wrapper; the inner sits
       one line-height BELOW and slides up into view, staggered per line.
       Structure: .reveal-lines > .rl-line > .rl-in (see _home.main.html).
       Base state (no .js-on) leaves .rl-in in place, so no-JS / reduced
       motion show the heading complete and static. The padding/margin pair
       gives descenders and italics room so the clip never crops a glyph. */
    .js-on .reveal-lines .rl-line { display: block; overflow: hidden;
                                    padding-bottom: .12em; margin-bottom: -.12em; }
    .js-on .reveal-lines .rl-in   { display: inline-block; transform: translateY(115%);
                                    transition: transform var(--dur-line) var(--ease-line); }
    .js-on .reveal-lines.is-in .rl-in            { transform: none; }
    .js-on .reveal-lines.is-in .rl-line:nth-child(2) .rl-in { transition-delay: .10s; }
    .js-on .reveal-lines.is-in .rl-line:nth-child(3) .rl-in { transition-delay: .20s; }
    .js-on .reveal-lines.is-in .rl-line:nth-child(4) .rl-in { transition-delay: .30s; }
    /* the generic is-in reset targets direct children (.rl-line); keep it from
       yanking the clipped inner to its final spot before the transition runs. */
    .js-on .reveal-lines.is-in .rl-in            { opacity: 1; }

    /* ── Line-draw: the timeline spine and the MADV connector grow DOWNWARD
       (transform-origin top) as the points cascade in, instead of fading.
       They opt out of the generic reveal above — which would snap them to
       transform:none — by carrying their own scaleY, !important where it must
       beat the generic is-in reset. JS-gated + reduced-motion-safe like the
       rest (no .js-on -> full-height line, no animation). */
    .js-on .reveal-stagger > .spine-draw {
      opacity: 1; transform: scaleY(0); transform-origin: top center;
      transition: transform 1s var(--ease-out);
    }
    .js-on .reveal-stagger.is-in > .spine-draw { transform: scaleY(1) !important; }

    .js-on .spine-conn {
      transform: scaleY(0); transform-origin: top center;
      transition: transform .65s var(--ease-out);
    }
    .js-on .reveal.is-in .spine-conn { transform: scaleY(1); }

    /* ── /a-sociedade/ timeline: the spine is segmented per milestone, so it
       draws PROGRESSIVELY as each <li> reveals on scroll rather than the whole
       line arriving at once. Five long milestones need that; the homepage's four
       one-liners do not.
       The !important and the 4-class specificity are both required: line 127
       (`.js-on .is-in > *`) forces transform:none !important on every direct
       child of a revealed element, which would flatten the draw to an instant
       full-height line. (0,4,0) beats its (0,2,0). */
    .js-on .reveal .spine-seg {
      transform: scaleY(0); transform-origin: top center;
      transition: transform .7s var(--ease-out);
    }
    .js-on .reveal.is-in > .spine-seg { transform: scaleY(1) !important; }

    /* ── Micro-interaction: area rows nudge on hover ── */
    #areas li a { transition: color .2s ease, padding-left .28s cubic-bezier(.22,.9,.3,1); }
    #areas li a:hover, #areas li a:focus-visible { padding-left: 8px; }
  }

  /* Counter: the real value lives in the HTML. JS only animates toward it. */
  .tabular { font-variant-numeric: tabular-nums; }

  /* ── Team carousel: CSS-only horizontal scroll-snap. Thin scrollbar as the
     scroll affordance; snap for tidy stops. -webkit-overflow-scrolling for
     momentum on touch. ── */
  .team-carousel { -webkit-overflow-scrolling: touch; scrollbar-width: thin;
                   scrollbar-color: #DDDAD5 transparent; scroll-padding-left: 1.5rem; }
  .team-carousel::-webkit-scrollbar { height: 6px; }
  .team-carousel::-webkit-scrollbar-track { background: transparent; }
  .team-carousel::-webkit-scrollbar-thumb { background: #DDDAD5; border-radius: 3px; }
  @media (min-width:1024px){ .team-carousel { scroll-padding-left: 4rem; } }

/* ── corner brackets (RRE detailing) ── */
.corner::before, .corner::after,
.corner > .c3, .corner > .c4 { content:""; position:absolute; width:9px; height:9px; }
.corner::before { top:0; left:0;  border-top:1px solid currentColor; border-left:1px solid currentColor; }
.corner::after  { top:0; right:0; border-top:1px solid currentColor; border-right:1px solid currentColor; }
.corner .c3     { bottom:0; left:0;  border-bottom:1px solid currentColor; border-left:1px solid currentColor; }
.corner .c4     { bottom:0; right:0; border-bottom:1px solid currentColor; border-right:1px solid currentColor; }

/* ── practice-area cycle list (Harvey) ──────────────────────────────
   No JS: every item is fully legible. JS dims the inactive ones and
   cycles focus, so nothing is ever hidden behind the effect.        */
#area-cycle a { color: #111110; display: block; padding: 2px 0; }
.js-on #area-cycle li.is-active a { color: #111110; }
#area-cycle a:hover, #area-cycle a:focus-visible { color: #6A1312 !important; }

/* ⚠ THE DIM IS GATED ON no-preference, AND MUST STAY GATED.
   js-on is added in <head> unconditionally, but the cycle script returns early
   under prefers-reduced-motion — so nothing ever gets .is-active. Ungated, this
   rule left the whole list stranded at --recede (#C9C4BF): about 2.1:1 on white,
   where large text needs 3:1. Reduced motion means "do not animate", not "make
   it unreadable". Found 2026-08-11 while porting the list to WordPress; the
   same fault was live on madv.pt and needs a rebuild there. */
@media (prefers-reduced-motion: no-preference) {
  .js-on #area-cycle a { color: var(--recede); transition: color .55s ease; }
}

/* ── hero field ────────────────────────────────────────────────────
   The shader paints into a canvas it appends here. Without JS we fall
   back to the baked texture (see <noscript> in the shell) with a slow
   CSS drift; the script neutralises that drift when it takes over.   */
@keyframes heroDrift{
  from{transform:translate3d(-2%,-1%,0) scale(1.12)}
  to  {transform:translate3d(3%,2%,0)   scale(1.20)}
}

/* ══ SHADER MASK ═══════════════════════════════════════════════════
   A masked numeral/word: the hero field shown through the glyphs, maroon
   only inside the letters (the accent-only device). Knockout layering, so
   the live canvas is never itself masked (see mask-field.js):
     .mask-fill  maroon solid   — the no-WebGL / no-JS fallback
     canvas      live field     — inserted between fill and cut
     .mask-cut   paper knockout — paper everywhere except the glyph holes
   ⚠ .mask-cut rect fill MUST equal the host section's background, or the
     area around the glyphs will not disappear. Currently paper (#FAFAFA). */
.mask{ position:relative; width:clamp(9rem,22vw,15rem); aspect-ratio:440/360; }
.mask-fill{ position:absolute; inset:0; background:#6A1312; }
.mask canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; }
.mask-cut{ position:absolute; inset:0; width:100%; height:100%; display:block; }
/* Direct child only — must NOT recolour the mask's inner white rect, whose
   luminance has to stay 1 (pure #fff) or the knockout goes translucent and
   the layer behind bleeds a faint box. */
.mask-cut > rect{ fill:#FAFAFA; }
.mask-text{ font-family:"Fraunces",serif; font-weight:600; }

/* Promise triangle cluster: desktop sits on the right; below lg it moves to the
   top-centre (resized) so it isn't cropped and reads above the statement. */
.cluster-mob{ display:none; }
@media (max-width:1023px){
  .cluster-desk{ display:none; }
  .cluster-mob{ display:inline; }
}

/* areas-index inline filter: hidden until area-filter.js marks it ready, so
   no-JS renders the full list with no orphaned input. */
[data-area-filter]{ display:none; }
[data-area-filter].is-ready{ display:block; }
/* same contract for the publicações author filter */
[data-post-filter]{ display:none; }
[data-post-filter].is-ready{ display:block; }
/* The [hidden] ATTRIBUTE is a UA-stylesheet rule, so any Tailwind display
   utility on the same element outranks it and the element stays on screen.
   Bit twice now: #contact-modal (guarded above) and these two.
     · the clear button carries inline-flex and rendered while hidden
     · a post list carries grid and is hidden at RUNTIME, so a static scan of
       the built HTML cannot catch it — only a rendered check can
   Always verify with offsetParent/computed display, never with el.hidden. */
[data-filter-clear][hidden],
[data-post-list][hidden]{ display:none !important; }

/* Featured post — ONLY while the archive is unfiltered. Once a facet is on,
   every result renders at the same size: a single double-width card among the
   matches reads as a different kind of object rather than one of N results,
   which is exactly what it looked like when a filter left only the lead.
   The 16/7 crop is a desktop device — at 390px it collapses to a ~129px
   letterbox, so mobile keeps the normal 3/2 either way. */
[data-post-list]:not(.is-filtering) [data-featured]{ grid-column: 1 / -1; }
[data-post-list]:not(.is-filtering) [data-featured] .post-title{
  font-size: clamp(1.6rem, 3vw, 2.3rem); line-height: 1.15; }
@media (min-width: 768px){
  [data-post-list]:not(.is-filtering) [data-featured] .post-img{ aspect-ratio: 16 / 7; }
}

/* field panel: the live hero field shown directly (no knockout) as a band/box.
   The maroon gradient on the element is the no-WebGL / no-JS fallback; the
   canvas covers it when the shader runs. */
.field-panel{ position:relative; overflow:hidden;
  background:radial-gradient(130% 220% at 22% -10%, #7a1614 0%, #3a0d0b 46%, #120302 100%); }
.field-panel canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; }

/* square mask (for the triad shape); base .mask defaults to the numeral aspect */
.mask--sq{ aspect-ratio:1; }

/* ── outer-rounded (concave) corners where the white block meets the field
   gutters. The block's bottom corners stay square; instead a paper fillet fills
   the gutter's bottom corner so the FIELD reads as an outer-rounded corner. Pure
   CSS via a radial-gradient pseudo-element (transparent inside the arc = field
   shows; paper outside = the concave fillet). Attached to the block's parent so
   it can sit OUTSIDE the block's overflow:hidden, in the gutter. */
.gutter-round{ position:relative; }
.gutter-round::before,
.gutter-round::after{
  content:""; position:absolute; bottom:0; width:1.5rem; height:1.5rem;
  pointer-events:none;
}
.gutter-round::before{                    /* left gutter, bottom-right corner */
  left:-1.5rem;
  background:radial-gradient(circle 1.5rem at top left, #0000 98%, #FFFFFF 100%);
}
.gutter-round::after{                     /* right gutter, bottom-left corner */
  right:-1.5rem;
  background:radial-gradient(circle 1.5rem at top right, #0000 98%, #FFFFFF 100%);
}

/* ── triangle-mesh accent (the client's signature motif, distilled) ─────
   Their triangle grid, in maroon, faded to nothing at one edge, BEHIND
   content. USE RARELY — a texture, never a full background. Put .tri-mesh on
   a container; it paints a faded ::before behind its (positioned) children.
   Fade angle via --tri-fade (default fades out to the left). */
.tri-mesh{ position:relative; }
.tri-mesh > *{ position:relative; z-index:1; }
.tri-mesh::before{
  content:""; position:absolute; inset:0; z-index:0; pointer-events:none; opacity:.32;
  background-image:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='34'%20height='30'%3E%3Cpolygon%20points='17,3%2032,27%202,27'%20fill='none'%20stroke='%236A1312'%20stroke-width='1'/%3E%3C/svg%3E");
  background-size:34px 30px;
  -webkit-mask-image:linear-gradient(var(--tri-fade,270deg),#000,transparent 65%);
          mask-image:linear-gradient(var(--tri-fade,270deg),#000,transparent 65%);
}

/* ══ MENU PANEL ════════════════════════════════════════════════════
   Cucinelli pattern, mirrored to the right.
   No JS  -> #menu-panel is a normal block of links at the end of the
             document. Everything reachable, nothing hidden.
   JS     -> becomes a fixed overlay that grows from the top-right.
   ══════════════════════════════════════════════════════════════════ */

/* Base (also the no-JS state): every item fully legible.
   The left padding reserves the current-page marker column on EVERY item, so
   marking one does not shove it out of alignment with the rest of the list.
   These live here rather than as utility classes on the markup because the
   `#menu-list a` ID selector outranks any Tailwind utility. */
#menu-list a { position:relative; display:block; padding:.18em 0 .18em 2.5rem; color:#111110; }

/* Current page. Colour is not the only signal: the rule below is a visible
   marker and aria-current carries it for assistive tech. `currentColor` keeps
   the marker in step with the hover dim/undim transitions further down. */
#menu-list a[aria-current="page"] { color:#6A1312; }
#menu-list a[aria-current="page"]::before{
  content:""; position:absolute; left:0; top:50%;
  width:1.5rem; height:1px; background:currentColor;
}

/* ---- JS-enhanced overlay ---- */
.js-on #menu-panel{
  position:fixed; top:.75rem; right:.75rem; bottom:.75rem; z-index:50;
  width:min(30rem, calc(100vw - 1.5rem));
  /* floated off the edges so the radius is visible; full border, not just left */
  border:1px solid #DDDAD5;
  border-radius:var(--r-lg);
  overflow-y:auto; overscroll-behavior:contain;
  /* grow from the top-right corner. The `round` keyword keeps the clip
     rectangle's corners rounded — a plain inset() clips to a sharp rect
     and would square off border-radius through the whole animation. */
  clip-path: inset(0 0 100% 100% round var(--r-lg));
  visibility:hidden;
  transition: clip-path .62s cubic-bezier(.22,.9,.26,1), visibility 0s linear .62s;
}
.js-on #menu-panel.is-open{
  clip-path: inset(0 0 0 0 round var(--r-lg));
  visibility:visible;
  transition: clip-path .62s cubic-bezier(.22,.9,.26,1), visibility 0s;
}
.js-on #menu-close{ display:flex; }
.js-on #menu-backdrop:not([hidden]){ opacity:1; }

/* items fade in just behind the panel edge */
.js-on #menu-panel .menu-inner > *{
  opacity:0; transform:translateY(10px);
  transition:opacity .5s ease, transform .5s cubic-bezier(.22,.9,.26,1);
}
.js-on #menu-panel.is-open .menu-inner > *{ opacity:1; transform:none; }
.js-on #menu-panel.is-open .menu-inner > *:nth-child(2){ transition-delay:.16s }
.js-on #menu-panel.is-open .menu-inner > *:nth-child(3){ transition-delay:.24s }

/* the Cucinelli move: hovering one item recedes the others */
/* Hover treatment mirrors the homepage practice-area list (#area-cycle):
   same 0.55s ease, same dim value, hovered item goes MAROON not ink.    */
.js-on #menu-list a{ transition: color .55s ease; }
/* The current page is EXEMPT from the recede. Without :not() these selectors
   (0,2,1) outrank #menu-list a[aria-current] (0,1,1), so the one item the user
   needs to locate dims to 1.66:1 the moment the pointer enters the list, and
   the ::before marker follows it via currentColor. */
.js-on #menu-list:hover a:not([aria-current="page"]),
.js-on #menu-list:focus-within a:not([aria-current="page"]){ color:var(--recede); }
/* ⚠ TEM DE VENCER O RECUO ACIMA, e não vencia. O item sob o rato também está
   dentro de #menu-list:hover, portanto as duas regras disputam-no:

       .js-on #menu-list:hover a:not([aria-current="page"])   (1,3,1)  recuo
       .js-on #menu-list a:hover                              (1,2,1)  hover

   O :not() conta para a especificidade, e o recuo ganhava — o link sob o rato
   ficava cinzento como os outros em vez de vinho. A intenção estava escrita no
   comentário lá em cima desde o início e nunca chegou a acontecer.

   Repetir o :not() e o :hover no mesmo selector leva-o a (1,4,1) e resolve sem
   depender da ordem das regras, que é o género de equilíbrio que se desfaz
   quando alguém reordena o ficheiro. */
.js-on #menu-list:hover a:not([aria-current="page"]):hover,
.js-on #menu-list:focus-within a:not([aria-current="page"]):focus-visible,
.js-on #menu-list a:focus-visible{ color:#6A1312; }

/* hamburger -> close */
#menu-toggle .mt-bar{ transition:transform .4s cubic-bezier(.22,.9,.26,1), opacity .3s ease; }
#menu-toggle[aria-expanded="true"] .mt-bar-1{ transform:translateY(6px) rotate(45deg); }
#menu-toggle[aria-expanded="true"] .mt-bar-2{ opacity:0; transform:translateY(-50%) scaleX(.4); }
#menu-toggle[aria-expanded="true"] .mt-bar-3{ transform:translateY(-6px) rotate(-45deg); }

body.menu-open{ overflow:hidden; }

@media (prefers-reduced-motion: reduce){
  .js-on #menu-panel,
  .js-on #menu-panel .menu-inner > *,
  #menu-toggle .mt-bar { transition-duration:.01ms !important; }
}

/* ── hover das imagens: 700ms ─────────────────────────────────────
   A marcação pede `duration-700` nas dezaseis imagens com hover do site, mas
   ESSA CLASSE NÃO ESTÁ COMPILADA no output.css — nada a usava até agora, e o
   Winden compila NO BROWSER. Sem esta regra as imagens ficavam sem duração
   nenhuma e o hover passava a instantâneo: pior do que os 500ms de que se
   estava a sair.

   ⚠ QUANDO O WINDEN FOR COMPILADO, os dois passam a dizer 700 e este bloco
   torna-se redundante — pode sair. Enquanto não for, é ele que manda.

   ⚠ E SE ALGUÉM MUDAR A CLASSE, TEM DE MUDAR ESTA LINHA TAMBÉM. É o preço de
   ter o valor em dois sítios, e é deliberado: a alternativa era a marcação
   dizer 700 e o site fazer 0.

   `img.grayscale` chega e não precisa de mais: TODAS as imagens com grayscale
   no site têm o hover — verificado nas quatro páginas que as têm, 16 de 16. O
   invólucro leva a mesma duração porque a sombra dele faz parte do mesmo gesto;
   a 500 acabava 200ms antes da imagem, e isso vê-se.                        */
img.grayscale,
div.team-card-photo{ transition-duration: .7s; }

/* 🔴 E O ZOOM TEM DE ENTRAR NA TRANSIÇÃO — no Tailwind 4 ele não é `transform`.
   A marcação pede `transition-[transform,filter]` e `group-hover:scale-[1.05]`,
   e isso funcionava no protótipo porque o Tailwind 3 compilava a escala DENTRO
   do transform (`--tw-scale-x` + `transform:…scaleX()`). O Tailwind 4 compila-a
   para a propriedade autónoma:

       group-hover:scale-[1.05] { … scale: 1.05 }

   `scale` não é `transform`, portanto não estava na lista de propriedades a
   transitar: a dessaturação fazia o seu percurso e o zoom SALTAVA. Metade da
   animação não acontecia — e é por isso que passar de 500 para 700ms não se
   notou. Não era a duração, era o que ela não cobria. */
img.grayscale{ transition-property: transform, filter, scale; }

/* ── cronologia: o numeral dos anos acompanha o scroll ────────────
   O "39 / anos de prática ininterrupta" vive na coluna da direita, debaixo de
   "Décadas de prática contínua.", ao lado da lista de marcos. A lista é alta; o
   numeral é baixo. Sem isto ele passa uma vez e desaparece enquanto ainda se
   está a ler o percurso que ele resume.

   ⚠ align-self:start É OBRIGATÓRIO, não é arrumação. Um item de grelha estica-se
   à altura da linha por omissão, e um elemento com a altura toda da sua área não
   tem para onde deslizar — o `position:sticky` fica lá, sem efeito e sem erro.
   Com start, a caixa passa a ter a altura do conteúdo e ganha o curso.

   ⚠ SÓ A PARTIR DE lg. Abaixo disso as duas colunas empilham, o numeral fica
   por cima dos marcos, e fixá-lo significaria tapar o que se está a ler.

   O top deixa passar o cabeçalho fixo com folga.                            */
@media (min-width: 1024px){
  .madv-years-slot{ position: sticky; top: 7rem; align-self: start; }
}

/* ── arquivo de publicações: o que ainda não foi revelado ─────────
   ⚠ CLASSE E NÃO `hidden`. O filtro (post-filter.js) é dono do atributo
   `hidden` e reescreve-o em cada passagem; se o "Carregar mais" usasse o
   mesmo mecanismo, o último a correr apagava o trabalho do outro. Duas
   razões diferentes para um cartão não aparecer, dois interruptores.

   Posta pelo assets/js/post-more.js — sem JavaScript não existe, e a lista
   sai inteira. */
[data-post-list] > .is-beyond{ display: none; }

/* ── cronologia: o numeral acompanha a leitura dos marcos ─────────
   O bloco dos anos vive na coluna direita, por baixo de "Décadas de prática
   contínua."; os marcos ocupam a coluna esquerda e são muito mais altos. Sem
   isto, o numeral saía do ecrã ao terceiro marco e o resto da lista lia-se sem
   ele — que é precisamente o número que lhe dá sentido.

   Fixo enquanto a coluna dos marcos passa, e volta a andar quando ela acaba.

   ⚠ O `align-self:start` NÃO É COSMÉTICO: sem ele o item da grelha estica-se
   até à altura da linha, fica do tamanho do que devia acompanhar, e não sobra
   percurso nenhum para colar. O sticky não dá erro — simplesmente não acontece,
   que é o modo de falhar mais caro de diagnosticar.

   ⚠ Só a partir de lg. Abaixo disso as colunas empilham, o numeral fica no
   fluxo normal a seguir aos marcos, e colá-lo tapava-os.

   O `top` tem de livrar o cabeçalho fixo (~84px) com folga.

   ⚠ PRECISA DE UMA ALTERAÇÃO NO OXYGEN PARA FAZER ALGUMA COISA, e é por isso
   que esta nota é longa. Medida a grelha, a cronologia da página inicial está
   assim:

       linha 1 | marcos (col 1-8, 751px) | "Décadas de prática contínua." (col 9-12)
       linha 2 | "O percurso que começou…" | o bloco dos anos (col 9-12, 225px)

   O numeral não está AO LADO dos marcos, está numa SEGUNDA LINHA por baixo
   deles. O contentor dele tem exactamente a altura dele, portanto não há
   percurso para colar: um `position:sticky` ali não dá erro e não faz nada.

   Para funcionar, o bloco dos anos tem de passar para dentro da coluna do
   título — a seguir ao h2, na linha 1 — e essa coluna leva a classe
   `madv-cronologia-lado`. Aí sim: fica por baixo de "Décadas de prática
   contínua." e acompanha a leitura dos 751px de marcos.

   ⚠ ISTO REARRUMA A GRELHA A PARTIR DO CSS, o que normalmente não se faz. É
   deliberado: a alternativa é arrastar o elemento no Oxygen para dentro da
   coluna do título, e enquanto isso não acontecer o numeral fica onde está.
   Feita essa mudança no builder, quase tudo aqui deixa de ser preciso —
   sobrevive só a regra do sticky.

   Como funciona: os dois invólucros da linha 2 são dissolvidos com
   `display:contents`, e os filhos passam a ser items da grelha exterior. A
   partir daí a colocação é explícita — os marcos passam a ocupar as DUAS linhas
   da esquerda, o numeral vai para a linha 2 da coluna da direita (por baixo do
   título), e a frase de fecho cai sozinha para a linha seguinte.

   ⚠ Os dois ganchos são classes NOSSAS — .madv-years-slot e .madv-timeline-slot.
   Só a coluna do título é alcançada por estrutura, e mesmo essa não precisa de
   ser tocada: fica onde já estava.                                           */
@media (min-width:1024px){
  /* dissolver a sub-grelha e o invólucro da linha 2 */
  div:has(> .madv-years-slot),
  div:has(> div > .madv-years-slot){ display: contents; }

  /* ⚠ AS LINHAS TÊM DE SER DIMENSIONADAS À MÃO. Sem isto o browser reparte os
     751px dos marcos pelas duas linhas em partes iguais, a linha 1 fica muito
     mais alta do que o título precisa, e o numeral nasce 250px abaixo dele —
     com uns magros 140px de percurso para colar. `max-content 1fr` dá à linha 1
     exactamente o que o título ocupa e entrega o resto à de baixo. */
  div:has(> .madv-timeline-slot){ grid-template-rows: max-content 1fr; }

  /* os marcos passam a cobrir as duas linhas — é o percurso que o numeral acompanha */
  .madv-timeline-slot{ grid-column: 1 / 9; grid-row: 1 / span 2; }

  /* o numeral, por baixo do título, e colado enquanto os marcos passam */
  .madv-years-slot{
    grid-column: 9 / 13;
    grid-row: 2;
    position: sticky;
    top: 6.5rem;
    align-self: start;
  }
}

/* ── menu backdrop: subtle blur behind the panel ──────────────────
   Driven by .is-on (not inline opacity) so the blur and the dim fade
   together. -webkit- prefix for Safari.

   ⚠ O POSICIONAMENTO E O ESCURECIMENTO ESTÃO AQUI, NÃO EM CLASSES.
   No protótipo a div traz `fixed inset-0 z-40 bg-ink/45 opacity-0
   transition-opacity duration-500 md:bg-ink/35`, e este bloco só acrescentava o
   desfoque. Em Oxygen a div saiu sem classe nenhuma: ficou transparente, sem
   dimensão e fora do fluxo de empilhamento — o menu abria sem véu, e o CSS do
   desfoque estava cá todo, à espera de um elemento que não tinha área para
   desfocar.

   Duas razões para ficar em CSS em vez de repor as classes:
     · `bg-ink/45` e `md:bg-ink/35` NÃO estão compiladas no output.css, porque
       nada no site as usa desde que a div as perdeu. Reporá-las obrigava a abrir
       o Winden e compilar — o CSS não depende de ninguém se lembrar disso.
     · Uma classe posta à mão num construtor volta a desaparecer numa edição
       distraída, e o sintoma é este: um véu que não aparece e nada a explicá-lo.

   ⚠ O ESCURECIMENTO É O --ink A 45%. Escrito em rgb() e não com o token porque
   precisa de alfa; se o --ink mudar, esta linha tem de mudar com ele.          */
#menu-backdrop{
  position: fixed; inset: 0; z-index: 40;
  background: rgb(17 17 16 / .45);
  opacity: 0;
  -webkit-backdrop-filter: blur(0px);
          backdrop-filter: blur(0px);
  transition: opacity .5s ease,
              -webkit-backdrop-filter .5s ease,
                      backdrop-filter .5s ease;
}
/* Mais leve no desktop, onde o painel não cobre o ecrã todo — era o md:bg-ink/35. */
@media (min-width:768px){
  #menu-backdrop{ background: rgb(17 17 16 / .35); }
}
#menu-backdrop.is-on{
  opacity: 1;
  -webkit-backdrop-filter: blur(5px);
          backdrop-filter: blur(5px);
}
@media (prefers-reduced-motion: reduce){
  #menu-backdrop{ transition-duration: .01ms !important; }
}

/* ── transparent header, desktop only, dark-hero pages ─────────────
   Below md the header stays solid and in flow (legibility on small
   screens matters more than the effect).                            */
@media (min-width: 768px){
    /* ⚠ `body.home .site-header` É UM ALIAS PARA O WORDPRESS, e não afecta este
     site. O build.py carimba data-header="overlay" por página; em Oxygen o
     template do header é UM só para todas, e não há forma de lhe pôr o atributo
     condicionalmente. A classe `home` que o WordPress já põe no <body> é o
     equivalente. Fica como selector alternativo em vez de um bloco duplicado, para
     as declarações continuarem a ter uma única origem. O <body> deste site não
     tem classe `home`, por isso o alias nunca casa aqui. */
  .site-header[data-header="overlay"],
  body.home .site-header{
    position:absolute; top:0; left:0; right:0;
    background:transparent;
    border-bottom-color:transparent;
  }
  /* lift the contents onto the dark field */
  .site-header[data-header="overlay"] a[aria-label],
  body.home .site-header a[aria-label]{ color:#FAFAFA; }
  .site-header[data-header="overlay"] #menu-toggle,
  body.home .site-header #menu-toggle{ color:#FAFAFA; }
  .site-header[data-header="overlay"] .lang-btn,
  body.home .site-header .lang-btn{ color:#FAFAFA; }
  .site-header[data-header="overlay"] .lang-menu,
  body.home .site-header .lang-menu{ background:#FAFAFA; }
  .site-header[data-header="overlay"] #menu-toggle .mt-bar,
  body.home .site-header #menu-toggle .mt-bar{ background:#FAFAFA; }
  /* the hero already bottom-aligns its content; just guarantee clearance */
  [data-header="overlay"] ~ main .hero-field{ top:0; }
}

/* ── language switcher ─────────────────────────────────────────────
   Dummy for now. Globe is inline SVG on currentColor so it inherits
   the header variant (light over the hero, ink elsewhere).           */
.lang-btn{ color:inherit; }
.lang-globe{ overflow:visible; }
.lang-globe circle,
.lang-globe path{ opacity:.75; }

/* Subtle "rotation": the meridian's width eases in and out, which reads
   as the globe turning without actually rotating anything. */
.lang-meridian{
  transform-origin: 10px 10px;
  transition: transform 1.15s cubic-bezier(.42,0,.32,1), opacity .5s ease;
  opacity:.85;
}
/* meridian sweeps through edge-on and back out the other side: reads as
   the globe turning, without rotating (and without a repeating loop). */
/* scaleX(-1) was a no-op: mirroring a symmetric ellipse looks identical.
   Hold a turned position instead, so rest and hover clearly differ. */
.lang:hover .lang-meridian,
.lang:focus-within .lang-meridian{ transform: scaleX(.22); opacity:1; }
.lang-globe{ transition: transform 1.15s cubic-bezier(.42,0,.32,1); }
.lang:hover .lang-globe,
.lang:focus-within .lang-globe{ transform: rotate(-12deg); }

.lang-current{ opacity:.85; transition:opacity .3s ease; }
.lang:hover .lang-current{ opacity:1; }

/* dropdown: closed by default once JS is on; plain list without JS */
.lang-menu{ display:none; z-index:60; }
.js-on .lang-menu{ display:none; }
.js-on .lang.is-open .lang-menu{ display:block; }
html:not(.js-on) .lang-menu{
  display:flex; position:static; border:0; background:none; margin:0; padding:0;
}
html:not(.js-on) .lang-menu a{ padding:.5rem .6rem; }

@media (prefers-reduced-motion: reduce){
  .lang-meridian{ transition-duration:.01ms !important; }
}

/* ── scroll header ─────────────────────────────────────────────────
   One header, three states:
     natural  — as authored (overlay on the homepage, in-flow elsewhere)
     is-fixed — pinned, solid, ink contents (it now sits over content)
     is-hidden— pinned but translated out (scrolling down)
   No duplicate markup, so there is still exactly one #menu-toggle.   */
.js-on .site-header{
  transition: transform .45s cubic-bezier(.22,.9,.26,1),
              background-color .3s ease, border-color .3s ease;
  will-change: transform;
}
.js-on .site-header.is-fixed{
  position:fixed; top:0; left:0; right:0; z-index:45;
  background:#FAFAFA; border-bottom-color:#DDDAD5;
  /* soft drop so the pinned bar reads as detached over paper sections
     (Continuidade, Publicações), where the hairline border alone is faint. */
  box-shadow:0 6px 22px -16px rgba(17,17,16,.28);
}
.js-on .site-header.is-fixed.is-hidden{ transform:translateY(-100%); }

/* pinned bar is always solid, so overlay pages must drop back to ink */
.js-on .site-header.is-fixed[data-header="overlay"] #menu-toggle,
.js-on .site-header.is-fixed[data-header="overlay"] .lang-btn,
.js-on body.home .site-header.is-fixed #menu-toggle,
.js-on body.home .site-header.is-fixed .lang-btn{ color:#111110; }

/* ⚠ O LOGO NÃO ACOMPANHA — VAI A VINHO, como em todas as outras páginas.
   Estava na lista acima e saía preto: a barra fixa da página inicial ficava a
   única do site com o logo a #111110. Medido numa interior: logo #6A1312,
   menu e línguas #111110 — é essa a combinação que isto repõe.

   #6A1312 é o --maroon do sistema. Um #691615 tirado de uma captura é quase o
   mesmo e seria um SEGUNDO vinho: a barra fixa deixava de ser idêntica às
   outras páginas, que é exactamente o que se quer aqui. */
.js-on .site-header.is-fixed[data-header="overlay"] a[aria-label],
.js-on body.home .site-header.is-fixed a[aria-label]{ color:#6A1312; }
.js-on .site-header.is-fixed[data-header="overlay"] #menu-toggle .mt-bar,
.js-on body.home .site-header.is-fixed #menu-toggle .mt-bar{ background:#111110; }

@media (prefers-reduced-motion: reduce){
  .js-on .site-header{ transition-duration:.01ms !important; }
}

/* ══ PRACTICE-AREA ROUTER ══════════════════════════════════════════
   Lives in the hero, over the dark field. Without JS the input is
   simply absent from the flow's usefulness — the two links below it
   still reach the areas index and contact, so nothing dead-ends.   */
.router-field{
  display:flex; align-items:center; gap:.75rem;
  background:rgba(242,241,241,.10);
  border:1px solid rgba(242,241,241,.28);
  border-radius:var(--r-sm);
  padding:.95rem 1.15rem;
  backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
  transition:border-color .3s ease, background-color .3s ease;
}
.router-field:focus-within{ border-color:rgba(242,241,241,.75); background:rgba(242,241,241,.16); }
.router-icon{ color:rgba(242,241,241,.55); flex:none; }
.router-field input{
  flex:1 1 auto; min-width:0; background:none; border:0; outline:0;
  color:#FAFAFA; font-size:15px; letter-spacing:.01em;
  /* A CAIXA DO INPUT É FIXADA AQUI, DE PROPÓSITO, e não deixada à herança.
     Sem estas três linhas a altura vem do reset do Tailwind, e esse reset MUDA
     entre versões: o v3 põe `line-height:inherit` nos controlos, o v4 põe
     `font:inherit`. Basta uma folha sem camada, ou um plugin de formulários, para
     o valor herdado passar a `normal` — e aí a altura passa a depender das
     métricas da fonte, ou seja muda quando a webfont troca. Com Instrument Sans a
     15px, `normal` dá 18,3px e `1.5` dá 22,5px: 4px que aparecem sozinhos a meio
     do carregamento. Explícito, a caixa é a mesma antes e depois da troca, e a
     mesma no estático e no WordPress.
     ⚠ O padding também é explícito: o reset do v4 zera-o pelo selector universal,
     não na regra dos controlos, e o @tailwindcss/forms repunha-o a .5rem .75rem. */
  line-height:1.5; padding:0;
  box-shadow:none;
}
/* O anel de foco do @tailwindcss/forms é um BOX-SHADOW, não um outline. O
   `outline:0` acima nunca lhe tocou — daí o anel azul ter sobrevivido a um reset
   que parecia cobri-lo. O foco visível desta caixa é a borda do .router-field a
   acender (:focus-within, logo abaixo). */
.router-field input:focus{ outline:0; box-shadow:none; }
.router-field input::placeholder{ color:rgba(242,241,241,.45); }

[data-router-results]{
  position:absolute; left:0; right:0; top:calc(100% + .5rem); z-index:30;
  background:#FAFAFA; border:1px solid #DDDAD5;
  border-radius:var(--r-md);
  /* overflow:hidden clips the first/last item's hover fill to the rounded
     corners. Results are capped at 5 short rows, so this never needs to
     scroll — the max-height is only a guard. */
  max-height:22rem; overflow:hidden;
}
[data-router-results] li + li{ border-top:1px solid #DDDAD5; }
[data-router-results] a{
  display:flex; align-items:baseline; justify-content:space-between; gap:1rem;
  padding:.85rem 1.15rem; color:#111110; transition:background-color .2s ease, color .2s ease;
}
[data-router-results] a:hover,
[data-router-results] a[aria-selected="true"]{ background:#111110; color:#FAFAFA; }
.router-area{ font-size:15px; line-height:1.3; }
.router-lawyer{ font-size:11px; letter-spacing:.14em; text-transform:uppercase; opacity:.55; white-space:nowrap; }
.router-empty{ padding:1rem 1.15rem; color:#6B6763; font-size:14px; line-height:1.6; }
.router-empty strong{ color:#111110; }
.router-empty-cta{ margin-top:.4rem; }
.router-empty a{ color:#6A1312; text-decoration:underline; text-underline-offset:3px; }

/* no JS -> hide the input, keep the links */
html:not(.js-on) .router-field{ display:none; }

/* ── router, light variant (variant B: input sits on paper) ────────── */
.router--light .router-field{
  background:rgba(17,17,16,.03);
  border-color:#DDDAD5;
  backdrop-filter:none; -webkit-backdrop-filter:none;
}
.router--light .router-field:focus-within{ border-color:#111110; background:rgba(17,17,16,.05); }
.router--light .router-icon{ color:#6B6763; }
.router--light .router-field input{ color:#111110; }
.router--light .router-field input::placeholder{ color:#9A948E; }

/* ══ CORPO DE UM DOCUMENTO ════════════════════════════════════════════
   Serve os artigos e as páginas legais — tudo o que é lido do princípio ao fim
   em vez de percorrido. O protótipo escreve estas classes elemento a elemento
   (md_to_html no build.py). Em WordPress o texto vem do editor SEM classe
   nenhuma — é HTML semântico limpo, h2/h3/p/ul/li — por isso o estilo tem de
   vir por descendência.

   ⚠ OS VALORES TÊM DE BATER CERTO COM md_to_html(). São o mesmo artigo nos dois
   sites; se um deles mudar de escala, mudam os dois. Um filtro em PHP a injectar
   classes no HTML guardado era a alternativa, e é regex sobre marcação: parte-se
   no dia em que o editor colar um <p> com um atributo.
   ══════════════════════════════════════════════════════════════════ */
.doc-prose > p{ margin-top:1.5rem; font-size:17px; line-height:1.75; color:var(--muted); }
.doc-prose > h2{
  margin-top:3.5rem; font-size:clamp(1.4rem,2.4vw,1.85rem); font-weight:400;
  line-height:1.15; letter-spacing:-0.02em; color:var(--ink);
}
.doc-prose > h3{ margin-top:2.5rem; font-size:19px; line-height:1.375; color:var(--ink); }
/* Listas: marcadores normais, sem filetes.

   As listas eram desenhadas como linhas de tabela — filete por cima do <ul> e
   por baixo de cada <li>, sem marcador nenhum. Passam a ser o que são: pontos
   nas <ul>, números nas <ol>, texto indentado a partir do marcador.

   ⚠ O list-style TEM DE SER REAFIRMADO. O preflight do Tailwind zera-o
   (`ol, ul { list-style:none; margin:0; padding:0 }`), e sem estas linhas os
   marcadores não aparecem por muito que o HTML seja uma lista a sério. É o
   mesmo motivo do padding-left: sem ele o marcador fica fora da caixa e é
   cortado.

   ⚠ DESCENDENTE E NÃO FILHO, ao contrário das regras acima. Uma lista aninhada
   não é filha do .doc-prose, e com `>` ficava sem marcador — que é exactamente
   o defeito que isto vem corrigir, só que um nível mais abaixo.

   A cor mantém-se em --muted, a mesma dos parágrafos à volta. */
.doc-prose ul,
.doc-prose ol{ margin-top:1.5rem; padding-left:1.5rem; }
.doc-prose ul{ list-style:disc; }
.doc-prose ol{ list-style:decimal; }
.doc-prose ul ul{ list-style:circle; }
.doc-prose ol ol{ list-style:lower-alpha; }
.doc-prose :is(ul,ol) :is(ul,ol){ margin-top:.5rem; }

/* Os números mais pequenos do que o texto que numeram.
   ⚠ NO ::marker E NÃO NO <li>. Encolher o li encolhia a frase toda — o marcador
   herda o tamanho de quem o contém. O ::marker aceita as propriedades de fonte
   precisamente para isto, e o número continua a assentar na mesma linha de base
   do texto. Herda a cor; não é aqui que se lhe mexe. */
.doc-prose ol > li::marker{ font-size:13px; }
.doc-prose li{ margin-top:.5rem; font-size:16px; line-height:1.65; color:var(--muted); }
.doc-prose li:first-child{ margin-top:0; }
.doc-prose > *:first-child{ margin-top:0; }

/* ══ SCROLL CUE ════════════════════════════════════════════════════
   A hairline with a single drop travelling down it — same vocabulary
   as the rest of the rules on the page, no bouncing chevron.
   Revealed by JS a few seconds after load (class .cue-on). NOTE the
   class is deliberately NOT .is-in: the scroll-reveal system declares
   `.js-on .is-in { opacity:1 !important }`, which silently overrode the
   fade-out here.  So it never
   competes with the hero's own entrance. Hidden once the user scrolls.
   ══════════════════════════════════════════════════════════════════ */
.scroll-cue{
  opacity:0; transform:translateY(-4px);
  transition:opacity 1.1s var(--ease-out), transform 1.1s var(--ease-out);
}
.scroll-cue.cue-on{ opacity:1; transform:none; }
.scroll-cue.cue-off{ opacity:0; transform:translateY(4px); transition-duration:.5s; }

.scroll-cue__track{
  display:block; position:relative; width:1px; height:56px;
  background:linear-gradient(180deg, rgba(17,17,16,.14) 0%, rgba(17,17,16,.04) 100%);
  overflow:hidden;
}
.scroll-cue__drop{
  position:absolute; left:0; top:0; width:1px; height:16px;
  background:linear-gradient(180deg, transparent 0%, #6A1312 55%, transparent 100%);
}
@media (prefers-reduced-motion: no-preference){
  .scroll-cue.cue-on .scroll-cue__drop{ animation:scrollDrop 2.6s cubic-bezier(.55,0,.45,1) infinite; }
}
@keyframes scrollDrop{
  0%   { transform:translateY(-16px); opacity:0 }
  18%  { opacity:1 }
  72%  { opacity:1 }
  100% { transform:translateY(56px); opacity:0 }
}

/* ── quick-contact lightbox ──────────────────────────────────────────
   Centred dialog opened by [data-contact-open]. Base state is fully
   visible; JS toggles .is-on to run the fade/scale. The `hidden`
   attribute (removed by JS on open) keeps it out of the no-JS render. */
body.contact-open{ overflow:hidden; }

/* The `flex` utility on #contact-modal is author CSS and would otherwise beat
   the UA stylesheet's [hidden]{display:none}, leaving a full-screen overlay
   that swallows every click. Re-assert hidden from the author layer. */
#contact-modal[hidden], #contact-backdrop[hidden]{ display:none !important; }

#contact-backdrop{ opacity:0; transition:opacity .4s ease; }
#contact-backdrop.is-on{ opacity:1; }

#contact-modal .contact-card{
  opacity:0; transform:translateY(12px) scale(.985);
  transition:opacity .34s ease, transform .34s var(--ease-out);
}
#contact-modal.is-on .contact-card{ opacity:1; transform:none; }
/* heading is focused only to move screen-reader attention on success; it is
   not interactive, so the visible box outline is noise. */
#contact-modal [data-contact-done] h3:focus{ outline:none; }

/* form controls: the reset strips radius, so restore it here */
.cf-input{
  width:100%; border:1px solid var(--rule); border-radius:var(--r-sm);
  background:#fff; padding:.7rem .9rem;
  font-size:15px; line-height:1.5; color:#111110;
  outline:none; transition:border-color .18s ease, box-shadow .18s ease;
}
.cf-input::placeholder{ color:#6B6763; opacity:.7; }
.cf-input:focus{ border-color:#111110; box-shadow:0 0 0 3px rgba(17,17,16,.06); }

@media (prefers-reduced-motion: reduce){
  #contact-backdrop, #contact-modal .contact-card{ transition:none; }
}

/* ── equipa: card grid + row drawer ──────────────────────────────────
   The panel is a sibling of the card and spans the full grid width, so on a
   3-column grid it opens as a drawer UNDER THE ROW, and on a 1-column grid it
   opens directly under its own card (an accordion). Same markup, same JS.

   NO-JS / NO-CSS-JS CONTRACT: panels are visible here by default. site.js is
   what collapses them, and the toggle button is hidden until JS exists — so
   with scripting off the page is a complete, readable directory.            */
/* ROW-GAP MUST STAY 0. The panel is a grid item, so the moment it stops being
   display:none the grid inserts a row — and a row-gap would appear at full size
   instantly, jumping the page ~40px before the height animation has run a single
   frame. Row spacing therefore lives on the cards, where it is constant whether
   a panel is open or not; the panel's own spacing is padding INSIDE the animated
   height. */
.team-grid{ display:grid; grid-template-columns:1fr; gap:0 1.75rem; }
.team-card{ margin-bottom:2.5rem; }
@media (min-width:640px){ .team-grid{ grid-template-columns:repeat(2,1fr); } }
@media (min-width:1024px){ .team-grid{ grid-template-columns:repeat(3,1fr); } }

.team-panel{ grid-column:1 / -1; }
.team-card{ display:flex; flex-direction:column; align-items:flex-start; }
.team-panel-in{ padding-bottom:2.5rem; }

/* button only means something once JS can toggle */
[data-team-toggle]{ display:none; }
.js-on [data-team-toggle]{ display:inline-flex; }

.js-on .team-panel{ overflow:hidden; }
.js-on .team-panel[hidden]{ display:none; }
.js-on .team-chev{ transition:transform .3s var(--ease-out); }
.js-on [aria-expanded="true"] .team-chev{ transform:rotate(180deg); }

/* GridZoom-derived, translated into this design's register: opening a profile
   pushes the other cards back (dim + settle) so attention lands on one person,
   and that person's portrait scales up into the detail panel. No full-screen
   photo takeover — this is a law firm, the grid stays the frame. */
@media (prefers-reduced-motion: no-preference){
  .js-on .team-card{ transition:opacity .45s var(--ease-out), transform .45s var(--ease-out); }
}
/* !important + higher specificity is REQUIRED here, not laziness: the reveal
   engine's `.js-on .is-in { opacity:1 !important }` lands on every card once it
   has scrolled into view, so without this the recede silently only worked on
   cards still below the fold. */
.js-on [data-team-grid].has-open .team-card:not(.is-active){
  opacity:.34 !important; transform:scale(.985) !important; }
.js-on [data-team-grid].has-open .team-card.is-active{ opacity:1; transform:none; }
.js-on .team-card.is-active .team-card-photo{ box-shadow:0 18px 44px -28px rgba(17,17,16,.5); }
/* An open profile HOLDS the hover treatment: the pointer has usually moved down
   into the panel by then, and letting the portrait fall back to grey made the
   open card look deselected while its profile was on screen. Matches the
   homepage carousel values exactly (colour + 1.05). */
.js-on .team-card.is-active img{ filter:none; transform:scale(1.05); }
/* the whole card is the hit area, so it should read as one */
.js-on .team-card{ cursor:pointer; }
.js-on .team-card:hover .team-card-photo{ box-shadow:0 14px 34px -26px rgba(17,17,16,.42); }
.js-on .team-card:hover [data-team-toggle]{ color:var(--maroon); }

@media (prefers-reduced-motion: no-preference){
  .js-on .team-panel{ transition:height .46s var(--ease-out); }
  .js-on .team-panel-in{ opacity:0; transition:opacity .38s var(--ease-out) .06s; }
  .js-on .team-panel.is-open .team-panel-in{ opacity:1; }
  /* the zoom: the detail portrait grows from roughly the card's scale */
  .js-on .team-detail-photo{ transform:scale(.94); transform-origin:left top;
                             transition:transform .55s var(--ease-out) .04s; }
  .js-on .team-panel.is-open .team-detail-photo{ transform:none; }
  /* detail blocks settle in after the panel has opened */
  .js-on .team-panel-in > div > div > div{ opacity:0; transform:translateY(10px);
      transition:opacity .42s var(--ease-out), transform .42s var(--ease-out); }
  .js-on .team-panel.is-open .team-panel-in > div > div > div{ opacity:1; transform:none;
      transition-delay:.14s; }
}

/* Reduced motion: no recede at all. It has to match the dim rule's specificity
   AND its !important, or the escalation above silently wins and these users get
   a 34% dim with no transition to explain it. */
@media (prefers-reduced-motion: reduce){
  .js-on [data-team-grid].has-open .team-card:not(.is-active){
    opacity:1 !important; transform:none !important; }
}

/* ── tactile press feedback (physics-active-state) ───────────────────
   Buttons/CTAs get a subtle pressed scale. Two things to beat:
   1) Tailwind's .transition-colors and the reveal-stagger transition both
      set `transition` — the html.js-on prefix (higher specificity) wins so
      the transform gets a FAST .14s press curve, while opacity keeps the
      slower reveal timing.
   2) The generic is-in reset (`.is-in > * { transform:none !important }`)
      would block the pressed scale — the !important on :active with the
      html.js-on prefix out-specifies it.
   Press buttons that live inside a reveal fade in (no rise) so their
   transform stays reserved for the press. Reduced motion opts out. */
html.js-on .press, .press{
  transition: opacity var(--dur-child) var(--ease-out),
              transform .14s var(--ease-out),
              color .15s ease, background-color .15s ease, border-color .15s ease;
}
html.js-on .reveal-stagger > .press,
html.js-on .reveal > .press,
html.js-on .reveal-sm > .press{ transform: none; }
html.js-on .press:active, .press:active{ transform: scale(.97) !important; }
@media (prefers-reduced-motion: reduce){
  html.js-on .press:active, .press:active{ transform: none !important; }
}


}
/* ═══════════════════════════════════════════════════════════════════
   APÊNDICE — só WordPress/Oxygen, e FORA da camada de propósito.

   Estas regras existem para corrigir o que o builder faz ao markup, e
   têm de ganhar às utilities do Tailwind. Sem camada, ganham.
   ═══════════════════════════════════════════════════════════════════ */

/* O Oxygen embrulha a saída de QUALQUER shortcode num <div class="oxy-shortcode">
   ao qual o builder não deixa aplicar classes. Isso QUEBRA A CORRENTE DE ALTURAS:
   o que estiver lá dentro e se dimensione em percentagem resolve contra esta div,
   que é height:auto, e não contra o elemento que colocámos na tela.

   Deu erro duas vezes, em direcções opostas, com o mesmo SVG h-full:
     · no header, dentro de um link de altura fixa, o SVG calculou 0px e o
       logótipo desapareceu;
     · na cronologia, dentro de um slot com altura em clamp (35px), o SVG saiu a
       684×174px — sem altura a que se agarrar, encheu a largura disponível.

   ⚠ REGRA: qualquer contentor que imponha uma altura e receba um shortcode tem
   de constar aqui. Um slot novo que se esqueça disto não dá erro — dá um SVG do
   tamanho errado, para mais ou para menos. */
.site-header a > .oxy-shortcode,
.madv-wordmark-slot > .oxy-shortcode { height: 100%; }

/* A língua activa no selector.

   O protótipo trocava text-ink/text-muted dentro do próprio atributo class. Aqui
   o atributo tem de ficar literal: o crawler do Winden lê-o como texto e PHP lá
   dentro esconderia as classes da compilação (includes/winden-crawler.php). Fica
   dito uma vez, e por semântica em vez de por duplicação. */
.lang-menu a[aria-current="true"] { color: var(--ink); }

/* ══ PUBLICAÇÕES DESLIGADAS ═══════════════════════════════════════════
   A secção "Do escritório" da página inicial não é um shortcode nosso: é uma
   secção do Oxygen com título, lede e o link "Todas as publicações", e os
   nossos blocos só lá dentro. Com o interruptor desligado, os cartões saem e
   fica a moldura — uma faixa com cabeçalho, muito espaço vazio, e um link para
   uma página que responde 404.

   O <body> ganha .madv-sem-publicacoes (includes/publications-page.php). Falta
   dizer qual é a secção, e são dois selectores de propósito:

   1. #madv-publicacoes-home — o id a pôr NA SECÇÃO, no Oxygen. É o alvo.
   2. :has(.madv-pubmask-slot) — a rede. Essa classe está no contentor da
      máscara e só existe nesta secção, portanto isto continua a acertar mesmo
      que o id seja apagado no builder um dia.

   A segunda não é redundância por gosto: um id posto à mão num construtor é
   exactamente o género de coisa que desaparece numa edição distraída, e o que
   ficava era uma faixa vazia no site sem nada a explicar porquê.

   ⚠ NUNCA PELO ID QUE O OXYGEN GERA. Um `.oxy-container-58-222` parece o alvo
   óbvio e é uma armadilha: esses números são gerados e mudam quando o elemento é
   recriado. A regra deixava de acertar sem nada mudar de visível no builder.

   ⚠ E ISTO ESCONDE, NÃO REMOVE — a marcação continua a ser servida. Para uma
   faixa de montra chega; a solução durável é uma condição na secção, no Oxygen. */
body.madv-sem-publicacoes #madv-publicacoes-home,
body.madv-sem-publicacoes section:has(.madv-pubmask-slot) {
  display: none;
}
