{
    "generated": "2026-09-02T14:40:02+03:00",
    "source_site": "https://gsap.pinul.com",
    "page": {
        "id": 461,
        "slug": "clares",
        "title": "Clares — orbital hero (test)",
        "url": "https://gsap.pinul.com/clares/"
    },
    "requires": {
        "bricks": "2.3.12",
        "wp": "7.1",
        "header_scripts": "<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js\\\"></script>\r\n\r\n\r\n<script src=\\\"https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js\\\" id=\\\"gsap-js-js\\\"></script>\r\n<script src=\\\"https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js?\\\" id=\\\"gsap-st-js\\\"></script>\r\n<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/SplitText.min.js\\\" integrity=\\\"sha512-wOeEC+9qERAzhliwBFPDb6t8TiFFxdxG8vhK/Ygs7TuC44bpg8pg/X2/U/u+0X4fK05wb9id1EIipnF02+CFQw==\\\" crossorigin=\\\"anonymous\\\" referrerpolicy=\\\"no-referrer\\\"></script>\r\n\r\n\r\n<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/ScrollSmoother.min.js\\\" ></script>\r\n<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/Observer.min.js\\\" integrity=\\\"sha512-GdLRoSd7fNE36D+SKii0MOd6MG1kljA09It5wir9Rv6lYYWWYjXQTCtvzvJppbEG0eAGdoavuIxDsDN8pzzD2A==\\\" crossorigin=\\\"anonymous\\\" referrerpolicy=\\\"no-referrer\\\"></script>\r\n<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/MotionPathPlugin.min.js\\\" integrity=\\\"sha512-vuNgw6lF+p97XmL9lFpbQZWBXxIFnwUVt7J/uknE4NJru5GJjjhIAauXdDtNdezqOJFnit+M3AeaM+gZ93hlmg==\\\" crossorigin=\\\"anonymous\\\" referrerpolicy=\\\"no-referrer\\\"></script>\r\n\r\n",
        "note": "Theme style sets a 10px rem base on this site; the ported CSS is already in px."
    },
    "page_settings": {
        "bodyClasses": "clares",
        "disableLazyLoad": true
    },
    "elements": [
        {
            "id": "clasec",
            "name": "section",
            "parent": 0,
            "children": [
                "clacode",
                "clanav",
                "clamenu",
                "claproj"
            ],
            "settings": {
                "tag": "section",
                "_cssClasses": "cla",
                "_padding": {
                    "top": "0",
                    "right": "0",
                    "bottom": "0",
                    "left": "0"
                }
            },
            "label": "CLARES"
        },
        {
            "id": "clacode",
            "name": "code",
            "parent": "clasec",
            "children": [],
            "settings": {
                "code": "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Tenor+Sans&display=swap\">\n\n<div id=\"cla-bg-fade\"></div>\n<canvas id=\"cla-scene\"></canvas>\n",
                "language": "html",
                "executeCode": true,
                "noRoot": true,
                "signature": "a93ea78c8dac0489fa61286adf9516bc",
                "cssCode": "/* CLARES — scoped port of the standalone orbital hero.\n   All rem values from the source were converted to px: this site's theme style\n   sets a 10px rem base, so rem would render everything at 62.5%. */\n\nbody.clares { background: #F5F1EC; overflow: hidden; }\nbody.clares #brx-header,\nbody.clares #brx-footer { display: none !important; }\n\n.cla {\n  --cla-bg: #F5F1EC;\n  --cla-nav-text: #404040;\n  --cla-divider: #D9D9D9;\n  --cla-logo: #1E1B18;\n  --cla-nav-pad: 48px;\n\n  display: block;\n  padding: 0;\n  margin: 0;\n  font-family: 'Tenor Sans', serif;\n  -webkit-font-smoothing: antialiased;\n}\n\n.cla *, .cla *::before, .cla *::after { box-sizing: border-box; }\n\n/* ---- Background fade-in overlay (white -> transparent) ---- */\n#cla-bg-fade {\n  position: fixed;\n  inset: 0;\n  background: #ffffff;\n  z-index: 50;\n  pointer-events: none;\n  opacity: 1;\n  animation: claBgFade 0.8s ease forwards;\n}\n@keyframes claBgFade { to { opacity: 0; } }\n\n/* ---- Navigation ---- */\n#cla-nav {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  z-index: 20;\n  opacity: 0;\n  transition: opacity 1s ease;\n}\n#cla-nav.cla-visible { opacity: 1; }\n\n.cla-nav-inner {\n  width: 100%;\n  padding: 28px var(--cla-nav-pad) 22px;\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: space-between;\n  position: relative;\n}\n\n.cla-nav-group {\n  display: flex;\n  flex-direction: row;\n  gap: 56px;\n  padding: 0;\n  margin: 0;\n  /* Bricks' .brxe-block is width:100%. Left as-is both nav groups fill the bar,\n     so justify-content:space-between has no free space to distribute and the\n     right-hand links collapse onto the centred wordmark. As a row-flex item,\n     width:auto sizes each group to its content. */\n  width: auto;\n}\n\n.cla .cla-nav-link {\n  font-family: 'Tenor Sans', serif;\n  font-size: 13.1px;\n  font-weight: 400;\n  letter-spacing: 0.25em;\n  text-transform: uppercase;\n  color: var(--cla-nav-text);\n  text-decoration: none;\n  cursor: pointer;\n  margin: 0;\n  transition: opacity 0.3s ease;\n}\n.cla .cla-nav-link:hover { opacity: 0.55; }\n\n.cla .cla-nav-brand {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  transform: translate(-50%, -55%);\n  font-family: 'Tenor Sans', serif;\n  font-size: 25.6px;\n  font-weight: 400;\n  letter-spacing: 0.32em;\n  text-transform: uppercase;\n  color: #000000;\n  padding-left: 0.32em; /* optical centering for letter-spacing */\n  white-space: nowrap;\n  margin: 0;\n}\n\n/* Divider spans full width, inset by the same padding as the nav buttons,\n   so it aligns exactly under INDEX (left) and CONTACT (right). */\n.cla .cla-nav-divider {\n  height: 1px;\n  /* Bricks sets width:100% on .brxe-block. Left as-is the side margins push\n     the line 2x--cla-nav-pad past the viewport; width:auto collapses it to 0\n     (the parent block is a flex column with align-items:flex-start) and\n     align-self:stretch is inert while an explicit width exists. So state the\n     width outright: margins + width then add up to exactly 100%. */\n  width: calc(100% - 2 * var(--cla-nav-pad));\n  margin: 0 var(--cla-nav-pad);\n  background: var(--cla-divider);\n}\n\n/* ---- 3D canvas ---- */\n#cla-scene {\n  position: fixed;\n  inset: 0;\n  z-index: 10;\n  display: block;\n  cursor: grab;\n  touch-action: none;\n}\n#cla-scene.cla-dragging { cursor: grabbing; }\n\n/* Mobile-only chrome is hidden on desktop/tablet by default. */\n.cla-nav-brand-mobile,\n#cla-hamburger,\n#cla-mobile-menu { display: none; }\n\n/* ====================================================================\n   Project detail page (shown after a card is clicked)\n   ==================================================================== */\n\n/* The image that flies out of the sphere and settles on the right.\n   Its top/left/width/height are driven in JS; object-fit cover only\n   matters at the final resting rect (where the box is not 3:4). */\n#cla-project-img {\n  position: fixed;\n  z-index: 15;\n  object-fit: cover;\n  object-position: center;\n  display: none;\n  border-radius: 0;\n  box-shadow: 0 12px 40px rgba(42, 37, 32, 0.15);\n  will-change: top, left, width, height;\n}\n\n/* Left-column text overlay */\n#cla-project {\n  position: fixed;\n  inset: 0;\n  z-index: 15;\n  pointer-events: none;\n  display: none;\n}\n#cla-project.cla-active { display: block; }\n\n#cla-project-text {\n  position: fixed;\n  display: block;\n  color: #404040;\n}\n\n.cla #cla-p-title {\n  position: absolute;\n  top: 0;\n  left: 0;\n  font-family: 'Tenor Sans', serif;\n  font-weight: 400;\n  font-size: clamp(35.2px, 3.6vw, 54.4px);\n  letter-spacing: normal;       /* normal tracking, unlike the nav */\n  line-height: 1.05;\n  color: #1E1B18;\n  margin: 0;\n}\n\n#cla-p-body {\n  position: absolute;\n  bottom: 0;                    /* anchored near the bottom; large gap below the title */\n  left: 0;\n  right: 0;\n  display: block;\n}\n\n.cla #cla-p-eyebrow {\n  font-family: 'Tenor Sans', serif;\n  font-size: 12.8px;\n  font-weight: 400;\n  letter-spacing: 0.25em;       /* generous, matching the nav */\n  text-transform: uppercase;\n  color: #1E1B18;\n  margin: 0;\n}\n\n.cla #cla-p-body p {\n  font-family: 'Tenor Sans', serif;\n  font-size: 14.4px;\n  font-weight: 400;\n  letter-spacing: normal;\n  line-height: 1.6;\n  color: #404040;\n  max-width: 440px;\n  margin: 0;\n}\n.cla #cla-p-1 { margin-top: 36px; }   /* breathing room below the eyebrow */\n.cla #cla-p-2 { margin-top: 16px; }   /* standard paragraph gap */\n\n/* fade-in + slide-up reveal (per-element delay set in JS) */\n.cla-reveal {\n  opacity: 0;\n  transform: translateY(20px);\n  transition: opacity 0.4s ease, transform 0.4s ease;\n}\n.cla-reveal.cla-show {\n  opacity: 1;\n  transform: translateY(0);\n}\n\n/* ====================================================================\n   Mobile (< 768px)\n   ==================================================================== */\n@media (max-width: 767px) {\n  .cla { --cla-nav-pad: 20px; }\n\n  /* --- Navigation: logo left, hamburger right --- */\n  #cla-nav {\n    z-index: 110;                       /* keep nav (and the close icon) above the menu */\n    background: var(--cla-bg);          /* opaque so scrolled content can't bleed through */\n  }\n  .cla-nav-inner { padding: 18px var(--cla-nav-pad) 15px; }\n  .cla-desktop { display: none !important; }   /* hide the four links + centre brand */\n\n  .cla .cla-nav-brand-mobile {\n    display: block;\n    font-family: 'Tenor Sans', serif;\n    font-weight: 400;\n    text-transform: uppercase;\n    letter-spacing: 0.25em;\n    color: #404040;\n    font-size: 19.2px;\n    padding-left: 0.25em;\n    text-decoration: none;\n    cursor: pointer;\n    margin: 0;\n  }\n\n  #cla-hamburger {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    gap: 5px;\n    width: 30px;\n    height: 22px;\n    padding: 0;\n    background: none;\n    border: none;\n    cursor: pointer;\n  }\n  #cla-hamburger span {\n    display: block;\n    width: 28px;\n    height: 1.5px;\n    background: #404040;\n    transition: transform 0.3s ease, opacity 0.3s ease;\n  }\n  #cla-hamburger.cla-open span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }\n  #cla-hamburger.cla-open span:nth-child(2) { opacity: 0; }\n  #cla-hamburger.cla-open span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }\n\n  /* --- Full-screen menu --- */\n  #cla-mobile-menu {\n    display: flex;\n    position: fixed;\n    inset: 0;\n    z-index: 90;                        /* below the nav bar so the close icon stays tappable */\n    background: #F5F1EC;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    gap: 44px;\n    opacity: 0;\n    transform: translateX(100%);\n    pointer-events: none;\n    transition: opacity 0.4s ease, transform 0.4s ease;\n  }\n  #cla-mobile-menu.cla-open { opacity: 1; transform: translateX(0); pointer-events: auto; }\n  .cla .cla-m-link {\n    font-family: 'Tenor Sans', serif;\n    font-weight: 400;\n    text-transform: uppercase;\n    letter-spacing: 0.25em;\n    color: #404040;\n    font-size: 27.2px;\n    text-decoration: none;\n    cursor: pointer;\n    margin: 0;\n  }\n\n  /* --- Project page: single column, scrollable --- */\n  #cla-project {\n    position: fixed;\n    inset: 0;\n    z-index: 15;\n    overflow-y: auto;\n    -webkit-overflow-scrolling: touch;\n    pointer-events: auto;\n    background: var(--cla-bg);\n    padding: 0 var(--cla-nav-pad);      /* padding-top set in JS (below the nav) */\n  }\n\n  /* image flowed in at the top once it has landed */\n  #cla-project-img.cla-m-inflow {\n    position: static;\n    display: block;\n    width: 100%;\n    height: 55vh;\n    margin: 0;\n    box-shadow: none;\n  }\n\n  #cla-project-text {\n    position: static;\n    left: auto; top: auto;\n    width: auto;\n    height: auto;\n    padding-bottom: 48px;               /* comfortable space at the end of scroll */\n  }\n  .cla #cla-p-title {\n    position: static;\n    font-size: clamp(32px, 7vw, 40px);\n    margin-top: 32px;\n  }\n  #cla-p-body {\n    position: static;\n    bottom: auto;\n    margin-top: 44px;                   /* generous gap below the title */\n  }\n  .cla #cla-p-eyebrow { font-size: 12px; }\n  .cla #cla-p-body p {\n    max-width: none;\n    font-size: 14.1px;\n  }\n  .cla #cla-p-1 { margin-top: 16px; }   /* eyebrow -> first paragraph */\n  .cla #cla-p-2 { margin-top: 16px; }\n}\n",
                "javascriptCode": "/* CLARES — orbital hero.\n   Three.js is pulled in with a dynamic import() from inside a classic script,\n   so this needs no import map and no type=\"module\" tag — which matters because\n   Bricks injects javascriptCode as a plain <script>. */\n(function () {\n  var root = document.querySelector('.cla');\n  if (!root) return;\n\n  var THREE_URL = 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';\n  var BASE = 'https://gsap.pinul.com/wp-content/uploads/2026/09/';\n\n  document.documentElement.style.overflow = 'hidden';\n\n  import(THREE_URL).then(start).catch(function (err) {\n    console.error('[clares] three.js failed to load:', err);\n  });\n\n  function start(THREE) {\n    // --------------------------------------------------------------------\n    // Config\n    // --------------------------------------------------------------------\n    var IMAGES = ['01','02','03','04','05','06','07','08','09','10','11','12']\n      .map(function (n) { return BASE + 'clares-' + n + '.jpg'; });\n\n    var CARD_W = 180;          // px (base scale)\n    var CARD_H = 240;          // px\n    var CARD_ASPECT = CARD_W / CARD_H;\n    var IMG_ASPECT = 1792 / 2400;    // native aspect of every photo (w/h)\n    var SPHERE_RADIUS_FRAC = 0.40;   // horizontal radius, of min viewport dimension\n    var SPHERE_Y_FACTOR = 0.60;      // vertical radius as fraction of horizontal\n\n    var MOBILE_BREAK = 768;\n    var TABLET_BREAK = 1024;\n    var MOBILE_ROLL = THREE.MathUtils.degToRad(80);   // roll the orbit toward vertical\n    var MOBILE_CARD_SCALE = 140 / 180;\n    var MOBILE_RADIUS_FRAC_X = 0.78;  // tall (vertical) radius, of viewport width\n    var MOBILE_RADIUS_FRAC_Y = 0.42;  // narrow (horizontal) radius, of viewport width\n    var DEPTH_SCALE_MIN = 0.42;\n    var DEPTH_SCALE_MAX = 1.0;\n    var DEPTH_OPACITY_MIN = 0.30;\n    var DEPTH_OPACITY_MAX = 1.0;\n    var NAV_SAFE_GAP = 16;\n    var REVOLUTION_SECONDS = 50;\n    var IDLE_YAW_SPEED = (Math.PI * 2) / REVOLUTION_SECONDS;\n    var BOB_AMPLITUDE = 6;\n    var BOB_PERIOD = 4;\n    var MAX_TILT = THREE.MathUtils.degToRad(25);\n    var HOVER_SCALE = 1.15;\n    var HOVER_FORWARD = 40;\n    var FOV = 40;\n\n    var STAGGER = 0.12;\n    var CARD_ENTER_DUR = 0.6;\n    var NAV_DELAY = 0.45;\n\n    // --------------------------------------------------------------------\n    // Scene setup\n    // --------------------------------------------------------------------\n    var canvas = document.getElementById('cla-scene');\n    var navEl = document.getElementById('cla-nav');\n\n    var renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, alpha: true });\n    renderer.setClearColor(0x000000, 0);\n    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n\n    var scene = new THREE.Scene();\n    var camera = new THREE.PerspectiveCamera(FOV, 1, 1, 20000);\n\n    // We work in \"pixel units\": 1 world unit is about 1 CSS pixel. The camera\n    // distance is chosen so the visible height at z=0 equals the viewport height.\n    var cameraDist = 1000;\n    var radiusX = 340, radiusY = 200, radiusZ = 340;\n    var centerY = 0;\n    var viewportMin = 800;\n\n    var isMobile = false;\n    var cardScale = 1;\n    var rollCos = 1, rollSin = 0;\n\n    function navPad() {\n      var v = parseFloat(getComputedStyle(root).getPropertyValue('--cla-nav-pad'));\n      return isNaN(v) ? 20 : v;\n    }\n\n    function layout() {\n      var w = window.innerWidth;\n      var h = window.innerHeight;\n      renderer.setSize(w, h);\n      camera.aspect = w / h;\n      cameraDist = (h / 2) / Math.tan(THREE.MathUtils.degToRad(FOV) / 2);\n      camera.position.set(0, 0, cameraDist);\n      camera.lookAt(0, 0, 0);\n      camera.updateProjectionMatrix();\n      viewportMin = Math.min(w, h);\n\n      var navH = (navEl && navEl.offsetHeight) ? navEl.offsetHeight : 72;\n      centerY = -navH / 2;\n\n      isMobile = w < MOBILE_BREAK;\n      cardScale = isMobile ? MOBILE_CARD_SCALE : 1;\n      var roll = isMobile ? MOBILE_ROLL : 0;\n      rollCos = Math.cos(roll);\n      rollSin = Math.sin(roll);\n\n      camera.updateMatrixWorld();\n\n      if (isMobile) {\n        radiusX = w * MOBILE_RADIUS_FRAC_X;\n        radiusY = w * MOBILE_RADIUS_FRAC_Y;\n        radiusZ = (radiusX + radiusY) / 2;\n      } else {\n        radiusX = viewportMin * SPHERE_RADIUS_FRAC;\n        radiusZ = radiusX;\n\n        // Shrink the vertical radius until the whole rotation clears the nav bar\n        // and stays inside the viewport, perspective and depth shrink included.\n        var topBound = navH + NAV_SAFE_GAP;\n        var botBound = h - NAV_SAFE_GAP;\n        var yFactor = (w < TABLET_BREAK) ? 0.85 : SPHERE_Y_FACTOR;\n        var r = radiusX * yFactor;\n        for (var iter = 0; iter < 40; iter++) {\n          var ext = verticalExtent(r);\n          if (ext[0] >= topBound && ext[1] <= botBound) break;\n          r *= 0.95;\n          if (r < 50) break;\n        }\n        radiusY = r;\n      }\n    }\n\n    var _v = new THREE.Vector3();\n    var _dir = new THREE.Vector3();\n    var _m = new THREE.Matrix4();\n    function verticalExtent(rY) {\n      var H = window.innerHeight;\n      var minPx = Infinity, maxPx = -Infinity;\n      for (var s = 0; s < 360; s += 6) {\n        _m.makeRotationY(THREE.MathUtils.degToRad(s));\n        for (var i = 0; i < baseDirs.length; i++) {\n          _dir.copy(baseDirs[i]).applyMatrix4(_m);\n          var depthScale = DEPTH_SCALE_MIN + (DEPTH_SCALE_MAX - DEPTH_SCALE_MIN) * ((_dir.z + 1) / 2);\n          var px = _dir.x * radiusX, py = _dir.y * rY + centerY, pz = _dir.z * radiusZ;\n          var hw = (CARD_W / 2) * depthScale;\n          var hh = (CARD_H / 2) * depthScale + BOB_AMPLITUDE;\n          var offs = [-hh, hh];\n          for (var k = 0; k < 2; k++) {\n            _v.set(px + hw, py + offs[k], pz).project(camera);\n            var sy = (-_v.y * 0.5 + 0.5) * H;\n            if (sy < minPx) minPx = sy;\n            if (sy > maxPx) maxPx = sy;\n          }\n        }\n      }\n      return [minPx, maxPx];\n    }\n\n    // --------------------------------------------------------------------\n    // Fibonacci sphere distribution\n    // --------------------------------------------------------------------\n    function fibonacciSphere(n) {\n      var pts = [];\n      var golden = Math.PI * (3 - Math.sqrt(5));\n      for (var i = 0; i < n; i++) {\n        var y = 1 - (i / (n - 1)) * 2;\n        var r = Math.sqrt(1 - y * y);\n        var theta = golden * i;\n        pts.push(new THREE.Vector3(Math.cos(theta) * r, y, Math.sin(theta) * r));\n      }\n      return pts;\n    }\n    var baseDirs = fibonacciSphere(IMAGES.length);\n\n    layout();\n\n    // --------------------------------------------------------------------\n    // Shadow texture (soft radial gradient) shared by all cards\n    // --------------------------------------------------------------------\n    function makeShadowTexture() {\n      var s = 256;\n      var c = document.createElement('canvas');\n      c.width = c.height = s;\n      var ctx = c.getContext('2d');\n      var g = ctx.createRadialGradient(s / 2, s / 2, 0, s / 2, s / 2, s / 2);\n      g.addColorStop(0, 'rgba(42,37,32,0.55)');\n      g.addColorStop(0.5, 'rgba(42,37,32,0.22)');\n      g.addColorStop(1, 'rgba(42,37,32,0)');\n      ctx.fillStyle = g;\n      ctx.fillRect(0, 0, s, s);\n      var tex = new THREE.CanvasTexture(c);\n      tex.colorSpace = THREE.SRGBColorSpace;\n      return tex;\n    }\n    var shadowTex = makeShadowTexture();\n\n    // --------------------------------------------------------------------\n    // Cover-fit a texture onto the 3:4 card\n    // --------------------------------------------------------------------\n    function coverTexture(tex) {\n      var img = tex.image;\n      if (!img || !img.width) return;\n      var imgAspect = img.width / img.height;\n      tex.center.set(0.5, 0.5);\n      tex.repeat.set(1, 1);\n      tex.offset.set(0, 0);\n      if (imgAspect > CARD_ASPECT) {\n        tex.repeat.x = CARD_ASPECT / imgAspect;\n      } else {\n        tex.repeat.y = imgAspect / CARD_ASPECT;\n      }\n    }\n\n    // --------------------------------------------------------------------\n    // Build cards\n    // --------------------------------------------------------------------\n    var loader = new THREE.TextureLoader();\n    loader.setCrossOrigin('anonymous');\n    var cardGeo = new THREE.PlaneGeometry(CARD_W, CARD_H);\n    var shadowGeo = new THREE.PlaneGeometry(CARD_W * 1.35, CARD_H * 1.2);\n\n    var cards = [];\n\n    baseDirs.forEach(function (dir, i) {\n      var group = new THREE.Group();\n\n      var shadowMat = new THREE.MeshBasicMaterial({ map: shadowTex, transparent: true, opacity: 0, depthWrite: false });\n      var shadow = new THREE.Mesh(shadowGeo, shadowMat);\n      shadow.position.set(0, -10, -2);\n      shadow.renderOrder = 0;\n      group.add(shadow);\n\n      var mat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0, depthWrite: false });\n      var mesh = new THREE.Mesh(cardGeo, mat);\n      mesh.renderOrder = 1;\n      group.add(mesh);\n\n      scene.add(group);\n\n      loader.load(IMAGES[i], function (t) {\n        t.colorSpace = THREE.SRGBColorSpace;\n        t.anisotropy = renderer.capabilities.getMaxAnisotropy();\n        coverTexture(t);\n        mat.map = t;\n        mat.needsUpdate = true;\n      });\n\n      cards.push({\n        group: group, mesh: mesh, mat: mat, shadow: shadow, shadowMat: shadowMat,\n        dir: dir.clone(),\n        phase: Math.random() * Math.PI * 2,\n        enterStart: i * STAGGER,\n        hover: 0,\n        holdDir: dir.clone(),\n        holding: false,\n        startRect: null\n      });\n    });\n\n    // --------------------------------------------------------------------\n    // Center logo (canvas texture plane at sphere center)\n    // --------------------------------------------------------------------\n    var logoMesh = null, logoMat = null;\n    function logoWidth() {\n      return isMobile ? Math.min(window.innerWidth * 0.62, 320) : viewportMin * 0.52;\n    }\n    function buildLogo() {\n      var cw = 2048, ch = 512;\n      var c = document.createElement('canvas');\n      c.width = cw; c.height = ch;\n      var ctx = c.getContext('2d');\n      ctx.clearRect(0, 0, cw, ch);\n      ctx.fillStyle = '#1E1B18';\n      ctx.textAlign = 'center';\n      ctx.textBaseline = 'middle';\n      if (ctx.letterSpacing !== undefined) ctx.letterSpacing = '40px';\n      ctx.font = '300px \"Tenor Sans\", serif';\n      ctx.fillText('CLARES', cw / 2 + 20, ch / 2 + 10);\n\n      var tex = new THREE.CanvasTexture(c);\n      tex.colorSpace = THREE.SRGBColorSpace;\n      tex.anisotropy = renderer.capabilities.getMaxAnisotropy();\n\n      logoMat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, opacity: 0, depthWrite: false });\n      var aspect = cw / ch;\n      var logoW = logoWidth();\n      logoMesh = new THREE.Mesh(new THREE.PlaneGeometry(logoW, logoW / aspect), logoMat);\n      logoMesh.position.set(0, centerY, 0);\n      scene.add(logoMesh);\n    }\n\n    if (document.fonts && document.fonts.ready) {\n      document.fonts.load('300px \"Tenor Sans\"').then(function () {\n        document.fonts.ready.then(buildLogo);\n      });\n    } else {\n      buildLogo();\n    }\n\n    // --------------------------------------------------------------------\n    // Interaction state\n    // --------------------------------------------------------------------\n    var yaw = 0, pitch = 0, yawVel = 0;\n    var dragging = false;\n    var lastX = 0, lastY = 0, lastMoveTime = 0;\n    var downX = 0, downY = 0, moved = false;\n\n    var raycaster = new THREE.Raycaster();\n    var pointerNDC = new THREE.Vector2();\n    var pointerInside = false;\n\n    // --------------------------------------------------------------------\n    // Project page state\n    // --------------------------------------------------------------------\n    var mode = 'hero';\n    var activeCard = -1;\n    var bobTime = 0;\n    var sceneFade = 1;\n    var fade = null;\n\n    var PROJECT_NAMES = [\n      'The Merge', 'Golden Hour', 'Still Life', 'Maison',\n      'The Crossing', 'Hinterland', 'Quietude', 'Field Notes',\n      'The Atelier', 'Solstice', 'Reverie', 'Provenance'\n    ];\n\n    var projectEl = document.getElementById('cla-project');\n    var projectImg = document.getElementById('cla-project-img');\n    var projectText = document.getElementById('cla-project-text');\n    var pTitle = document.getElementById('cla-p-title');\n    var revealEls = ['cla-p-title', 'cla-p-eyebrow', 'cla-p-1', 'cla-p-2']\n      .map(function (id) { return document.getElementById(id); });\n    var hamburgerEl = document.getElementById('cla-hamburger');\n    var mobileMenuEl = document.getElementById('cla-mobile-menu');\n\n    function startFade(to, durSec) {\n      fade = { t0: performance.now(), durMs: durSec * 1000, from: sceneFade, to: to };\n    }\n\n    function onPointerDown(e) {\n      if (mode !== 'hero') return;\n      dragging = true;\n      canvas.classList.add('cla-dragging');\n      lastX = e.clientX; lastY = e.clientY;\n      downX = e.clientX; downY = e.clientY; moved = false;\n      pointerNDC.x = (e.clientX / window.innerWidth) * 2 - 1;\n      pointerNDC.y = -(e.clientY / window.innerHeight) * 2 + 1;\n      lastMoveTime = performance.now();\n      yawVel = 0;\n      if (canvas.setPointerCapture) canvas.setPointerCapture(e.pointerId);\n    }\n\n    function onPointerMove(e) {\n      pointerNDC.x = (e.clientX / window.innerWidth) * 2 - 1;\n      pointerNDC.y = -(e.clientY / window.innerHeight) * 2 + 1;\n      pointerInside = true;\n\n      if (!dragging) return;\n      if (Math.hypot(e.clientX - downX, e.clientY - downY) > 6) moved = true;\n      var now = performance.now();\n      var dt = Math.max((now - lastMoveTime) / 1000, 1e-4);\n      var dx = e.clientX - lastX;\n      var dy = e.clientY - lastY;\n\n      var yawDelta = dx * 0.006;\n      yaw += yawDelta;\n      pitch = THREE.MathUtils.clamp(pitch + dy * 0.006, -MAX_TILT, MAX_TILT);\n\n      yawVel = yawDelta / dt;\n      lastX = e.clientX; lastY = e.clientY;\n      lastMoveTime = now;\n    }\n\n    function onPointerUp(e) {\n      if (!dragging) return;\n      dragging = false;\n      canvas.classList.remove('cla-dragging');\n      if (canvas.releasePointerCapture) canvas.releasePointerCapture(e.pointerId);\n\n      // A tap (no real drag) on a card opens its project page.\n      if (!moved && mode === 'hero') {\n        raycaster.setFromCamera(pointerNDC, camera);\n        var best = -1, bestDist = Infinity;\n        for (var i = 0; i < cards.length; i++) {\n          if (!cards[i].mesh.visible) continue;\n          var hit = raycaster.intersectObject(cards[i].mesh, false);\n          if (hit.length && hit[0].distance < bestDist) { bestDist = hit[0].distance; best = i; }\n        }\n        if (best >= 0) { enterProject(best); return; }\n      }\n      yawVel = THREE.MathUtils.clamp(yawVel, -6, 6);\n    }\n\n    canvas.addEventListener('pointerdown', onPointerDown);\n    window.addEventListener('pointermove', onPointerMove);\n    window.addEventListener('pointerup', onPointerUp);\n    canvas.addEventListener('pointerleave', function () { pointerInside = false; });\n\n    window.addEventListener('resize', function () {\n      layout();\n      if (logoMesh) {\n        var aspect = 2048 / 512;\n        var logoW = logoWidth();\n        logoMesh.geometry.dispose();\n        logoMesh.geometry = new THREE.PlaneGeometry(logoW, logoW / aspect);\n        logoMesh.position.set(0, centerY, 0);\n      }\n    });\n\n    // --------------------------------------------------------------------\n    // Animation loop\n    // --------------------------------------------------------------------\n    var tmp = new THREE.Vector3();\n    var rotM = new THREE.Matrix4();\n    var qx = new THREE.Quaternion();\n    var qy = new THREE.Quaternion();\n    var xAxis = new THREE.Vector3(1, 0, 0);\n    var yAxis = new THREE.Vector3(0, 1, 0);\n    var toCam = new THREE.Vector3();\n\n    var clock = new THREE.Clock();\n    var elapsed = 0;\n    var navShown = false;\n    var hoveredIndex = -1;\n\n    function buildRotationMatrix() {\n      qx.setFromAxisAngle(xAxis, pitch);\n      qy.setFromAxisAngle(yAxis, yaw);\n      qy.multiply(qx);\n      rotM.makeRotationFromQuaternion(qy);\n    }\n\n    function updateHover() {\n      if (dragging || !pointerInside) { hoveredIndex = -1; return; }\n      raycaster.setFromCamera(pointerNDC, camera);\n      var best = -1, bestDist = Infinity;\n      for (var i = 0; i < cards.length; i++) {\n        var hit = raycaster.intersectObject(cards[i].mesh, false);\n        if (hit.length && hit[0].distance < bestDist) { bestDist = hit[0].distance; best = i; }\n      }\n      hoveredIndex = best;\n    }\n\n    function step(dt) {\n      elapsed += dt;\n      var heroMode = (mode === 'hero');\n      if (heroMode) bobTime += dt;\n\n      if (fade) {\n        var k = Math.min(1, (performance.now() - fade.t0) / fade.durMs);\n        sceneFade = fade.from + (fade.to - fade.from) * k;\n        if (k >= 1) fade = null;\n      }\n\n      if (heroMode && !dragging) {\n        yaw += (IDLE_YAW_SPEED + yawVel) * dt;\n        yawVel *= Math.exp(-dt / 0.7);\n        if (Math.abs(yawVel) < 1e-4) yawVel = 0;\n        pitch += (0 - pitch) * (1 - Math.exp(-dt / 1.1));\n      }\n\n      buildRotationMatrix();\n      if (heroMode) updateHover();\n\n      for (var i = 0; i < cards.length; i++) {\n        var c = cards[i];\n\n        var eIn = THREE.MathUtils.clamp((elapsed - c.enterStart) / CARD_ENTER_DUR, 0, 1);\n        var ease = eIn < 1 ? (1 - Math.pow(1 - eIn, 3)) : 1;\n        var enterOpacity = ease;\n        var enterScale = 0.7 + 0.3 * ease;\n\n        if (heroMode) {\n          var isHover = (i === hoveredIndex);\n          if (isHover && !c.holding) {\n            c.holdDir.copy(c.dir).applyMatrix4(rotM);\n            c.holding = true;\n          } else if (!isHover) {\n            c.holding = false;\n          }\n          var hoverTarget = isHover ? 1 : 0;\n          var hoverRate = isHover ? (dt / 0.4) : (dt / 0.6);\n          c.hover += (hoverTarget - c.hover) * Math.min(1, hoverRate * 2.2);\n          c.hover = THREE.MathUtils.clamp(c.hover, 0, 1);\n        }\n        var hEase = c.hover * c.hover * (3 - 2 * c.hover);\n\n        tmp.copy(c.dir).applyMatrix4(rotM);\n        if (c.hover > 0.001) tmp.lerp(c.holdDir, hEase).normalize();\n\n        var px = tmp.x * radiusX;\n        var py = tmp.y * radiusY + centerY;\n        var pz = tmp.z * radiusZ;\n\n        var depthT = (tmp.z + 1) / 2;\n        var depthScale = DEPTH_SCALE_MIN + (DEPTH_SCALE_MAX - DEPTH_SCALE_MIN) * depthT;\n        var depthOpacity = DEPTH_OPACITY_MIN + (DEPTH_OPACITY_MAX - DEPTH_OPACITY_MIN) * depthT;\n\n        py += BOB_AMPLITUDE * Math.sin(bobTime * (Math.PI * 2 / BOB_PERIOD) + c.phase);\n\n        if (isMobile) {\n          var rx = px, ry = py - centerY;\n          px = rx * rollCos - ry * rollSin;\n          py = centerY + rx * rollSin + ry * rollCos;\n        }\n\n        if (c.hover > 0.001) {\n          toCam.set(camera.position.x - px, camera.position.y - py, camera.position.z - pz).normalize();\n          var fwd = HOVER_FORWARD * hEase;\n          px += toCam.x * fwd; py += toCam.y * fwd; pz += toCam.z * fwd;\n        }\n\n        c.group.position.set(px, py, pz);\n        c.group.lookAt(camera.position);\n\n        var sc = enterScale * depthScale * (1 + (HOVER_SCALE - 1) * hEase);\n        c.group.scale.setScalar(sc * cardScale);\n\n        var opacity = depthOpacity + (1 - depthOpacity) * hEase;\n        var sf = (i === activeCard) ? 1 : sceneFade;\n        c.mat.opacity = enterOpacity * opacity * sf;\n        c.shadowMat.opacity = enterOpacity * opacity * (0.35 + 0.55 * hEase) * sf;\n        c.shadow.position.y = -10 - 8 * hEase;\n\n        var dist = camera.position.distanceTo(c.group.position);\n        c.mesh.renderOrder = -dist;\n        c.shadow.renderOrder = -dist - 0.5;\n      }\n\n      if (logoMesh) {\n        logoMesh.lookAt(camera.position);\n        logoMesh.renderOrder = -camera.position.distanceTo(logoMesh.position);\n        var logoIn = THREE.MathUtils.clamp((elapsed - NAV_DELAY) / 1.0, 0, 1);\n        logoMat.opacity = logoIn * sceneFade;\n      }\n\n      if (!navShown && elapsed > NAV_DELAY) {\n        navEl.classList.add('cla-visible');\n        navShown = true;\n      }\n    }\n\n    // --------------------------------------------------------------------\n    // Project page transitions\n    // --------------------------------------------------------------------\n    var _rc = new THREE.Vector3();\n    function cardScreenRect(group) {\n      camera.updateMatrixWorld();\n      group.updateWorldMatrix(true, false);\n      var W = window.innerWidth, H = window.innerHeight;\n      var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n      var corners = [[-CARD_W/2,-CARD_H/2],[CARD_W/2,-CARD_H/2],[-CARD_W/2,CARD_H/2],[CARD_W/2,CARD_H/2]];\n      for (var i = 0; i < corners.length; i++) {\n        _rc.set(corners[i][0], corners[i][1], 0).applyMatrix4(group.matrixWorld).project(camera);\n        var sx = (_rc.x * 0.5 + 0.5) * W, sy = (-_rc.y * 0.5 + 0.5) * H;\n        minX = Math.min(minX, sx); maxX = Math.max(maxX, sx);\n        minY = Math.min(minY, sy); maxY = Math.max(maxY, sy);\n      }\n      return { left: minX, top: minY, width: maxX - minX, height: maxY - minY };\n    }\n\n    // Final right-column image rect, derived from the nav so paddings match exactly.\n    function computeImageRect() {\n      var links = root.querySelectorAll('.cla-nav-link');\n      var indexEl = links[0], contactEl = links[links.length - 1];\n      var ri = indexEl.getBoundingClientRect();\n      var rc = contactEl.getBoundingClientRect();\n      var padLeft = ri.left;\n      var rightPad = window.innerWidth - rc.right;\n      var navBottom = navEl.getBoundingClientRect().bottom;\n      var top = navBottom + rightPad;\n      var right = window.innerWidth - rightPad;\n      var bottom = window.innerHeight - rightPad;\n      var height = bottom - top;\n      var width = height * IMG_ASPECT;\n      var left = right - width;\n      return { left: left, top: top, width: width, height: height, padLeft: padLeft };\n    }\n\n    function computeMobileImageRect() {\n      var pad = navPad();\n      var navBottom = navEl.getBoundingClientRect().bottom;\n      var top = navBottom + 16;\n      return { left: pad, top: top, width: window.innerWidth - pad * 2, height: window.innerHeight * 0.55, pad: pad, navBottom: navBottom };\n    }\n\n    function positionProjectText(fin) {\n      projectText.style.left = fin.padLeft + 'px';\n      projectText.style.top = fin.top + 'px';\n      projectText.style.width = Math.max(280, fin.left - fin.padLeft - 48) + 'px';\n      projectText.style.height = Math.max(200, window.innerHeight - fin.top - 63) + 'px';\n    }\n\n    var EASE = 'cubic-bezier(0.65, 0, 0.35, 1)';\n    function imgMoveTransition(dur) {\n      return 'top ' + dur + 's ' + EASE + ', left ' + dur + 's ' + EASE +\n             ', width ' + dur + 's ' + EASE + ', height ' + dur + 's ' + EASE +\n             ', opacity 0.6s ease, box-shadow ' + dur + 's ease';\n    }\n\n    function enterProject(i) {\n      if (mode !== 'hero') return;\n      mode = 'enter';\n      activeCard = i;\n      dragging = false; canvas.classList.remove('cla-dragging');\n      hoveredIndex = -1;\n\n      startFade(0, 0.5);\n      pTitle.textContent = PROJECT_NAMES[i % PROJECT_NAMES.length];\n      revealEls.forEach(function (el) { el.style.transitionDelay = '0s'; el.classList.remove('cla-show'); });\n      projectEl.classList.add('cla-active');\n\n      projectImg.src = IMAGES[i];\n      if (projectImg.decode) projectImg.decode().catch(function () {});\n\n      setTimeout(function () {\n        if (mode !== 'enter') return;\n        var c = cards[i];\n        var rect = cardScreenRect(c.group);\n        c.startRect = rect;\n        var held = c.mat.opacity;\n        c.mesh.visible = false; c.shadow.visible = false;\n\n        projectImg.classList.remove('cla-m-inflow');\n        projectImg.style.position = 'fixed';\n        projectImg.style.transition = 'none';\n        projectImg.style.display = 'block';\n        projectImg.style.left = rect.left + 'px';\n        projectImg.style.top = rect.top + 'px';\n        projectImg.style.width = rect.width + 'px';\n        projectImg.style.height = rect.height + 'px';\n        projectImg.style.opacity = String(held);\n        projectImg.style.boxShadow = '0 12px 40px rgba(42, 37, 32, 0.15)';\n        void projectImg.offsetWidth;\n\n        var fin = isMobile ? computeMobileImageRect() : computeImageRect();\n        if (isMobile) {\n          ['left', 'top', 'width', 'height'].forEach(function (p) { projectText.style[p] = ''; });\n          projectEl.style.paddingTop = fin.top + 'px';\n          projectEl.scrollTop = 0;\n        } else {\n          positionProjectText(fin);\n        }\n        projectImg.style.transition = imgMoveTransition(1.0);\n        projectImg.style.left = fin.left + 'px';\n        projectImg.style.top = fin.top + 'px';\n        projectImg.style.width = fin.width + 'px';\n        projectImg.style.height = fin.height + 'px';\n        projectImg.style.opacity = '1';\n        projectImg.style.boxShadow = 'none';\n        mode = 'project-anim';\n\n        setTimeout(function () {\n          if (mode !== 'project-anim') return;\n          if (isMobile) {\n            ['position', 'left', 'top', 'width', 'height'].forEach(function (p) { projectImg.style[p] = ''; });\n            projectImg.classList.add('cla-m-inflow');\n          }\n          var delays = [0, 0.15, 0.30, 0.45];\n          revealEls.forEach(function (el, k) { el.style.transitionDelay = delays[k] + 's'; el.classList.add('cla-show'); });\n          mode = 'project';\n        }, 1000);\n      }, 600);\n    }\n\n    function returnToHero() {\n      if (mode !== 'project') return;\n      mode = 'exit';\n\n      var outDelays = [0.18, 0.12, 0.06, 0];\n      revealEls.forEach(function (el, k) { el.style.transitionDelay = outDelays[k] + 's'; el.classList.remove('cla-show'); });\n\n      setTimeout(function () {\n        if (mode !== 'exit') return;\n        var c = cards[activeCard];\n        var rect = c.startRect || cardScreenRect(c.group);\n\n        if (isMobile) {\n          var r = projectImg.getBoundingClientRect();\n          projectImg.classList.remove('cla-m-inflow');\n          projectImg.style.position = 'fixed';\n          projectImg.style.transition = 'none';\n          projectImg.style.left = r.left + 'px';\n          projectImg.style.top = r.top + 'px';\n          projectImg.style.width = r.width + 'px';\n          projectImg.style.height = r.height + 'px';\n          projectImg.style.boxShadow = '0 12px 40px rgba(42, 37, 32, 0.15)';\n          void projectImg.offsetWidth;\n        }\n\n        projectImg.style.transition = imgMoveTransition(0.8);\n        projectImg.style.left = rect.left + 'px';\n        projectImg.style.top = rect.top + 'px';\n        projectImg.style.width = rect.width + 'px';\n        projectImg.style.height = rect.height + 'px';\n        projectImg.style.boxShadow = '0 12px 40px rgba(42, 37, 32, 0.15)';\n\n        setTimeout(function () {\n          if (mode !== 'exit') return;\n          projectImg.style.display = 'none';\n          ['position', 'left', 'top', 'width', 'height'].forEach(function (p) { projectImg.style[p] = ''; });\n          projectImg.classList.remove('cla-m-inflow');\n          projectEl.style.paddingTop = '';\n          projectEl.scrollTop = 0;\n          c.mesh.visible = true; c.shadow.visible = true;\n          projectEl.classList.remove('cla-active');\n          activeCard = -1;\n          startFade(1, 0.5);\n          mode = 'hero';\n        }, 800);\n      }, 400);\n    }\n\n    // Desktop INDEX returns to the sphere; Escape does too.\n    var navLinks = root.querySelectorAll('.cla-nav-link');\n    if (navLinks.length) {\n      navLinks[0].addEventListener('click', function (e) {\n        e.preventDefault();\n        if (mode === 'project') returnToHero();\n      });\n    }\n    window.addEventListener('keydown', function (e) {\n      if (e.key === 'Escape' && mode === 'project') returnToHero();\n    });\n\n    // ---- Mobile menu ----\n    function openMenu() {\n      mobileMenuEl.classList.add('cla-open');\n      hamburgerEl.classList.add('cla-open');\n      hamburgerEl.setAttribute('aria-label', 'Close menu');\n    }\n    function closeMenu() {\n      mobileMenuEl.classList.remove('cla-open');\n      hamburgerEl.classList.remove('cla-open');\n      hamburgerEl.setAttribute('aria-label', 'Open menu');\n    }\n    if (hamburgerEl) {\n      hamburgerEl.addEventListener('click', function () {\n        mobileMenuEl.classList.contains('cla-open') ? closeMenu() : openMenu();\n      });\n    }\n    root.querySelectorAll('#cla-mobile-menu .cla-m-link').forEach(function (link) {\n      link.addEventListener('click', function (e) {\n        e.preventDefault();\n        closeMenu();\n        if (link.dataset.nav === 'index' && mode === 'project') returnToHero();\n      });\n    });\n    var brandMobile = root.querySelector('.cla-nav-brand-mobile');\n    if (brandMobile) {\n      brandMobile.addEventListener('click', function () {\n        if (mode === 'project') returnToHero();\n      });\n    }\n\n    // Keep the project layout aligned on resize while it is showing.\n    window.addEventListener('resize', function () {\n      if (mode !== 'project') return;\n      if (isMobile) {\n        projectEl.style.paddingTop = computeMobileImageRect().top + 'px';\n      } else {\n        var fin = computeImageRect();\n        projectImg.style.transition = 'none';\n        projectImg.style.left = fin.left + 'px';\n        projectImg.style.top = fin.top + 'px';\n        projectImg.style.width = fin.width + 'px';\n        projectImg.style.height = fin.height + 'px';\n        positionProjectText(fin);\n      }\n    });\n\n    function animate() {\n      requestAnimationFrame(animate);\n      var dt = Math.min(clock.getDelta(), 0.05);\n      step(dt);\n      renderer.render(scene, camera);\n    }\n    animate();\n  }\n})();\n"
            },
            "label": "Clares engine (canvas + CSS + Three.js)"
        },
        {
            "id": "clanav",
            "name": "block",
            "parent": "clasec",
            "children": [
                "clanavin",
                "cladiv"
            ],
            "settings": {
                "_cssId": "cla-nav"
            },
            "label": "Nav"
        },
        {
            "id": "clanavin",
            "name": "block",
            "parent": "clanav",
            "children": [
                "clagl",
                "clabrand",
                "clagr",
                "clabrandm",
                "clahamb"
            ],
            "settings": {
                "_cssClasses": "cla-nav-inner"
            }
        },
        {
            "id": "clagl",
            "name": "block",
            "parent": "clanavin",
            "children": [
                "clan1",
                "clan2"
            ],
            "settings": {
                "_cssClasses": "cla-nav-group cla-desktop"
            },
            "label": "Nav left"
        },
        {
            "id": "clan1",
            "name": "text-basic",
            "parent": "clagl",
            "children": [],
            "settings": {
                "text": "Index",
                "tag": "span",
                "_cssClasses": "cla-nav-link"
            }
        },
        {
            "id": "clan2",
            "name": "text-basic",
            "parent": "clagl",
            "children": [],
            "settings": {
                "text": "Journal",
                "tag": "span",
                "_cssClasses": "cla-nav-link"
            }
        },
        {
            "id": "clabrand",
            "name": "text-basic",
            "parent": "clanavin",
            "children": [],
            "settings": {
                "text": "Clares",
                "tag": "div",
                "_cssClasses": "cla-nav-brand cla-desktop"
            },
            "label": "Brand"
        },
        {
            "id": "clagr",
            "name": "block",
            "parent": "clanavin",
            "children": [
                "clan3",
                "clan4"
            ],
            "settings": {
                "_cssClasses": "cla-nav-group cla-desktop"
            },
            "label": "Nav right"
        },
        {
            "id": "clan3",
            "name": "text-basic",
            "parent": "clagr",
            "children": [],
            "settings": {
                "text": "Info",
                "tag": "span",
                "_cssClasses": "cla-nav-link"
            }
        },
        {
            "id": "clan4",
            "name": "text-basic",
            "parent": "clagr",
            "children": [],
            "settings": {
                "text": "Contact",
                "tag": "span",
                "_cssClasses": "cla-nav-link"
            }
        },
        {
            "id": "clabrandm",
            "name": "text-basic",
            "parent": "clanavin",
            "children": [],
            "settings": {
                "text": "Clares",
                "tag": "span",
                "_cssClasses": "cla-nav-brand-mobile"
            },
            "label": "Brand (mobile)"
        },
        {
            "id": "clahamb",
            "name": "code",
            "parent": "clanavin",
            "children": [],
            "settings": {
                "code": "<button id=\"cla-hamburger\" type=\"button\" aria-label=\"Open menu\"><span></span><span></span><span></span></button>",
                "language": "html",
                "executeCode": true,
                "noRoot": true,
                "signature": "2d48239b034ab3186ff8a359181c8383"
            },
            "label": "Hamburger (markup)"
        },
        {
            "id": "cladiv",
            "name": "block",
            "parent": "clanav",
            "children": [],
            "settings": {
                "_cssClasses": "cla-nav-divider"
            },
            "label": "Divider"
        },
        {
            "id": "clamenu",
            "name": "block",
            "parent": "clasec",
            "children": [
                "clam1",
                "clam2",
                "clam3",
                "clam4"
            ],
            "settings": {
                "_cssId": "cla-mobile-menu"
            },
            "label": "Mobile menu"
        },
        {
            "id": "clam1",
            "name": "text-basic",
            "parent": "clamenu",
            "children": [],
            "settings": {
                "text": "Index",
                "tag": "span",
                "_cssClasses": "cla-m-link",
                "_attributes": [
                    {
                        "id": "claattr1",
                        "name": "data-nav",
                        "value": "index"
                    }
                ]
            }
        },
        {
            "id": "clam2",
            "name": "text-basic",
            "parent": "clamenu",
            "children": [],
            "settings": {
                "text": "Journal",
                "tag": "span",
                "_cssClasses": "cla-m-link"
            }
        },
        {
            "id": "clam3",
            "name": "text-basic",
            "parent": "clamenu",
            "children": [],
            "settings": {
                "text": "Info",
                "tag": "span",
                "_cssClasses": "cla-m-link"
            }
        },
        {
            "id": "clam4",
            "name": "text-basic",
            "parent": "clamenu",
            "children": [],
            "settings": {
                "text": "Contact",
                "tag": "span",
                "_cssClasses": "cla-m-link"
            }
        },
        {
            "id": "claproj",
            "name": "block",
            "parent": "clasec",
            "children": [
                "claimg",
                "clatext"
            ],
            "settings": {
                "_cssId": "cla-project"
            },
            "label": "Project detail"
        },
        {
            "id": "claimg",
            "name": "code",
            "parent": "claproj",
            "children": [],
            "settings": {
                "code": "<img id=\"cla-project-img\" alt=\"\">",
                "language": "html",
                "executeCode": true,
                "noRoot": true,
                "signature": "56bd23b3c750eb3210860ea8b99db60d"
            },
            "label": "Project image (markup)"
        },
        {
            "id": "clatext",
            "name": "block",
            "parent": "claproj",
            "children": [
                "clatitle",
                "clabody"
            ],
            "settings": {
                "_cssId": "cla-project-text"
            },
            "label": "Project text"
        },
        {
            "id": "clatitle",
            "name": "heading",
            "parent": "clatext",
            "children": [],
            "settings": {
                "text": "The Merge",
                "tag": "h1",
                "_cssId": "cla-p-title",
                "_cssClasses": "cla-reveal"
            },
            "label": "Title (set by JS on click)"
        },
        {
            "id": "clabody",
            "name": "block",
            "parent": "clatext",
            "children": [
                "claeye",
                "clap1",
                "clap2"
            ],
            "settings": {
                "_cssId": "cla-p-body"
            }
        },
        {
            "id": "claeye",
            "name": "text-basic",
            "parent": "clabody",
            "children": [],
            "settings": {
                "text": "The Concept",
                "tag": "div",
                "_cssId": "cla-p-eyebrow",
                "_cssClasses": "cla-reveal"
            }
        },
        {
            "id": "clap1",
            "name": "text-basic",
            "parent": "clabody",
            "children": [],
            "settings": {
                "text": "A quiet study in texture and light, captured across the rural estates of southern France. This series explores the interplay between architecture, nature, and the human form — finding beauty in the spaces where the cultivated meets the untamed.",
                "tag": "p",
                "_cssId": "cla-p-1",
                "_cssClasses": "cla-reveal"
            }
        },
        {
            "id": "clap2",
            "name": "text-basic",
            "parent": "clabody",
            "children": [],
            "settings": {
                "text": "Shot over three weeks during late autumn, the warm natural palette and soft directional light give each frame a sense of stillness. The project is a meditation on place, presence, and the slow passage of time through forgotten landscapes.",
                "tag": "p",
                "_cssId": "cla-p-2",
                "_cssClasses": "cla-reveal"
            }
        }
    ],
    "media": [
        {
            "attachment_id": 448,
            "file": "clares-01.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/clares-01.jpg",
            "mime": "image/jpeg",
            "bytes": 108355
        },
        {
            "attachment_id": 449,
            "file": "clares-02.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/clares-02.jpg",
            "mime": "image/jpeg",
            "bytes": 77220
        },
        {
            "attachment_id": 450,
            "file": "clares-03.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/clares-03.jpg",
            "mime": "image/jpeg",
            "bytes": 137309
        },
        {
            "attachment_id": 451,
            "file": "clares-04.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/clares-04.jpg",
            "mime": "image/jpeg",
            "bytes": 97725
        },
        {
            "attachment_id": 452,
            "file": "clares-05.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/clares-05.jpg",
            "mime": "image/jpeg",
            "bytes": 94239
        },
        {
            "attachment_id": 453,
            "file": "clares-06.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/clares-06.jpg",
            "mime": "image/jpeg",
            "bytes": 81145
        },
        {
            "attachment_id": 454,
            "file": "clares-07.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/clares-07.jpg",
            "mime": "image/jpeg",
            "bytes": 97408
        },
        {
            "attachment_id": 455,
            "file": "clares-08.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/clares-08.jpg",
            "mime": "image/jpeg",
            "bytes": 96941
        },
        {
            "attachment_id": 456,
            "file": "clares-09.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/clares-09.jpg",
            "mime": "image/jpeg",
            "bytes": 78773
        },
        {
            "attachment_id": 457,
            "file": "clares-10.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/clares-10.jpg",
            "mime": "image/jpeg",
            "bytes": 107675
        },
        {
            "attachment_id": 458,
            "file": "clares-11.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/clares-11.jpg",
            "mime": "image/jpeg",
            "bytes": 88113
        },
        {
            "attachment_id": 459,
            "file": "clares-12.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/clares-12.jpg",
            "mime": "image/jpeg",
            "bytes": 105253
        },
        {
            "attachment_id": 463,
            "file": "thumb-clares.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/thumb-clares.jpg",
            "mime": "image/jpeg",
            "bytes": 89132
        }
    ],
    "media_note": "The pages build these URLs in JS, so they do not appear literally in the element tree. Re-upload every file below to the target site and point the BASE / src values at the new uploads path."
}