{
  "demo": true,
  "settings": {
    "merge": {
      "companyName": "Neon Forge",
      "tagline": "We build games and the tools that ship them."
    }
  },
  "data_tables": [
    {
      "key": "contact_submissions",
      "name": "Contact Enquiries",
      "description": "Seeded contact-form submissions for the Neon Forge demo. A submission becomes a row here and emails the site admins.",
      "columns": [
        { "key": "name", "label": "Name", "type": "text", "required": true },
        { "key": "email", "label": "Email", "type": "email", "required": true },
        { "key": "studio", "label": "Studio", "type": "text", "required": false },
        { "key": "message", "label": "Message", "type": "text", "required": true }
      ]
    }
  ],
  "pages": [
    {
      "slug": "/",
      "title": "Neon Forge",
      "template_key": "landing",
      "blocks": [
        {
          "block_key": "hero",
          "title": "Hero",
          "slot": "main",
          "sort_order": 1,
          "content": {
            "template": "centered",
            "minHeight": "screen",
            "eyebrow": "Build 2.049",
            "headline": "Ship the impossible",
            "subheadline": "A game studio and the tools that ship it.",
            "description": "We build a rendering engine, live ops, and a build farm that never sleeps. Then we make games on top of it.",
            "ctaPrimary": { "text": "Get the SDK", "url": "/contact" },
            "ctaSecondary": { "text": "Read the devlog", "url": "/blog" }
          }
        },
        {
          "block_key": "feature-grid",
          "title": "What we ship",
          "slot": "main",
          "sort_order": 2,
          "content": {
            "eyebrow": "The stack",
            "heading": "One pipeline, end to end",
            "intro": "Engine, tooling, and live service. Built in house, shipped together.",
            "layout": "card",
            "columns": 3,
            "items": [
              { "icon": "lucide:cpu", "title": "Render engine", "text": "Neon lighting, volumetric fog, and a deferred path tuned for night maps." },
              { "icon": "lucide:terminal", "title": "Build farm", "text": "Shader caching across branches. Green builds in minutes, not hours." },
              { "icon": "lucide:radio", "title": "Live ops", "text": "Ship events, patch balance, and roll back without a redeploy." }
            ]
          }
        },
        {
          "block_key": "cyberpunk:roster_grid",
          "title": "The crew",
          "slot": "main",
          "sort_order": 3,
          "content": {
            "eyebrow": "The crew",
            "heading": "Who ships this",
            "intro": "A small team that builds the engine, the game, and the pipeline in between.",
            "members": [
              { "name": "Nova Reyes", "handle": "@nova", "role": "Engine lead", "status": true },
              { "name": "Kai Okafor", "handle": "@kaisdf", "role": "Graphics", "status": true },
              { "name": "Sable Lin", "handle": "@sable", "role": "Live ops", "status": false },
              { "name": "Juno Vale", "handle": "@juno", "role": "Gameplay", "status": true },
              { "name": "Rin Castellano", "handle": "@rin", "role": "Tools", "status": true },
              { "name": "Ash Mori", "handle": "@ash", "role": "Netcode", "status": false }
            ]
          }
        },
        {
          "block_key": "cyberpunk:patch_notes",
          "title": "Patch notes",
          "slot": "main",
          "sort_order": 4,
          "content": {
            "heading": "Patch notes",
            "intro": "What changed, build by build.",
            "entries": [
              { "version": "v2.049.0", "date": "2049.04.01", "tag": "FEATURE", "items": [ { "text": "Volumetric neon fog on all night maps." }, { "text": "Build farm caches shaders across branches." } ] },
              { "version": "v2.048.3", "date": "2049.03.18", "tag": "FIX", "items": [ { "text": "Fixed a stall when hot-reloading materials." }, { "text": "Reduced desync on high-latency links." } ] },
              { "version": "v2.048.0", "date": "2049.03.02", "tag": "BALANCE", "items": [ { "text": "Tuned dash cooldown from 4s to 3.2s." } ] }
            ]
          }
        },
        {
          "block_key": "cta",
          "title": "CTA",
          "slot": "main",
          "sort_order": 5,
          "content": {
            "background": "band",
            "heading": "Start building tonight",
            "text": "Grab the SDK and ship your first build before the coffee runs out.",
            "primaryLabel": "Get the SDK",
            "primaryUrl": "/contact"
          }
        }
      ]
    },
    {
      "slug": "/about",
      "title": "The studio",
      "template_key": "default",
      "blocks": [
        { "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "The studio", "level": 1 } },
        { "block_key": "rich-text", "title": "Story", "slot": "main", "sort_order": 2, "content": { "html": "<p>Neon Forge started as a build tool for a game that shipped two years late. The game came and went. The tool outlived it. We kept sharpening it in the dark between projects, then built more games to prove it could take the punishment.</p><p>Now we run both halves at once. The engine and the live service on one side, the games that stress-test them on the other. Nothing ships to a player before it has shipped to us first, on the same pipeline, under the same load.</p><p>We are a small crew by design. Fourteen people who would rather own the whole stack than manage a hundred who own slices of it. Everyone here writes code that runs in production, and everyone here has been paged at 3am by something they wrote. That keeps us honest.</p><p>The look of this place is not an accident either. Neon on black, mono readouts, a little glitch in the type. It is how our tools have always looked on the wall of screens in the build room, and we never saw a reason to dress it up for the website.</p>" } },
        { "block_key": "cyberpunk:code_neon", "title": "Quickstart", "slot": "main", "sort_order": 3, "content": { "language": "bash", "code": "npm install @neon-forge/sdk\nnpx neon-forge init my-game\nnpx neon-forge ship --stage prod", "copy": "on" } },
        { "block_key": "divider", "title": "Divider", "slot": "main", "sort_order": 4, "content": { "variant": "line", "weight": "medium" } },
        { "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 5, "content": { "style": "pull", "quote": "Build the tool. Ship the game. Run both in production before you trust either.", "attribution": "Nova Reyes", "role": "Engine lead, Neon Forge" } }
      ]
    },
    {
      "slug": "/blog",
      "title": "Devlog",
      "template_key": "blog-index",
      "blocks": [
        { "block_key": "card", "title": "Post: Rain", "slot": "main", "sort_order": 1, "content": { "layout": "vertical", "title": "Making it rain", "text": "<p>How we built a dependency-free digital-rain effect that stays smooth at 60 fps.</p>", "link": "/blog/making-it-rain", "linkLabel": "Read on" } },
        { "block_key": "card", "title": "Post: Shaders", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "title": "Caching shaders across branches", "text": "<p>Green builds in minutes. What we changed in the build farm and why.</p>", "link": "/blog/caching-shaders", "linkLabel": "Read on" } },
        { "block_key": "card", "title": "Post: Netcode", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "title": "Netcode on bad links", "text": "<p>Cutting desync on 200ms connections without rewriting the sim.</p>", "link": "/blog/netcode-bad-links", "linkLabel": "Read on" } }
      ]
    },
    {
      "slug": "/blog/making-it-rain",
      "title": "Making it rain",
      "template_key": "article",
      "parent_slug": "/blog",
      "blocks": [
        { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Making it rain", "level": 1 } },
        { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>The hero on our home page runs a digital-rain canvas. No libraries, no sprite sheets, no video. Just a few dozen lines that draw falling glyphs onto a single <code>&lt;canvas&gt;</code> and never touch the network again.</p><p>We wanted the front door of the studio to feel like the wall of screens in our build room: green-on-black readouts, columns of characters sliding down, the faint sense that something is compiling behind the glass. A GIF would have looked dead. A video would have cost a megabyte and stuttered on the first scroll. So we drew it live.</p><p>The whole effect is one idea repeated: divide the width into columns, give each column a <em>drop</em> that falls one row per tick, and paint a random glyph wherever that drop currently sits. When a drop runs off the bottom, roll a die and occasionally send it back to the top. That randomness is what stops the rain from marching in lockstep.</p><p>The trick that makes it read as rain instead of confetti is the fade. Every frame we do not clear the canvas. We paint a translucent rectangle of the background colour over the whole thing, just barely opaque, then draw the new glyphs on top. Yesterday's characters do not vanish; they dim, one step per frame, leaving a comet tail behind each drop. Clear the canvas outright and you get sharp dots with no soul. Fade it and you get rain.</p><p>Colour comes from the theme, not from a hex code baked into the script. On boot we read the CSS custom properties off the document root, the primary and accent channels, and pick one of them at random per glyph. Retheme the site and the rain retints itself for free. That single decision is why the effect survives every palette we throw at it.</p><p>Performance was the part we actually sweated. A canvas that repaints as fast as the browser allows will happily cook a laptop fan for a background nobody is looking at. So we cap the loop at roughly eighteen frames a second. Rain does not need sixty. The <code>requestAnimationFrame</code> callback fires as often as the display wants, but we bail early unless enough milliseconds have passed since the last real paint. Half the frames do nothing but check a clock.</p><p>We also refused to let it run when it should not. The canvas only wakes up once it scrolls into view, wired through an <code>IntersectionObserver</code>, so a page that never shows the hero never spends a cycle on it. If the visitor has asked for reduced motion, we skip the animation entirely and let a static neon gradient stand in. The rain is a flourish, never a requirement.</p><p>Sizing is handled the boring, correct way. A <code>ResizeObserver</code> watches the hero and rebuilds the column list whenever the box changes, multiplying the backing store by the device pixel ratio so the glyphs stay crisp on retina panels without rendering four times the pixels on a phone. Resize the window and the rain reflows mid-fall.</p><p>None of this is clever, and that is the point. The failure mode of a signature animation is a dependency you have to keep patched and a bundle that grows every time someone adds a feature. This one has no dependencies, ships as inline script inside the theme, and has not changed since the day it worked. It will still run when the framework it was born next to is three major versions gone.</p><p>If you are building your own, start with the fade and the frame cap. Everything else is decoration. Get a single column falling with a trailing tail at a sane frame rate, and you already have the effect. The rest is just more columns.</p>" } }
      ]
    },
    {
      "slug": "/blog/caching-shaders",
      "title": "Caching shaders across branches",
      "template_key": "article",
      "parent_slug": "/blog",
      "blocks": [
        { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Caching shaders across branches", "level": 1 } },
        { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>Every branch used to recompile the whole shader set from scratch. That is minutes per build, times every push, times every engineer. On a busy day the build farm spent more time on shaders nobody had touched than on the code that changed.</p><p>The insight was embarrassingly simple once we saw it. A shader does not know what branch it lives on. If the source bytes are identical, the compiled output is identical. So we keyed the cache on a hash of the shader source and its compile flags, not on the branch name.</p><p>Now two branches that share a shader share the compile. The first branch to build a given shader pays for it; everyone after pulls the artifact from the cache in milliseconds. A feature branch that only touches gameplay code inherits every shader the mainline already compiled.</p><p>Green builds went from tens of minutes back down to single digits. The farm stopped melting under merge storms. Nobody has thought about shader compile times since, which is the highest compliment we can pay a piece of infrastructure.</p>" } }
      ]
    },
    {
      "slug": "/blog/netcode-bad-links",
      "title": "Netcode on bad links",
      "template_key": "article",
      "parent_slug": "/blog",
      "blocks": [
        { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Netcode on bad links", "level": 1 } },
        { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>Players on slow connections kept desyncing, and the complaints all rhymed: the world would freeze for a beat, then snap somewhere they did not expect. Rewriting the simulation to be network-native was a six-month job we did not have. We needed a fix that lived on top of the sim we already trusted.</p><p>So we added client prediction with a short rollback window. The client stops waiting for the server to bless every input and starts guessing. Press dash, and you dash now, locally, immediately. The input also goes to the server, which is still the only authority that counts.</p><p>When the server's version of events comes back, the client compares it against what it predicted. Most of the time they agree and nothing happens. When they disagree, the client quietly rewinds the last few frames, replays them with the corrected state, and rolls forward again, all inside a window short enough that the eye never catches the seam.</p><p>The result is a game that feels instant on a bad link and stays authoritative on the server. Desync on two-hundred-millisecond connections dropped to something players stopped reporting. The simulation never changed. We just stopped making it wait.</p>" } }
      ]
    },
    {
      "slug": "/contact",
      "title": "Contact",
      "template_key": "contact",
      "blocks": [
        {
          "block_key": "contact-form",
          "title": "Contact form",
          "slot": "main",
          "sort_order": 1,
          "content": {
            "heading": "Request access",
            "description": "Tell us what you are building. We reply within one business day.",
            "submitLabel": "Request access",
            "successMessage": "Message received. We will reply within one business day.",
            "fields": [
              { "name": "name", "label": "Your name", "type": "text", "required": true, "width": "half" },
              { "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
              { "name": "studio", "label": "Studio", "type": "text", "required": false, "width": "full" },
              { "name": "message", "label": "What are you building", "type": "textarea", "required": true, "width": "full" }
            ],
            "formConfig": { "enabled": true, "captchaEnabled": false, "targetTable": "contact_submissions" }
          }
        },
        {
          "block_key": "contact-card",
          "title": "Reach us",
          "slot": "main",
          "sort_order": 2,
          "content": {
            "heading": "Other ways in",
            "columns": 3,
            "channels": [
              { "icon": "lucide:mail", "label": "Email", "value": "crew@neonforge.dev", "link": "mailto:crew@neonforge.dev" },
              { "icon": "simple-icons:github", "label": "Source", "value": "github.com/neon-forge", "link": "https://github.com/neon-forge" },
              { "icon": "lucide:message-square", "label": "Discord", "value": "Join the build channel", "link": "#" }
            ]
          }
        }
      ]
    },
    {
      "slug": "/login",
      "title": "Sign in",
      "template_key": "auth",
      "blocks": [
        { "block_key": "auth-form", "title": "Sign in", "slot": "main", "sort_order": 1, "content": { "default_tab": "login", "show_social": false } }
      ]
    }
  ],
  "menu_items": [
    { "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
    { "menu": "main", "label": "Studio", "page_slug": "/about", "sort_order": 2 },
    { "menu": "main", "label": "Devlog", "page_slug": "/blog", "sort_order": 3 },
    { "menu": "main", "label": "Contact", "page_slug": "/contact", "sort_order": 4 }
  ]
}
