/* Harmonia — the generative banner on the homepage hero.
   Every selector is hm- prefixed: this file is loaded site-wide, so it
   must not be able to touch anything but its own markup. */
:root{--hm-ink:#2e4245; --hm-ink-lt:#5d7478;}

  /* The fog palette: cool greys drifting green and blue, with ink for the
     drawing. Sampled off a still of an ink circle written into mist. */
  /* No frame, no border, no inner rule. The image simply is the field. */
  .hm-stage{
    /* Fills the slide, but stops where the fixed .portfolio-footer begins.
       The hero itself deliberately runs full height beneath that footer — the
       site's own controls compensate for it — so the correction belongs HERE,
       on the artwork only. Without it the medallion centres in a box whose
       bottom 90px is hidden, and reads about 45px low. */
    position:absolute; left:0; right:0; top:0; bottom:var(--footer-h);
    width:auto; height:auto;
    background:
      radial-gradient(1250px 800px at 50% 44%, #f2f6f4 0%, #dbe4e2 40%, #b9c7c7 100%),
      linear-gradient(158deg,#e7edea 0%,#c3d0cf 60%,#aebdbe 100%);
    overflow:hidden;

    /* ── The loader ────────────────────────────────────────────────────
       The banner is built in one synchronous pass and is not shown until
       it is complete: the stage holds at zero and harmonia.js adds
       .hm-ready on the frame after it has finished composing. Nobody ever
       sees a half-assembled picture, and there is no spinner - the thing
       simply is not there, and then it is.

       Only the artwork waits. The page's own boot cover (page-boot.js) is
       a separate, earlier gate; this one covers the case where the cover
       is absent or already gone.

       The animation line is a DEADMAN SWITCH, the same trick .page-boot
       uses in master.css: if harmonia.js never runs, never loads, or
       throws, the keyframe reveals the stage anyway a second in. A banner
       that is merely static is a small problem; a banner that is
       permanently invisible is a broken homepage. */
    opacity:0;
    transition:opacity .5s ease;
    animation:hm-failsafe 0s linear 1.4s forwards;
  }
  .hm-stage.hm-ready{opacity:1; animation:none}
  @keyframes hm-failsafe{to{opacity:1}}
  svg.hm-plate{position:absolute; inset:0; width:100%; height:100%; display:block}

  /* The field and the constellation are separate <svg> elements so each gets
     its OWN compositor layer. That is the whole reason the drawing is smooth:
     inside one shared svg, a single line drawing itself repaints every path on
     the plate, every frame - measured at 20fps against 80fps split out, with
     the identical animation. will-change is what actually promotes them; take
     it off and they fold back into the page's layer and the cost returns.
     They keep their layers permanently because they draw on every replay, so
     this is one of the few places a standing will-change is right.
     pointer-events stay off: the hover is measured against #hm-art alone. */
  svg.hm-layer{will-change:transform; pointer-events:none}

  /* ---- the ground never stops moving ---- */
  #hm-arcsA,#hm-arcsB,#hm-arcsC,#hm-spiral{transform-box:view-box}
  #hm-arcsA{transform-origin:250px 170px;  animation:hm-spin 210s linear infinite}
  #hm-arcsB{transform-origin:1360px 730px; animation:hm-spinR 265s linear infinite}
  #hm-arcsC{transform-origin:800px 450px;  animation:hm-spin 350s linear infinite}
  #hm-spiral{transform-origin:800px 450px; animation:hm-spinR 420s linear infinite}
  @keyframes hm-spin { to{transform:rotate(360deg)} }
  @keyframes hm-spinR{ to{transform:rotate(-360deg)} }

  /* ---- the medallion breathes; its bands drift against one another ----

     ROTATING an svg group is free. SCALING one is not, and the difference is
     brutal: measured 2026-08-07, the old `hm-breathe` on #hm-mandala - a 1.2%
     pulse over 19 seconds, barely visible - held the whole page at 62fps
     against 120fps with it off, permanently, the entire time the banner is on
     screen. Every band rotation together costs nothing by comparison.

     The reason is that a rotation can be composited by re-using the raster the
     medallion already has, while a scale changes the resolution it has to be
     drawn at, so all ninety-odd paths are re-rasterised every frame.

     So the breath moved inward. The well, the mark and the egg-and-dart ring
     around them breathe as one - a small area, measured completely free at
     120fps - and the outer bands hold still and turn. The amplitude is larger
     (2.8% against 1.2%) because it now acts over a radius of ~146 rather than
     400, which keeps the actual movement about the same size as before.

     Do not put a scale back on #hm-mandala, #hm-fit or the svg itself. Scaling
     the svg root was measured at SIX fps. */
  #hm-well, #hm-mark, #hm-bandMoulding{
    transform-box:view-box; transform-origin:800px 450px;
    animation:hm-breathe 19s ease-in-out infinite}
  #hm-bandArcade{transform-box:view-box; transform-origin:800px 450px; animation:hm-spinR 300s linear infinite}
  #hm-bandStars {transform-box:view-box; transform-origin:800px 450px; animation:hm-spin  240s linear infinite}
  #hm-bandKey   {transform-box:view-box; transform-origin:800px 450px; animation:hm-spinR 400s linear infinite}
  #hm-bandGirih {transform-box:view-box; transform-origin:800px 450px; animation:hm-spin  520s linear infinite}
  @keyframes hm-breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.028)}}

  .hm-parallax{transition:transform 1.6s cubic-bezier(.2,.7,.2,1)}

  /* ── Performance ─────────────────────────────────────────────────────
     The fog, the dot grid and the grain all live in HTML/CSS, NOT in the
     SVG. Animating anything inside an SVG <filter> makes the browser re-run
     that filter on the CPU every frame - a 78px blur at 1600x900 is what
     made this chunky. Blurring each blob individually and then animating
     only `transform` lets the compositor reuse the blurred layer, so the
     GPU just moves it. Same reason the rotating bands get `will-change`. */
  .hm-fog{position:absolute; inset:-18%; pointer-events:none; z-index:0}
  .hm-fog i{position:absolute; display:block; border-radius:50%;
         filter:blur(70px); will-change:transform}
  .hm-f1{left:12%;  top:18%; width:44%; height:46%; background:#ffffff; opacity:.62;
      animation:hm-drift1 74s ease-in-out infinite}
  .hm-f2{left:52%;  top:48%; width:50%; height:50%; background:#e6efec; opacity:.55;
      animation:hm-drift2 96s ease-in-out infinite}
  .hm-f3{left:38%;  top:2%;  width:38%; height:36%; background:#cfe0e2; opacity:.5;
      animation:hm-drift3 63s ease-in-out infinite}
  .hm-f4{left:2%;   top:62%; width:42%; height:40%; background:#b9ccc6; opacity:.45;
      animation:hm-drift2 88s ease-in-out infinite reverse}
  .hm-f5{left:70%;  top:0%;  width:36%; height:38%; background:#ffffff; opacity:.48;
      animation:hm-drift1 110s ease-in-out infinite reverse}
  .hm-f6{left:26%;  top:74%; width:48%; height:34%; background:#aebfc2; opacity:.38;
      animation:hm-drift3 81s ease-in-out infinite}
  @keyframes hm-drift1{0%,100%{transform:translate3d(0,0,0) scale(1)}
                    50%{transform:translate3d(90px,-46px,0) scale(1.16)}}
  @keyframes hm-drift2{0%,100%{transform:translate3d(0,0,0) scale(1.08)}
                    50%{transform:translate3d(-110px,38px,0) scale(.92)}}
  @keyframes hm-drift3{0%,100%{transform:translate3d(0,0,0) scale(.96)}
                    50%{transform:translate3d(58px,64px,0) scale(1.2)}}

  /* the dotted ground: a tiled CSS background sliding exactly one cell, so
     the loop is seamless and the whole thing is one composited layer */
  #hm-dotfield{
    position:absolute; inset:-40px; z-index:1; pointer-events:none; opacity:0;
    background-image:radial-gradient(circle, rgba(46,66,69,.5) 1.05px, transparent 1.2px);
    background-size:28px 28px;
    -webkit-mask-image:radial-gradient(ellipse 62% 64% at 50% 50%, #000 0%, rgba(0,0,0,.55) 56%, transparent 100%);
    mask-image:radial-gradient(ellipse 62% 64% at 50% 50%, #000 0%, rgba(0,0,0,.55) 56%, transparent 100%);
    will-change:transform; animation:hm-dotdrift 52s linear infinite;
  }
  @keyframes hm-dotdrift{to{transform:translate3d(28px,28px,0)}}

  .hm-grain{position:absolute; inset:0; z-index:5; pointer-events:none; opacity:.5;
    background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/><feColorMatrix type='saturate' values='0'/><feComponentTransfer><feFuncA type='linear' slope='.05'/></feComponentTransfer></filter><rect width='160' height='160' filter='url(%23n)'/></svg>")}

  /* ── Hover ───────────────────────────────────────────────────────────
     The ring under the cursor answers: it gains contrast and stops turning
     while you are on it. Only that ring, so moving outward walks you through
     the bands one at a time and the piece teaches you it is not a video. */
  #hm-mandala g{transition:filter .5s ease}
  .hm-lit{filter:contrast(1.75) drop-shadow(0 0 7px rgba(30,46,49,.30));
       animation-play-state:paused}
  #hm-stage{cursor:crosshair}

  /* The constellation is drawn CRISP, then softened once and left alone.
     Blurring it while it draws would re-run the filter every frame - the
     exact trap the fog taught us. After this it never animates again. */
  #hm-network{transition:filter 1.2s ease}
  #hm-network.hm-soft{filter:blur(2.4px)}

  /* let the compositor cache each turning band instead of re-rasterising it.
     #hm-mandala is deliberately NOT in this list any more: it no longer
     transforms at all, and a will-change on a static element only pins a
     layer it never uses. */
  #hm-bandArcade,#hm-bandStars,#hm-bandKey,#hm-bandGirih,#hm-arcsA,#hm-arcsB,#hm-arcsC,#hm-spiral{
    will-change:transform}

  /* The overture LIGHTENS rather than covers. No dark plate, no button box -
     the fog behind stays visible and simply brightens toward the middle, and
     the word is just the word, in ink. */
  .hm-overture{position:absolute; inset:0; z-index:8; display:flex; flex-direction:column;
    align-items:center; justify-content:center; gap:30px;
    background:
      radial-gradient(620px 380px at 50% 50%, rgba(255,255,255,.72) 0%, rgba(255,255,255,.34) 48%, rgba(255,255,255,0) 100%),
      radial-gradient(1500px 900px at 50% 50%, rgba(226,235,232,.55) 0%, rgba(210,222,222,.28) 60%, rgba(198,212,213,0) 100%);
    transition:opacity 1.8s ease, visibility 1.8s}
  .hm-overture.hm-gone{opacity:0; visibility:hidden}
  .hm-overture p{margin:0; font-size:11px; letter-spacing:.46em; text-transform:uppercase; color:rgba(46,66,69,.42)}
  .hm-begin{font:inherit; font-size:19px; letter-spacing:.54em; text-indent:.54em;
    text-transform:uppercase; color:rgba(30,44,47,.9);
    background:none; border:0; padding:6px 4px; cursor:pointer; transition:.6s ease}
  .hm-begin:hover{color:#0f1c1f; letter-spacing:.68em; text-indent:.68em}

  /* Legible controls. These were nearly invisible, which is no good on a
     homepage banner where people need to see there is something to press. */
  /* Top-right. Both bottom corners are already taken by the site's own hero
     controls - dots bottom-left, arrows bottom-right - so these would either
     collide or hide behind them. */
  .hm-tools{position:absolute; right:24px; top:20px; z-index:9; display:flex; gap:10px}
  /* Solid, not translucent, and no backdrop-filter: a frosted panel over a
     pale background is effectively invisible, and backdrop-filter drops out
     entirely in some renderers. These have to be obviously pressable. */
  /* Pills tinted out of the fog itself rather than white, so they belong to
     the picture instead of being stuck on top of it. The outline does the work
     of saying "pressable"; no bright fill is needed. No backdrop-filter -
     it drops out entirely in some renderers and the button vanishes. */
  .hm-tools button{
    font-family:inherit; font-size:11px; font-weight:500; letter-spacing:.16em;
    text-transform:uppercase; color:#3d5457;
    background:rgba(222,232,230,.38);
    border:1px solid rgba(61,84,87,.30);
    border-radius:999px; padding:10px 20px; cursor:pointer;
    transition:.32s ease;
    display:inline-flex; align-items:center; gap:9px;
  }
  .hm-tools button:hover{
    background:rgba(234,241,239,.62);
    border-color:rgba(61,84,87,.6);
    color:#1e3033;
  }
  .hm-tools button .hm-dot{width:7px; height:7px; border-radius:50%;
    background:#22383c; opacity:.28; transition:.28s}
  .hm-tools button.hm-on .hm-dot{opacity:1; background:#1f7d72}

  /* ── The copy ────────────────────────────────────────────────────────
     Reuses the site's own .hero-content / .eyebrow / h1 / p, so the type
     scale matches every other banner. Only the colour changes: this banner
     is pale, where the others are dark photographs. It sits in the left
     column and the medallion stays centred - the two do not fight. */
  .hm-copy{
    position:absolute; inset:0; z-index:4;
    display:flex; flex-direction:column; justify-content:center;
    pointer-events:none;                 /* the artwork keeps its hover */
    max-width:min(36%, 500px);
    padding:0 0 0 clamp(28px, 5vw, 84px);
  }

  /* The same block mirrored: right-hand column, right-aligned, reading
     right to left. Noto Kufi because Sora has no Arabic glyphs at all. */
  .hm-copy-ar{
    left:auto; right:0; text-align:right;
    align-items:flex-end;
    padding:0 clamp(28px, 5vw, 84px) 0 0;
    margin-left:auto;
    font-family:"Noto Kufi Arabic","Segoe UI",Tahoma,sans-serif;
  }
  .hm-copy-ar h1{
    max-width:none; letter-spacing:0; line-height:1.25;
    font-size:clamp(30px, 3.1vw, 52px);
  }
  /* Every line in this column must share one right edge. Shrink-to-fit flex
     items do NOT: a short heading hugs its own text and its edge drifts (it
     measured 26px short of the paragraphs). Give each child the full column
     width and let text-align do the aligning, so the edge is the column's,
     not the text's. */
  .hm-copy-ar .eyebrow,
  .hm-copy-ar h1,
  .hm-copy-ar p{
    width:100%; align-self:stretch; text-align:right;
  }
  .hm-copy-ar p{ line-height:1.95; max-width:none }
  .hm-copy-ar .eyebrow{ letter-spacing:0 }
  .hm-copy .eyebrow{color:#4a6a6e; opacity:.95}
  .hm-copy h1{
    color:#1b2b2e; letter-spacing:-1.5px;
    font-size:clamp(34px, 3.6vw, 62px); max-width:11ch;
    text-shadow:0 1px 0 rgba(255,255,255,.5);
  }
  .hm-copy p{
    color:#3f5659; max-width:52ch;
    font-size:clamp(13px, .95vw, 15.5px); line-height:1.62; margin:18px 0 0;
  }
  .hm-copy p em{font-style:italic; color:#22383c}

  /* Below this the text would collide with the medallion, so drop the body
     copy and keep the title. */
  @media (max-width:1400px){
    .hm-copy p{display:none}          /* titles only — the body would collide */
  }
  @media (max-width:900px){
    .hm-copy-ar{display:none}         /* one language at a time on narrow screens */
    .hm-copy{max-width:min(70%, 520px)}
  }
  @media (max-width:760px){
    .hm-copy{max-width:100%; padding:0 24px; align-items:center; text-align:center}
    .hm-copy h1{max-width:none; font-size:clamp(26px,7vw,40px)}
  }

  /* ══════════ PORTRAIT: the art on top, the words underneath ══════════
     The composition is 16:9 and the plate is drawn with preserveAspectRatio
     "slice", so on a tall window it scales up hard and you see a narrow
     vertical band through the middle of it - the medallion crowded, the two
     text columns squeezed against its edges, and a great deal of empty field
     above and below. It reads badly on a monitor turned on its side, which is
     how Asghar runs his.

     So in portrait the stage stops being one layer with text laid over it and
     becomes two stacked registers: the artwork keeps the upper seven tenths
     and the words sit in the bottom three, on the same wash. Nothing overlaps,
     and the medallion gets a box much nearer its own proportions.

     The artwork needs no other help: fitMandala() measures the svg's real box
     and scales the medallion into it, so confining the svg here is enough for
     the medallion to resize itself to suit. It recomputes on resize, which is
     also when this media query flips.

     Only the three svgs and the dot grid are confined. The fog and the grain
     stay full-bleed on purpose - they are atmosphere, and they are what makes
     the two registers read as one picture rather than a panel with a caption
     bolted underneath. */
  @media (orientation: portrait){
    /* ONLY THE MEDALLION IS CONFINED. The field and the constellation stay
       full-bleed and run the whole height of the stage, exactly as they do in
       landscape.

       Confining all three was wrong and it showed: the bright halo wash is a
       rect inside the field svg, so clipping that svg cut the background off
       with a hard horizontal edge partway down the banner, and the picture
       stopped rather than continuing behind the words. The atmosphere - halo,
       arcs, spiral, constellation, dot grid, fog - is the thing that ties the
       two registers together, so it has to flow past the join. Only the
       medallion needs to get out of the way of the text.

       Set the HEIGHT, not `bottom`. Two traps, both hit on the way here:
       .hm-plate already sets height:100%, and a box with top, height and
       bottom all specified is over-constrained - `bottom` is thrown away and
       the artwork runs on under the words as if nothing had changed. But
       `height:auto` is worse: an <svg> is a replaced element, so auto means
       its INTRINSIC size - 16:9 off the viewBox - and the medallion collapses
       to a third of the frame and rides up to the top. Stating the height
       outright is the only form that behaves. */
    .hm-stage svg#hm-art{ height:65%; bottom:auto }

    .hm-copy{
      /* The two languages MEET IN THE MIDDLE rather than being flung out to
         opposite edges of the screen: each column is anchored to the centre
         line and grows outward, so they read as one bilingual block with a
         rule between them instead of two unrelated corners. */
      top:auto; bottom:0; height:35%;
      left:auto; right:50%;
      width:min(46%, 540px); max-width:none;
      /* flex-start, NOT center: the English and Arabic columns are different
         lengths, and centring each in the band independently would leave their
         eyebrows and titles at different heights. Anchored at the top they
         share one line, which is the whole point of the mirrored layout. */
      justify-content:flex-start;
      padding:clamp(14px,1.8vh,28px) clamp(22px,3vw,44px) 0 0;
    }
    .hm-copy-ar{
      left:50%; right:auto; margin-left:0;
      padding:clamp(14px,1.8vh,28px) 0 0 clamp(22px,3vw,44px);
    }
    /* The rule itself. Faded at both ends rather than a hard stroke - it is a
       fold in the page, not a border, and a full-strength line here would be
       the heaviest mark on a banner made entirely of hairlines. */
    .hm-copy-ar::before{
      content:""; position:absolute; left:0; top:2%; bottom:10%; width:1px;
      background:linear-gradient(to bottom,
        rgba(46,66,69,0), rgba(46,66,69,.28) 12%,
        rgba(46,66,69,.28) 84%, rgba(46,66,69,0));
    }
    /* the column IS the measure now, so stop constraining inside it */
    .hm-copy h1, .hm-copy p{max-width:none}
    /* There is real room down here, unlike alongside the medallion, so the
       BODY COPY COMES BACK IN FULL - both paragraphs. It is hidden below
       1400px in landscape only because it would collide with the medallion,
       and down here it cannot. The two columns are unequal in length and that
       is fine; they are anchored at the top of the band, not stretched to
       match. */
    .hm-copy p{display:block; max-width:46ch; margin:12px 0 0}
  }

  /* A phone, or a narrow window turned tall: one language, full width, and a
     deeper band because the same paragraph needs far more lines at this width
     (it overflowed the 30% band on a 390x844 phone). */
  @media (orientation: portrait) and (max-width:900px){
    /* The words win the argument at this width. The same two paragraphs need
       roughly half again as many lines as they do on a portrait monitor, and
       truncating both mid-sentence reads as broken - so the artwork gives up
       the room rather than the copy. It is still the first thing on screen and
       still perfectly legible at this size. */
    /* #hm-art ONLY, exactly as in the block above. This said `svg.hm-plate`
       and so clipped all three layers, which brought the cut-off background
       straight back at this size - and worse on a touch device, where the
       medallion rule below takes it to 100% while the field stayed at 42%. */
    .hm-stage svg#hm-art{ height:42% }
    .hm-copy-ar{display:none}
    .hm-copy{
      width:100%; max-width:100%; height:58%;
      align-items:flex-start; text-align:left;
      padding:clamp(12px,1.6vh,22px) 24px 0;
    }
    .hm-copy h1{max-width:none}
    /* the same words need roughly half again as many lines at this width */
    .hm-copy p{font-size:12.5px; line-height:1.55; margin-top:10px}
  }

  /* Belt and braces on every portrait size: the band is a fixed fraction of
     the viewport but the copy is not, so on some phone nobody has tested the
     paragraph would run out of the bottom of it. Clamping the lines means the
     worst case is a truncated sentence rather than text spilling over the
     footer. */
  @media (orientation: portrait){
    .hm-copy p{
      display:-webkit-box; -webkit-box-orient:vertical; overflow:hidden;
      -webkit-line-clamp:10;                 /* generous: never bites at 1080+ */
    }
  }
  @media (orientation: portrait) and (max-width:900px){
    .hm-copy p{-webkit-line-clamp:9}
  }

  /* ══════════ TOUCH DEVICES: the artwork alone ══════════
     On an iPad or a phone the copy is dropped entirely and the piece gets the
     whole stage. There is no room for an essay next to a medallion at these
     sizes, and a hero that is one strong image beats a hero that is a cramped
     image plus a wall of text nobody reads on a train.

     Keyed off the INPUT, not the width: `hover:none` + `pointer:coarse` is
     true for touch screens and false for a desktop monitor, so this cannot
     catch a portrait monitor the way a width breakpoint would - Asghar's is
     1080-1200 wide, right in the middle of iPad territory. The width cap is
     only there so a large touch display or a kiosk keeps its words; 1366
     clears the biggest iPad in landscape. */
  @media (hover:none) and (pointer:coarse) and (max-width:1366px){
    .hm-copy, .hm-copy-ar{display:none}
    .hm-stage svg#hm-art{height:100%}
  }

  .hm-stroke{fill:none; stroke-linecap:round; stroke-linejoin:round}
  .hm-fillin{opacity:0}
  @media (prefers-reduced-motion:reduce){
    #hm-arcsA,#hm-arcsB,#hm-arcsC,#hm-spiral,#hm-bandArcade,#hm-bandStars,#hm-bandKey,#hm-bandGirih,
    #hm-well,#hm-mark,#hm-bandMoulding{animation:none}
  }
