foxygit / Hush Log in
commit 3ec890af0b7bbd284534604eea2164ebd71f109a
Author:     MrJensK <jens.se@icloud.com>
AuthorDate: Mon Aug 24 15:59:59 2026 +0200
Commit:     MrJensK <jens.se@icloud.com>
CommitDate: Mon Aug 24 16:00:09 2026 +0200

    Initial commit: Hush markdown editor

    WYSIWYG markdown editor in C using Clay + raylib, with undo/redo,
    text selection, a status bar, and a config file for theming.

    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---
 .gitignore                                  |    6 +
 CMakeLists.txt                              |   77 +
 README.md                                   |   86 +
 assets/fonts/DejaVuSans-Bold.ttf            |  Bin 0 -> 708920 bytes
 assets/fonts/DejaVuSans-BoldOblique.ttf     |  Bin 0 -> 645600 bytes
 assets/fonts/DejaVuSans-Oblique.ttf         |  Bin 0 -> 637648 bytes
 assets/fonts/DejaVuSans.ttf                 |  Bin 0 -> 759720 bytes
 assets/fonts/DejaVuSansMono-Bold.ttf        |  Bin 0 -> 334268 bytes
 assets/fonts/DejaVuSansMono-BoldOblique.ttf |  Bin 0 -> 254960 bytes
 assets/fonts/DejaVuSansMono-Oblique.ttf     |  Bin 0 -> 253448 bytes
 assets/fonts/DejaVuSansMono.ttf             |  Bin 0 -> 343140 bytes
 example.md                                  |   23 +
 src/app_mode.h                              |   21 +
 src/clay_impl.c                             |    3 +
 src/config.c                                |  160 +
 src/config.h                                |   27 +
 src/document.c                              |   55 +
 src/document.h                              |   48 +
 src/editor.c                                |  427 +++
 src/editor.h                                |   63 +
 src/fonts.c                                 |   61 +
 src/fonts.h                                 |   28 +
 src/hittest.c                               |  176 +
 src/hittest.h                               |   38 +
 src/inline_parse.c                          |  117 +
 src/inline_parse.h                          |   32 +
 src/layout_cache.c                          |   55 +
 src/layout_cache.h                          |   38 +
 src/main.c                                  |  183 +
 src/markdown_io.c                           |  167 +
 src/markdown_io.h                           |   26 +
 src/render.c                                |  507 +++
 src/render.h                                |   29 +
 src/strbuf.c                                |   59 +
 src/strbuf.h                                |   20 +
 src/test_main.c                             |  439 +++
 src/theme.c                                 |    9 +
 src/theme.h                                 |   57 +
 src/undo.c                                  |   82 +
 src/undo.h                                  |   47 +
 src/utf8.h                                  |   24 +
 src/wrap.c                                  |  147 +
 src/wrap.h                                  |   38 +
 vendor/clay.h                               | 5058 +++++++++++++++++++++++++++
 vendor/clay_renderer_raylib.c               |  321 ++
 45 files changed, 8754 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3132455
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+build/
+build_verify/
+*.o
+compile_commands.json
+test_numbered.md
+test_roundtrip.md
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..3cd53c0
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,77 @@
+cmake_minimum_required(VERSION 3.15)
+project(hush C)
+
+set(CMAKE_C_STANDARD 11)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+if(NOT CMAKE_BUILD_TYPE)
+    set(CMAKE_BUILD_TYPE Release)
+endif()
+
+include(FetchContent)
+
+set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
+set(BUILD_GAMES OFF CACHE BOOL "" FORCE)
+set(SUPPORT_MODULE_RAUDIO OFF CACHE BOOL "" FORCE)
+
+FetchContent_Declare(
+    raylib
+    GIT_REPOSITORY https://github.com/raysan5/raylib.git
+    GIT_TAG 5.5
+    GIT_SHALLOW TRUE
+)
+FetchContent_MakeAvailable(raylib)
+
+add_executable(hush
+    src/clay_impl.c
+    src/strbuf.c
+    src/document.c
+    src/inline_parse.c
+    src/wrap.c
+    src/fonts.c
+    src/layout_cache.c
+    src/hittest.c
+    src/theme.c
+    src/undo.c
+    src/config.c
+    src/editor.c
+    src/render.c
+    src/markdown_io.c
+    src/main.c
+)
+
+target_include_directories(hush PRIVATE vendor)
+target_link_libraries(hush PRIVATE raylib m)
+target_compile_definitions(hush PRIVATE ASSETS_DIR="${CMAKE_SOURCE_DIR}/assets")
+
+if (UNIX AND NOT APPLE)
+    target_link_libraries(hush PRIVATE dl pthread)
+endif()
+
+target_compile_options(hush PRIVATE -Wall -Wextra -Wno-unused-parameter)
+
+# Headless logic test: no window/GPU context is created, so it can run in CI or over SSH.
+add_executable(hush_test
+    src/clay_impl.c
+    src/strbuf.c
+    src/document.c
+    src/inline_parse.c
+    src/wrap.c
+    src/fonts.c
+    src/layout_cache.c
+    src/hittest.c
+    src/theme.c
+    src/undo.c
+    src/config.c
+    src/editor.c
+    src/render.c
+    src/markdown_io.c
+    src/test_main.c
+)
+target_include_directories(hush_test PRIVATE vendor)
+target_link_libraries(hush_test PRIVATE raylib m)
+target_compile_definitions(hush_test PRIVATE ASSETS_DIR="${CMAKE_SOURCE_DIR}/assets")
+if (UNIX AND NOT APPLE)
+    target_link_libraries(hush_test PRIVATE dl pthread)
+endif()
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6d0c817
--- /dev/null
+++ b/README.md
@@ -0,0 +1,86 @@
+# Hush
+
+A small WYSIWYG markdown editor, built in C with [Clay](https://github.com/nicbarker/clay) and [raylib](https://www.raylib.com/).
+
+Markdown syntax you type (`# `, `**bold**`, `` `code` ``, `- `, `1. `, `> `, `` ``` ``, `[text](url)`) is rendered as styled text as you go, rather than shown as raw markup — the underlying markup only reappears around a span while your cursor is inside it.
+
+## Features
+
+- Headings, bold, italic, inline code, links, block quotes, code blocks, bullet and numbered lists
+- Text selection (Shift+arrows or click-drag) with copy/paste
+- Undo/redo
+- A status bar showing the document name, size, and line count
+- A config file for font, text color, and background color
+- Full Swedish/Latin-1 character support (åäö, etc.)
+
+## Building
+
+Requires CMake 3.15+, a C11 compiler, and a network connection on first build (raylib is fetched automatically via CMake's `FetchContent`).
+
+```sh
+cmake -B build
+cmake --build build
+```
+
+This produces two binaries in `build/`:
+
+- `hush` — the editor itself
+- `hush_test` — a headless test suite for the document/editor logic (no window or GPU context needed, so it can run over SSH or in CI)
+
+## Usage
+
+```sh
+./build/hush [file.md]
+```
+
+Run without an argument to start with an empty, untitled document.
+
+### Keyboard shortcuts
+
+| Shortcut | Action |
+|---|---|
+| Arrow keys | Move the cursor |
+| Shift + arrow keys | Extend the selection |
+| Click + drag | Select text with the mouse |
+| Ctrl+S | Save |
+| Ctrl+C / Ctrl+V | Copy / paste |
+| Ctrl+Z / Ctrl+Shift+Z | Undo / redo |
+| Ctrl+X | Quit (prompts to save if there are unsaved changes) |
+| F1 (or Ctrl+/) | Show this shortcut list |
+| Enter | New line / new block |
+| Backspace / Delete | Delete a character (merges/demotes blocks at boundaries) |
+| Home / End | Start / end of the current line |
+
+The window's close button goes through the same unsaved-changes prompt as Ctrl+X.
+
+## Configuration
+
+On first run, Hush creates a config file at `$XDG_CONFIG_HOME/hush/config` (or `~/.config/hush/config` if that variable isn't set), with every available key present but commented out. Uncomment and edit a line to change it; unknown or malformed lines are ignored.
+
+```
+text_color = #1F2328
+bg_color = #FDFDFB
+
+font_sans_regular = /path/to/font.ttf
+font_sans_bold = /path/to/font-bold.ttf
+font_sans_italic = /path/to/font-italic.ttf
+font_sans_bold_italic = /path/to/font-bold-italic.ttf
+font_mono_regular = /path/to/mono.ttf
+font_mono_bold = /path/to/mono-bold.ttf
+font_mono_italic = /path/to/mono-italic.ttf
+font_mono_bold_italic = /path/to/mono-bold-italic.ttf
+```
+
+Font paths are optional per style; any left unset fall back to the bundled DejaVu fonts. `text_color` and `bg_color` accept `#RRGGBB` (the `#` is optional).
+
+## Testing
+
+```sh
+./build/hush_test
+```
+
+Runs the full logic test suite (document editing, markdown round-tripping, undo/redo, selection, config parsing, ...) and prints a pass/fail summary.
+
+## Acknowledgments
+
+Built with [Clay](https://github.com/nicbarker/clay) and [raylib](https://www.raylib.com/), and bundles the [DejaVu fonts](https://dejavu-fonts.github.io/).
diff --git a/assets/fonts/DejaVuSans-Bold.ttf b/assets/fonts/DejaVuSans-Bold.ttf
new file mode 100644
index 0000000..0a7fb3f
Binary files /dev/null and b/assets/fonts/DejaVuSans-Bold.ttf differ
diff --git a/assets/fonts/DejaVuSans-BoldOblique.ttf b/assets/fonts/DejaVuSans-BoldOblique.ttf
new file mode 100644
index 0000000..e68b9ba
Binary files /dev/null and b/assets/fonts/DejaVuSans-BoldOblique.ttf differ
diff --git a/assets/fonts/DejaVuSans-Oblique.ttf b/assets/fonts/DejaVuSans-Oblique.ttf
new file mode 100644
index 0000000..59c3198
Binary files /dev/null and b/assets/fonts/DejaVuSans-Oblique.ttf differ
diff --git a/assets/fonts/DejaVuSans.ttf b/assets/fonts/DejaVuSans.ttf
new file mode 100644
index 0000000..1728e9e
Binary files /dev/null and b/assets/fonts/DejaVuSans.ttf differ
diff --git a/assets/fonts/DejaVuSansMono-Bold.ttf b/assets/fonts/DejaVuSansMono-Bold.ttf
new file mode 100644
index 0000000..d4fc522
Binary files /dev/null and b/assets/fonts/DejaVuSansMono-Bold.ttf differ
diff --git a/assets/fonts/DejaVuSansMono-BoldOblique.ttf b/assets/fonts/DejaVuSansMono-BoldOblique.ttf
new file mode 100644
index 0000000..39cdaa5
Binary files /dev/null and b/assets/fonts/DejaVuSansMono-BoldOblique.ttf differ
diff --git a/assets/fonts/DejaVuSansMono-Oblique.ttf b/assets/fonts/DejaVuSansMono-Oblique.ttf
new file mode 100644
index 0000000..47b50f4
Binary files /dev/null and b/assets/fonts/DejaVuSansMono-Oblique.ttf differ
diff --git a/assets/fonts/DejaVuSansMono.ttf b/assets/fonts/DejaVuSansMono.ttf
new file mode 100644
index 0000000..5a6544a
Binary files /dev/null and b/assets/fonts/DejaVuSansMono.ttf differ
diff --git a/example.md b/example.md
new file mode 100644
index 0000000..7977d6b
--- /dev/null
+++ b/example.md
@@ -0,0 +1,23 @@
+# Hush
+
+En liten **äkta** WYSIWYG-redigerare för *markdown*, byggd i C och `clay.h`.
+
+## Funktioner
+
+- Rubriker, **fetstil** och *kursiv* text
+- Punktlistor och numrerade listor
+- Inline-kod som `printf("hej")` och länkar som [Clay](https://github.com/nicbarker/clay)
+
+1. Först
+2. Sedan
+3. Till sist
+
+> Ett citat med lite mer text som förhoppningsvis radbryts fint när det blir tillräckligt långt för att testa ordbrytningen ordentligt.
+
+```
+int main(void) {
+    return 0;
+}
+```
+
+Vanlig text med åäö för att testa svenska tecken.
diff --git a/src/app_mode.h b/src/app_mode.h
new file mode 100644
index 0000000..ac42ccd
--- /dev/null
+++ b/src/app_mode.h
@@ -0,0 +1,21 @@
+#ifndef HUSH_APP_MODE_H
+#define HUSH_APP_MODE_H
+
+/* Presentation/flow state -- deliberately kept out of EditorState, which is document-editing
+   state. Threaded through main.c's loop and into render_frame alongside the other per-frame
+   values (e.g. caretBlinkT) it already passes explicitly. */
+typedef enum {
+    MODE_EDITING,
+    MODE_QUIT_CONFIRM,
+    MODE_SHORTCUTS_HELP,
+} AppMode;
+
+typedef enum {
+    MODAL_ACTION_NONE,
+    MODAL_ACTION_SAVE_QUIT,
+    MODAL_ACTION_DISCARD_QUIT,
+    MODAL_ACTION_CANCEL,
+    MODAL_ACTION_CLOSE_HELP,
+} ModalAction;
+
+#endif
diff --git a/src/clay_impl.c b/src/clay_impl.c
new file mode 100644
index 0000000..b22f9f3
--- /dev/null
+++ b/src/clay_impl.c
@@ -0,0 +1,3 @@
+#define CLAY_IMPLEMENTATION
+#include "clay.h"
+#include "clay_renderer_raylib.c"
diff --git a/src/config.c b/src/config.c
new file mode 100644
index 0000000..0ec6bc2
--- /dev/null
+++ b/src/config.c
@@ -0,0 +1,160 @@
+#include "config.h"
+#include "theme.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <sys/stat.h>
+
+static const char *FONT_KEY_NAMES[FONT_COUNT] = {
+    "font_sans_regular", "font_sans_bold", "font_sans_italic", "font_sans_bold_italic",
+    "font_mono_regular", "font_mono_bold", "font_mono_italic", "font_mono_bold_italic",
+};
+
+static void config_set_defaults(Config *cfg) {
+    cfg->textColor = DEFAULT_COL_TEXT;
+    cfg->bgColor = DEFAULT_COL_BG;
+    for (int i = 0; i < FONT_COUNT; i++) cfg->fontPaths[i] = NULL;
+}
+
+void config_free(Config *cfg) {
+    for (int i = 0; i < FONT_COUNT; i++) {
+        free(cfg->fontPaths[i]);
+        cfg->fontPaths[i] = NULL;
+    }
+}
+
+static const char *trim(const char *s, int len, int *outLen) {
+    while (len > 0 && isspace((unsigned char)s[0])) { s++; len--; }
+    while (len > 0 && isspace((unsigned char)s[len - 1])) len--;
+    *outLen = len;
+    return s;
+}
+
+static bool parse_hex_byte(const char *s, unsigned char *out) {
+    unsigned char v = 0;
+    for (int i = 0; i < 2; i++) {
+        char c = s[i];
+        v <<= 4;
+        if (c >= '0' && c <= '9') v |= (unsigned char)(c - '0');
+        else if (c >= 'a' && c <= 'f') v |= (unsigned char)(c - 'a' + 10);
+        else if (c >= 'A' && c <= 'F') v |= (unsigned char)(c - 'A' + 10);
+        else return false;
+    }
+    *out = v;
+    return true;
+}
+
+static bool parse_hex_color(const char *v, int len, Clay_Color *out) {
+    if (len > 0 && v[0] == '#') { v++; len--; }
+    if (len != 6) return false;
+    unsigned char r, g, b;
+    if (!parse_hex_byte(v, &r) || !parse_hex_byte(v + 2, &g) || !parse_hex_byte(v + 4, &b)) return false;
+    out->r = r; out->g = g; out->b = b; out->a = 255;
+    return true;
+}
+
+bool config_parse_line(const char *line, int len, Config *cfg) {
+    int trimmedLen;
+    const char *t = trim(line, len, &trimmedLen);
+    if (trimmedLen == 0 || t[0] == '#') return false;
+
+    const char *eq = memchr(t, '=', (size_t)trimmedLen);
+    if (!eq) return false;
+
+    int keyLen;
+    const char *key = trim(t, (int)(eq - t), &keyLen);
+    int valueLen;
+    const char *value = trim(eq + 1, (int)(t + trimmedLen - (eq + 1)), &valueLen);
+    if (keyLen == 0 || valueLen == 0) return false;
+
+    if (keyLen == 10 && memcmp(key, "text_color", 10) == 0) return parse_hex_color(value, valueLen, &cfg->textColor);
+    if (keyLen == 8 && memcmp(key, "bg_color", 8) == 0) return parse_hex_color(value, valueLen, &cfg->bgColor);
+
+    for (int i = 0; i < FONT_COUNT; i++) {
+        size_t nameLen = strlen(FONT_KEY_NAMES[i]);
+        if ((size_t)keyLen == nameLen && memcmp(key, FONT_KEY_NAMES[i], nameLen) == 0) {
+            free(cfg->fontPaths[i]);
+            cfg->fontPaths[i] = malloc((size_t)valueLen + 1);
+            memcpy(cfg->fontPaths[i], value, (size_t)valueLen);
+            cfg->fontPaths[i][valueLen] = '\0';
+            return true;
+        }
+    }
+
+    return false;
+}
+
+static bool build_config_path(char *buf, size_t bufSize) {
+    const char *xdg = getenv("XDG_CONFIG_HOME");
+    if (xdg && xdg[0]) { snprintf(buf, bufSize, "%s/hush/config", xdg); return true; }
+    const char *home = getenv("HOME");
+    if (!home || !home[0]) return false;
+    snprintf(buf, bufSize, "%s/.config/hush/config", home);
+    return true;
+}
+
+/* Creates every directory component of `path` (a file path; only the directory portion is
+   created). Tolerates components that already exist. */
+static void mkdir_parents(const char *path) {
+    char buf[1024];
+    size_t n = strlen(path);
+    if (n >= sizeof buf) return;
+    memcpy(buf, path, n + 1);
+
+    for (size_t i = 1; i < n; i++) {
+        if (buf[i] != '/') continue;
+        buf[i] = '\0';
+        mkdir(buf, 0755);
+        buf[i] = '/';
+    }
+}
+
+static void write_default_config(const char *path) {
+    mkdir_parents(path);
+    FILE *f = fopen(path, "wb");
+    if (!f) return;
+    fprintf(f,
+        "# hush config file\n"
+        "# Uncomment and edit any line below to override the default. Unknown or malformed\n"
+        "# lines are ignored, so it's safe to experiment.\n"
+        "\n"
+        "# text_color = #%02X%02X%02X\n"
+        "# bg_color = #%02X%02X%02X\n"
+        "\n"
+        "# Font overrides (paths to .ttf files). Relative paths are resolved relative to the\n"
+        "# directory hush is launched from. Leave commented out to use the bundled DejaVu fonts.\n"
+        "# font_sans_regular = /path/to/font.ttf\n"
+        "# font_sans_bold = /path/to/font-bold.ttf\n"
+        "# font_sans_italic = /path/to/font-italic.ttf\n"
+        "# font_sans_bold_italic = /path/to/font-bold-italic.ttf\n"
+        "# font_mono_regular = /path/to/mono.ttf\n"
+        "# font_mono_bold = /path/to/mono-bold.ttf\n"
+        "# font_mono_italic = /path/to/mono-italic.ttf\n"
+        "# font_mono_bold_italic = /path/to/mono-bold-italic.ttf\n",
+        (unsigned)DEFAULT_COL_TEXT.r, (unsigned)DEFAULT_COL_TEXT.g, (unsigned)DEFAULT_COL_TEXT.b,
+        (unsigned)DEFAULT_COL_BG.r, (unsigned)DEFAULT_COL_BG.g, (unsigned)DEFAULT_COL_BG.b);
+    fclose(f);
+}
+
+bool config_load(Config *out) {
+    config_set_defaults(out);
+
+    char path[1024];
+    if (!build_config_path(path, sizeof path)) return true;
+
+    FILE *f = fopen(path, "rb");
+    if (!f) {
+        write_default_config(path);
+        return true;
+    }
+
+    char line[1024];
+    while (fgets(line, sizeof line, f)) {
+        int len = (int)strlen(line);
+        while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) len--;
+        config_parse_line(line, len, out);
+    }
+    fclose(f);
+    return true;
+}
diff --git a/src/config.h b/src/config.h
new file mode 100644
index 0000000..5ca17ac
--- /dev/null
+++ b/src/config.h
@@ -0,0 +1,27 @@
+#ifndef HUSH_CONFIG_H
+#define HUSH_CONFIG_H
+
+#include "clay.h"
+#include "fonts.h"
+#include <stdbool.h>
+
+typedef struct {
+    Clay_Color textColor;
+    Clay_Color bgColor;
+    char *fontPaths[FONT_COUNT]; /* malloc'd override path per slot, or NULL = bundled default */
+} Config;
+
+/* Fills `out` with the built-in defaults, then overlays whatever is set in the user's config
+   file (XDG_CONFIG_HOME/hush/config, or ~/.config/hush/config). If no config file exists yet,
+   creates one there with every key present but commented out, showing the defaults, so it's
+   self-documenting on first run. Always returns true -- filesystem failures (can't read, can't
+   create the default file, no $HOME) are non-fatal and just leave `out` at its defaults. */
+bool config_load(Config *out);
+void config_free(Config *cfg);
+
+/* Exposed for headless unit testing of the line parser without touching a real file. Returns
+   true if the line set a recognized key on `cfg`; false for comments/blank/unrecognized/
+   malformed lines -- never fatal, so a typo in a hand-edited file can't stop hush from starting. */
+bool config_parse_line(const char *line, int len, Config *cfg);
+
+#endif
diff --git a/src/document.c b/src/document.c
new file mode 100644
index 0000000..ef7f308
--- /dev/null
+++ b/src/document.c
@@ -0,0 +1,55 @@
+#include "document.h"
+#include <stdlib.h>
+#include <string.h>
+
+static void ensure_cap(Document *doc, int needed) {
+    if (needed <= doc->cap) return;
+    int newCap = doc->cap > 0 ? doc->cap : 8;
+    while (newCap < needed) newCap *= 2;
+    doc->blocks = realloc(doc->blocks, (size_t)newCap * sizeof(Block));
+    doc->cap = newCap;
+}
+
+void document_init(Document *doc) {
+    doc->blocks = NULL;
+    doc->count = 0;
+    doc->cap = 0;
+    document_insert_block(doc, 0, BLOCK_PARAGRAPH, "", 0);
+}
+
+void document_free(Document *doc) {
+    for (int i = 0; i < doc->count; i++) {
+        sb_free(&doc->blocks[i].text);
+    }
+    free(doc->blocks);
+    doc->blocks = NULL;
+    doc->count = 0;
+    doc->cap = 0;
+}
+
+void document_insert_block(Document *doc, int index, BlockType type, const char *text, int len) {
+    ensure_cap(doc, doc->count + 1);
+    memmove(&doc->blocks[index + 1], &doc->blocks[index], (size_t)(doc->count - index) * sizeof(Block));
+    doc->count++;
+    Block *b = &doc->blocks[index];
+    b->type = type;
+    sb_init(&b->text);
+    sb_append(&b->text, text, len);
+}
+
+void document_clone(Document *dst, const Document *src) {
+    dst->count = src->count;
+    dst->cap = src->count;
+    dst->blocks = dst->cap > 0 ? malloc((size_t)dst->cap * sizeof(Block)) : NULL;
+    for (int i = 0; i < src->count; i++) {
+        dst->blocks[i].type = src->blocks[i].type;
+        sb_init(&dst->blocks[i].text);
+        sb_append(&dst->blocks[i].text, src->blocks[i].text.data, src->blocks[i].text.len);
+    }
+}
+
+void document_remove_block(Document *doc, int index) {
+    sb_free(&doc->blocks[index].text);
+    memmove(&doc->blocks[index], &doc->blocks[index + 1], (size_t)(doc->count - index - 1) * sizeof(Block));
+    doc->count--;
+}
diff --git a/src/document.h b/src/document.h
new file mode 100644
index 0000000..34b76ea
--- /dev/null
+++ b/src/document.h
@@ -0,0 +1,48 @@
+#ifndef HUSH_DOCUMENT_H
+#define HUSH_DOCUMENT_H
+
+#include "strbuf.h"
+
+typedef enum {
+    BLOCK_PARAGRAPH,
+    BLOCK_H1,
+    BLOCK_H2,
+    BLOCK_H3,
+    BLOCK_BULLET,
+    BLOCK_NUMBERED,
+    BLOCK_QUOTE,
+    BLOCK_CODE,
+} BlockType;
+
+typedef struct {
+    BlockType type;
+    StrBuf text; /* raw text, without block-level prefix. Code blocks may embed '\n'. */
+} Block;
+
+typedef struct {
+    Block *blocks;
+    int count;
+    int cap;
+} Document;
+
+/* True for block types that "continue" themselves on Enter (list-like blocks). */
+static inline int block_type_is_continuable(BlockType t) {
+    return t == BLOCK_BULLET || t == BLOCK_NUMBERED || t == BLOCK_QUOTE;
+}
+
+void document_init(Document *doc);
+void document_free(Document *doc);
+
+/* Deep-copies `src` into `dst`. `dst` must not already hold live blocks (it is treated as
+   uninitialized, not freed first) — used to snapshot state for undo/redo. */
+void document_clone(Document *dst, const Document *src);
+
+/* Inserts a new block at `index`, shifting later blocks right. Text is copied. */
+void document_insert_block(Document *doc, int index, BlockType type, const char *text, int len);
+void document_remove_block(Document *doc, int index);
+
+static inline Block *document_block(Document *doc, int index) {
+    return &doc->blocks[index];
+}
+
+#endif
diff --git a/src/editor.c b/src/editor.c
new file mode 100644
index 0000000..0126c0b
--- /dev/null
+++ b/src/editor.c
@@ -0,0 +1,427 @@
+#include "editor.h"
+#include "markdown_io.h"
+#include "hittest.h"
+#include "utf8.h"
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+static void reset_preferred_x(EditorState *ed) {
+    ed->preferredX = -1.0f;
+}
+
+/* Collapses the selection anchor onto the current cursor position (extend=false), or leaves
+   it untouched to extend the existing selection (extend=true). */
+static void snap_anchor_to_cursor(EditorState *ed, bool extend) {
+    if (extend) return;
+    ed->selAnchorBlock = ed->cursorBlock;
+    ed->selAnchorOffset = ed->cursorOffset;
+}
+
+void editor_init(EditorState *ed) {
+    document_init(&ed->doc);
+    ed->cursorBlock = 0;
+    ed->cursorOffset = 0;
+    ed->preferredX = -1.0f;
+    ed->filePath = NULL;
+    ed->dirty = false;
+    undo_manager_init(&ed->undo, 200);
+    ed->selAnchorBlock = 0;
+    ed->selAnchorOffset = 0;
+}
+
+void editor_free(EditorState *ed) {
+    document_free(&ed->doc);
+    free(ed->filePath);
+    ed->filePath = NULL;
+    undo_manager_free(&ed->undo);
+}
+
+bool editor_load(EditorState *ed, const char *path) {
+    free(ed->filePath);
+    size_t n = strlen(path);
+    ed->filePath = malloc(n + 1);
+    memcpy(ed->filePath, path, n + 1);
+
+    bool ok = document_load_file(&ed->doc, path);
+    ed->cursorBlock = 0;
+    ed->cursorOffset = 0;
+    ed->dirty = false;
+    undo_manager_reset(&ed->undo);
+    ed->selAnchorBlock = 0;
+    ed->selAnchorOffset = 0;
+    reset_preferred_x(ed);
+    return ok;
+}
+
+bool editor_save(EditorState *ed) {
+    if (!ed->filePath) return false;
+    bool ok = document_save_file(&ed->doc, ed->filePath);
+    if (ok) ed->dirty = false;
+    return ok;
+}
+
+const char *editor_display_name(const EditorState *ed) {
+    if (!ed->filePath) return "Untitled";
+    const char *slash = strrchr(ed->filePath, '/');
+    return slash ? slash + 1 : ed->filePath;
+}
+
+/* Converts a leading run of paragraph text (e.g. "#", "##", "-", "1.") typed just before the
+   space that was just inserted into the corresponding block type, WYSIWYG-style. */
+static void maybe_autoformat_block(EditorState *ed) {
+    Block *b = &ed->doc.blocks[ed->cursorBlock];
+    if (b->type != BLOCK_PARAGRAPH) return;
+
+    int upto = ed->cursorOffset - 1; /* text before the space just typed */
+    if (upto <= 0) return;
+    const char *t = b->text.data;
+
+    BlockType newType;
+    if (upto == 1 && t[0] == '#') newType = BLOCK_H1;
+    else if (upto == 2 && t[0] == '#' && t[1] == '#') newType = BLOCK_H2;
+    else if (upto == 3 && t[0] == '#' && t[1] == '#' && t[2] == '#') newType = BLOCK_H3;
+    else if (upto == 1 && (t[0] == '-' || t[0] == '*')) newType = BLOCK_BULLET;
+    else if (upto == 1 && t[0] == '>') newType = BLOCK_QUOTE;
+    else if (upto >= 2 && isdigit((unsigned char)t[0])) {
+        int i = 0;
+        while (i < upto && isdigit((unsigned char)t[i])) i++;
+        if (i < upto && t[i] == '.' && i + 1 == upto) newType = BLOCK_NUMBERED;
+        else return;
+    } else {
+        return;
+    }
+
+    sb_delete(&b->text, 0, ed->cursorOffset);
+    ed->cursorOffset = 0;
+    b->type = newType;
+}
+
+bool editor_has_selection(const EditorState *ed) {
+    return ed->selAnchorBlock != ed->cursorBlock || ed->selAnchorOffset != ed->cursorOffset;
+}
+
+void editor_selection_range(const EditorState *ed, int *startBlock, int *startOffset, int *endBlock, int *endOffset) {
+    int ab = ed->selAnchorBlock, ao = ed->selAnchorOffset, cb = ed->cursorBlock, co = ed->cursorOffset;
+    if (ab < cb || (ab == cb && ao <= co)) {
+        *startBlock = ab; *startOffset = ao; *endBlock = cb; *endOffset = co;
+    } else {
+        *startBlock = cb; *startOffset = co; *endBlock = ab; *endOffset = ao;
+    }
+}
+
+/* If a selection is active, deletes it (merging a cross-block selection into the start
+   block, which keeps its type) and collapses the cursor/anchor to the deletion point.
+   Returns whether there was a selection to delete. */
+static bool delete_selection_if_any(EditorState *ed) {
+    if (!editor_has_selection(ed)) return false;
+    int sb, so, eb, eo;
+    editor_selection_range(ed, &sb, &so, &eb, &eo);
+    if (sb == eb) {
+        sb_delete(&ed->doc.blocks[sb].text, so, eo - so);
+    } else {
+        Block *startBlock = &ed->doc.blocks[sb];
+        Block *endBlock = &ed->doc.blocks[eb];
+        sb_delete(&startBlock->text, so, startBlock->text.len - so);
+        sb_append(&startBlock->text, endBlock->text.data + eo, endBlock->text.len - eo);
+        for (int i = eb; i > sb; i--) document_remove_block(&ed->doc, i);
+    }
+    ed->cursorBlock = ed->selAnchorBlock = sb;
+    ed->cursorOffset = ed->selAnchorOffset = so;
+    ed->dirty = true;
+    reset_preferred_x(ed);
+    return true;
+}
+
+char *editor_selection_to_text(const EditorState *ed, int *outLen) {
+    if (!editor_has_selection(ed)) { if (outLen) *outLen = 0; return NULL; }
+    int sb, so, eb, eo;
+    editor_selection_range(ed, &sb, &so, &eb, &eo);
+    StrBuf out;
+    sb_init(&out);
+    for (int i = sb; i <= eb; i++) {
+        Block *b = &ed->doc.blocks[i];
+        int start = (i == sb) ? so : 0;
+        int end = (i == eb) ? eo : b->text.len;
+        if (i > sb) sb_append_char(&out, '\n');
+        sb_append(&out, b->text.data + start, end - start);
+    }
+    char *result = malloc((size_t)out.len + 1);
+    memcpy(result, out.data, (size_t)out.len);
+    result[out.len] = '\0';
+    if (outLen) *outLen = out.len;
+    sb_free(&out);
+    return result;
+}
+
+static void insert_utf8_raw(EditorState *ed, const char *bytes, int n) {
+    Block *b = &ed->doc.blocks[ed->cursorBlock];
+    sb_insert(&b->text, ed->cursorOffset, bytes, n);
+    ed->cursorOffset += n;
+    ed->dirty = true;
+    if (n == 1 && bytes[0] == ' ') maybe_autoformat_block(ed);
+    reset_preferred_x(ed);
+}
+
+void editor_insert_utf8(EditorState *ed, const char *bytes, int n) {
+    if (n <= 0) return;
+    undo_manager_before_edit(&ed->undo, EDIT_TYPE, &ed->doc, ed->cursorBlock, ed->cursorOffset);
+    delete_selection_if_any(ed);
+    insert_utf8_raw(ed, bytes, n);
+    snap_anchor_to_cursor(ed, false);
+    undo_manager_after_edit(&ed->undo, EDIT_TYPE, ed->cursorBlock, ed->cursorOffset);
+}
+
+void editor_paste(EditorState *ed, const char *text, int len) {
+    if (len <= 0) return;
+    char *tmp = malloc((size_t)len);
+    int n = 0;
+    for (int i = 0; i < len; i++) {
+        char c = text[i];
+        if (c == '\r') continue;
+        if (c == '\n' || c == '\t') c = ' ';
+        if ((unsigned char)c < 0x20) continue;
+        tmp[n++] = c;
+    }
+    if (n > 0) {
+        undo_manager_before_edit(&ed->undo, EDIT_PASTE, &ed->doc, ed->cursorBlock, ed->cursorOffset);
+        delete_selection_if_any(ed);
+        insert_utf8_raw(ed, tmp, n);
+        snap_anchor_to_cursor(ed, false);
+        undo_manager_after_edit(&ed->undo, EDIT_PASTE, ed->cursorBlock, ed->cursorOffset);
+    }
+    free(tmp);
+}
+
+static void backspace_raw(EditorState *ed) {
+    Block *b = &ed->doc.blocks[ed->cursorBlock];
+    if (ed->cursorOffset > 0) {
+        int prevStart = utf8_prev_start(b->text.data, ed->cursorOffset);
+        sb_delete(&b->text, prevStart, ed->cursorOffset - prevStart);
+        ed->cursorOffset = prevStart;
+        ed->dirty = true;
+    } else if (b->type != BLOCK_PARAGRAPH) {
+        if (b->type == BLOCK_CODE) {
+            for (int i = 0; i < b->text.len; i++) if (b->text.data[i] == '\n') b->text.data[i] = ' ';
+        }
+        b->type = BLOCK_PARAGRAPH;
+        ed->dirty = true;
+    } else if (ed->cursorBlock > 0) {
+        Block *prev = &ed->doc.blocks[ed->cursorBlock - 1];
+        int mergeAt = prev->text.len;
+        sb_append(&prev->text, b->text.data, b->text.len);
+        document_remove_block(&ed->doc, ed->cursorBlock);
+        ed->cursorBlock--;
+        ed->cursorOffset = mergeAt;
+        ed->dirty = true;
+    }
+    reset_preferred_x(ed);
+}
+
+void editor_backspace(EditorState *ed) {
+    undo_manager_before_edit(&ed->undo, EDIT_BACKSPACE, &ed->doc, ed->cursorBlock, ed->cursorOffset);
+    if (delete_selection_if_any(ed)) {
+        undo_manager_after_edit(&ed->undo, EDIT_BACKSPACE, ed->cursorBlock, ed->cursorOffset);
+        return;
+    }
+    backspace_raw(ed);
+    snap_anchor_to_cursor(ed, false);
+    undo_manager_after_edit(&ed->undo, EDIT_BACKSPACE, ed->cursorBlock, ed->cursorOffset);
+}
+
+static void delete_forward_raw(EditorState *ed) {
+    Block *b = &ed->doc.blocks[ed->cursorBlock];
+    if (ed->cursorOffset < b->text.len) {
+        int nlen = utf8_next_len(b->text.data, ed->cursorOffset, b->text.len);
+        sb_delete(&b->text, ed->cursorOffset, nlen);
+        ed->dirty = true;
+    } else if (ed->cursorBlock < ed->doc.count - 1) {
+        Block *next = &ed->doc.blocks[ed->cursorBlock + 1];
+        if (b->type == BLOCK_CODE && next->type != BLOCK_CODE) {
+            sb_append_char(&b->text, '\n');
+        } else if (next->type == BLOCK_CODE && b->type != BLOCK_CODE) {
+            for (int i = 0; i < next->text.len; i++) if (next->text.data[i] == '\n') next->text.data[i] = ' ';
+        }
+        sb_append(&b->text, next->text.data, next->text.len);
+        document_remove_block(&ed->doc, ed->cursorBlock + 1);
+        ed->dirty = true;
+    }
+    reset_preferred_x(ed);
+}
+
+void editor_delete_forward(EditorState *ed) {
+    undo_manager_before_edit(&ed->undo, EDIT_DELETE_FWD, &ed->doc, ed->cursorBlock, ed->cursorOffset);
+    if (delete_selection_if_any(ed)) {
+        undo_manager_after_edit(&ed->undo, EDIT_DELETE_FWD, ed->cursorBlock, ed->cursorOffset);
+        return;
+    }
+    delete_forward_raw(ed);
+    snap_anchor_to_cursor(ed, false);
+    undo_manager_after_edit(&ed->undo, EDIT_DELETE_FWD, ed->cursorBlock, ed->cursorOffset);
+}
+
+static void enter_raw(EditorState *ed) {
+    Block *b = &ed->doc.blocks[ed->cursorBlock];
+
+    if (b->type == BLOCK_CODE) {
+        int lineStart = ed->cursorOffset;
+        while (lineStart > 0 && b->text.data[lineStart - 1] != '\n') lineStart--;
+        int lineEnd = ed->cursorOffset;
+        while (lineEnd < b->text.len && b->text.data[lineEnd] != '\n') lineEnd++;
+
+        if (lineEnd - lineStart == 3 && memcmp(b->text.data + lineStart, "```", 3) == 0) {
+            int removeFrom = lineStart > 0 ? lineStart - 1 : lineStart;
+            sb_delete(&b->text, removeFrom, b->text.len - removeFrom);
+            document_insert_block(&ed->doc, ed->cursorBlock + 1, BLOCK_PARAGRAPH, "", 0);
+            ed->cursorBlock++;
+            ed->cursorOffset = 0;
+            ed->dirty = true;
+            reset_preferred_x(ed);
+            return;
+        }
+
+        sb_insert(&b->text, ed->cursorOffset, "\n", 1);
+        ed->cursorOffset++;
+        ed->dirty = true;
+        reset_preferred_x(ed);
+        return;
+    }
+
+    if (b->type == BLOCK_PARAGRAPH && b->text.len == 3 && memcmp(b->text.data, "```", 3) == 0) {
+        b->type = BLOCK_CODE;
+        sb_clear(&b->text);
+        ed->cursorOffset = 0;
+        ed->dirty = true;
+        reset_preferred_x(ed);
+        return;
+    }
+
+    if (block_type_is_continuable(b->type) && b->text.len == 0) {
+        b->type = BLOCK_PARAGRAPH;
+        ed->dirty = true;
+        reset_preferred_x(ed);
+        return;
+    }
+
+    int splitAt = ed->cursorOffset;
+    int tailLen = b->text.len - splitAt;
+    BlockType newType = block_type_is_continuable(b->type) ? b->type : BLOCK_PARAGRAPH;
+
+    document_insert_block(&ed->doc, ed->cursorBlock + 1, newType, b->text.data + splitAt, tailLen);
+    b = &ed->doc.blocks[ed->cursorBlock]; /* doc.blocks may have been reallocated */
+    sb_delete(&b->text, splitAt, tailLen);
+
+    ed->cursorBlock++;
+    ed->cursorOffset = 0;
+    ed->dirty = true;
+    reset_preferred_x(ed);
+}
+
+void editor_enter(EditorState *ed) {
+    undo_manager_before_edit(&ed->undo, EDIT_ENTER, &ed->doc, ed->cursorBlock, ed->cursorOffset);
+    delete_selection_if_any(ed);
+    enter_raw(ed);
+    snap_anchor_to_cursor(ed, false);
+    undo_manager_after_edit(&ed->undo, EDIT_ENTER, ed->cursorBlock, ed->cursorOffset);
+}
+
+void editor_move_left(EditorState *ed, bool extend) {
+    if (ed->cursorOffset > 0) {
+        Block *b = &ed->doc.blocks[ed->cursorBlock];
+        ed->cursorOffset = utf8_prev_start(b->text.data, ed->cursorOffset);
+    } else if (ed->cursorBlock > 0) {
+        ed->cursorBlock--;
+        ed->cursorOffset = ed->doc.blocks[ed->cursorBlock].text.len;
+    }
+    snap_anchor_to_cursor(ed, extend);
+    reset_preferred_x(ed);
+}
+
+void editor_move_right(EditorState *ed, bool extend) {
+    Block *b = &ed->doc.blocks[ed->cursorBlock];
+    if (ed->cursorOffset < b->text.len) {
+        ed->cursorOffset += utf8_next_len(b->text.data, ed->cursorOffset, b->text.len);
+    } else if (ed->cursorBlock < ed->doc.count - 1) {
+        ed->cursorBlock++;
+        ed->cursorOffset = 0;
+    }
+    snap_anchor_to_cursor(ed, extend);
+    reset_preferred_x(ed);
+}
+
+void editor_move_home(EditorState *ed, bool extend) {
+    ed->cursorOffset = 0;
+    snap_anchor_to_cursor(ed, extend);
+    reset_preferred_x(ed);
+}
+
+void editor_move_end(EditorState *ed, bool extend) {
+    ed->cursorOffset = ed->doc.blocks[ed->cursorBlock].text.len;
+    snap_anchor_to_cursor(ed, extend);
+    reset_preferred_x(ed);
+}
+
+void editor_move_vertical(EditorState *ed, LayoutCache *cache, Font *fonts, int dir, bool extend) {
+    if (ed->preferredX < 0.0f) {
+        float x, y, h;
+        if (hittest_caret(cache, &ed->doc, fonts, ed->cursorBlock, ed->cursorOffset, &x, &y, &h)) {
+            ed->preferredX = x;
+        } else {
+            ed->preferredX = 0.0f;
+        }
+    }
+    VerticalMoveResult r = hittest_vertical(cache, &ed->doc, fonts, ed->cursorBlock, ed->cursorOffset, ed->preferredX, dir);
+    if (r.found) {
+        ed->cursorBlock = r.blockIndex;
+        ed->cursorOffset = r.offset;
+    }
+    snap_anchor_to_cursor(ed, extend);
+}
+
+void editor_click(EditorState *ed, LayoutCache *cache, Font *fonts, Vector2 point) {
+    HitResult r = hittest_point(cache, &ed->doc, fonts, point);
+    ed->cursorBlock = r.blockIndex;
+    ed->cursorOffset = r.offset;
+    ed->selAnchorBlock = r.blockIndex;
+    ed->selAnchorOffset = r.offset;
+    reset_preferred_x(ed);
+}
+
+void editor_drag_to(EditorState *ed, LayoutCache *cache, Font *fonts, Vector2 point) {
+    HitResult r = hittest_point(cache, &ed->doc, fonts, point);
+    ed->cursorBlock = r.blockIndex;
+    ed->cursorOffset = r.offset;
+    reset_preferred_x(ed);
+}
+
+bool editor_undo(EditorState *ed) {
+    Snapshot s;
+    if (!snapshot_stack_pop(&ed->undo.undo, &s)) return false;
+    snapshot_stack_push(&ed->undo.redo, &ed->doc, ed->cursorBlock, ed->cursorOffset);
+    document_free(&ed->doc);
+    ed->doc = s.doc;
+    ed->cursorBlock = s.cursorBlock;
+    ed->cursorOffset = s.cursorOffset;
+    ed->selAnchorBlock = s.cursorBlock;
+    ed->selAnchorOffset = s.cursorOffset;
+    ed->dirty = true;
+    ed->undo.lastKind = EDIT_NONE;
+    reset_preferred_x(ed);
+    return true;
+}
+
+bool editor_redo(EditorState *ed) {
+    Snapshot s;
+    if (!snapshot_stack_pop(&ed->undo.redo, &s)) return false;
+    snapshot_stack_push(&ed->undo.undo, &ed->doc, ed->cursorBlock, ed->cursorOffset);
+    document_free(&ed->doc);
+    ed->doc = s.doc;
+    ed->cursorBlock = s.cursorBlock;
+    ed->cursorOffset = s.cursorOffset;
+    ed->selAnchorBlock = s.cursorBlock;
+    ed->selAnchorOffset = s.cursorOffset;
+    ed->dirty = true;
+    ed->undo.lastKind = EDIT_NONE;
+    reset_preferred_x(ed);
+    return true;
+}
diff --git a/src/editor.h b/src/editor.h
new file mode 100644
index 0000000..e4eac2e
--- /dev/null
+++ b/src/editor.h
@@ -0,0 +1,63 @@
+#ifndef HUSH_EDITOR_H
+#define HUSH_EDITOR_H
+
+#include "document.h"
+#include "layout_cache.h"
+#include "undo.h"
+#include "raylib.h"
+#include <stdbool.h>
+
+typedef struct {
+    Document doc;
+    int cursorBlock;
+    int cursorOffset;
+    float preferredX; /* sticky x used across consecutive up/down moves; <0 means "recompute" */
+    char *filePath;   /* owned, may be NULL if the editor was started without a file */
+    bool dirty;
+    UndoManager undo;
+    int selAnchorBlock, selAnchorOffset; /* selection anchor; anchor == cursor means "no selection" */
+} EditorState;
+
+void editor_init(EditorState *ed);
+void editor_free(EditorState *ed);
+
+/* Loads `path` into the editor, remembering it for future saves regardless of success
+   (so Ctrl+S can create a brand new file). Returns false if the file could not be read. */
+bool editor_load(EditorState *ed, const char *path);
+bool editor_save(EditorState *ed);
+
+/* Basename of ed->filePath, or "Untitled" if the editor has no associated file yet. */
+const char *editor_display_name(const EditorState *ed);
+
+void editor_insert_utf8(EditorState *ed, const char *bytes, int n);
+void editor_paste(EditorState *ed, const char *text, int len);
+void editor_backspace(EditorState *ed);
+void editor_delete_forward(EditorState *ed);
+void editor_enter(EditorState *ed);
+
+bool editor_undo(EditorState *ed);
+bool editor_redo(EditorState *ed);
+
+/* `extend`: false moves the caret and collapses/resets the selection anchor to it (plain
+   movement); true moves only the caret, extending the selection from the existing anchor
+   (Shift+movement). */
+void editor_move_left(EditorState *ed, bool extend);
+void editor_move_right(EditorState *ed, bool extend);
+void editor_move_home(EditorState *ed, bool extend);
+void editor_move_end(EditorState *ed, bool extend);
+void editor_move_vertical(EditorState *ed, LayoutCache *cache, Font *fonts, int dir, bool extend);
+
+/* Mouse press: moves the caret and (re)establishes the selection anchor there. */
+void editor_click(EditorState *ed, LayoutCache *cache, Font *fonts, Vector2 point);
+/* Mouse drag (button held after the initial press): moves only the caret, extending the
+   selection from whatever editor_click most recently set as the anchor. */
+void editor_drag_to(EditorState *ed, LayoutCache *cache, Font *fonts, Vector2 point);
+
+bool editor_has_selection(const EditorState *ed);
+/* Normalizes the anchor/cursor pair into forward document order. */
+void editor_selection_range(const EditorState *ed, int *startBlock, int *startOffset, int *endBlock, int *endOffset);
+/* Returns the selected text (blocks joined with '\n'), or NULL if there is no selection.
+   Caller must free() the result. */
+char *editor_selection_to_text(const EditorState *ed, int *outLen);
+
+#endif
diff --git a/src/fonts.c b/src/fonts.c
new file mode 100644
index 0000000..b158e52
--- /dev/null
+++ b/src/fonts.c
@@ -0,0 +1,61 @@
+#include "fonts.h"
+#include <stdlib.h>
+
+#define FONT_BASE_SIZE 48
+
+/* ASCII + Latin-1 supplement, so accented characters (e.g. Swedish å ä ö) render correctly,
+   plus a couple of symbols used in the UI that fall outside that range. */
+static int *build_codepoints(int *count) {
+    int latin1 = 255 - 32 + 1;
+    static const int extra[] = {
+        0x2022, /* bullet, used for BLOCK_BULLET markers */
+        0x2014, /* em dash, used in the status bar and modal UI text */
+    };
+    int extraCount = (int)(sizeof(extra) / sizeof(extra[0]));
+    int n = latin1 + extraCount;
+
+    int *cps = malloc((size_t)n * sizeof(int));
+    for (int i = 0; i < latin1; i++) cps[i] = 32 + i;
+    for (int i = 0; i < extraCount; i++) cps[latin1 + i] = extra[i];
+
+    *count = n;
+    return cps;
+}
+
+static void load_one(Font *font, const char *overridePath, const char *defaultPath, const int *codepoints, int codepointCount) {
+    const char *path = (overridePath && overridePath[0]) ? overridePath : defaultPath;
+    *font = LoadFontEx(path, FONT_BASE_SIZE, (int *)codepoints, codepointCount);
+    if (font->texture.id == 0 && overridePath) {
+        /* Bad override path (e.g. a typo) -- fall back to the bundled default rather than
+           leaving this style slot with no usable font. */
+        *font = LoadFontEx(defaultPath, FONT_BASE_SIZE, (int *)codepoints, codepointCount);
+    }
+    if (font->texture.id != 0) {
+        /* Glyphs are drawn well below FONT_BASE_SIZE (see theme.h's FS_* sizes), so without
+           mipmaps, plain bilinear minification aliases badly ("wrong resolution" look). */
+        GenTextureMipmaps(&font->texture);
+        SetTextureFilter(font->texture, TEXTURE_FILTER_TRILINEAR);
+    }
+}
+
+void fonts_load(Font *fonts, char *const *overridePaths) {
+    int codepointCount;
+    int *codepoints = build_codepoints(&codepointCount);
+    char *none[FONT_COUNT] = {0};
+    char *const *ov = overridePaths ? overridePaths : none;
+
+    load_one(&fonts[FONT_SANS_REGULAR], ov[FONT_SANS_REGULAR], ASSETS_DIR "/fonts/DejaVuSans.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_SANS_BOLD], ov[FONT_SANS_BOLD], ASSETS_DIR "/fonts/DejaVuSans-Bold.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_SANS_ITALIC], ov[FONT_SANS_ITALIC], ASSETS_DIR "/fonts/DejaVuSans-Oblique.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_SANS_BOLD_ITALIC], ov[FONT_SANS_BOLD_ITALIC], ASSETS_DIR "/fonts/DejaVuSans-BoldOblique.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_MONO_REGULAR], ov[FONT_MONO_REGULAR], ASSETS_DIR "/fonts/DejaVuSansMono.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_MONO_BOLD], ov[FONT_MONO_BOLD], ASSETS_DIR "/fonts/DejaVuSansMono-Bold.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_MONO_ITALIC], ov[FONT_MONO_ITALIC], ASSETS_DIR "/fonts/DejaVuSansMono-Oblique.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_MONO_BOLD_ITALIC], ov[FONT_MONO_BOLD_ITALIC], ASSETS_DIR "/fonts/DejaVuSansMono-BoldOblique.ttf", codepoints, codepointCount);
+
+    free(codepoints);
+}
+
+void fonts_unload(Font *fonts) {
+    for (int i = 0; i < FONT_COUNT; i++) UnloadFont(fonts[i]);
+}
diff --git a/src/fonts.h b/src/fonts.h
new file mode 100644
index 0000000..6b352f6
--- /dev/null
+++ b/src/fonts.h
@@ -0,0 +1,28 @@
+#ifndef HUSH_FONTS_H
+#define HUSH_FONTS_H
+
+#include "raylib.h"
+
+#define FONT_SANS_REGULAR 0
+#define FONT_SANS_BOLD 1
+#define FONT_SANS_ITALIC 2
+#define FONT_SANS_BOLD_ITALIC 3
+#define FONT_MONO_REGULAR 4
+#define FONT_MONO_BOLD 5
+#define FONT_MONO_ITALIC 6
+#define FONT_MONO_BOLD_ITALIC 7
+#define FONT_COUNT 8
+
+/* fonts[] must have room for FONT_COUNT entries. Loaded at a large base size so that
+   scaling down for on-screen font sizes stays sharp. `overridePaths[i]`, if non-NULL,
+   replaces the bundled default for that slot (see font_index/FONT_* above); pass NULL for
+   the whole array to use only the bundled defaults. A bad override path falls back to the
+   bundled default rather than leaving an unusable font. */
+void fonts_load(Font *fonts, char *const *overridePaths);
+void fonts_unload(Font *fonts);
+
+static inline int font_index(int mono, int bold, int italic) {
+    return (mono ? FONT_MONO_REGULAR : FONT_SANS_REGULAR) + (bold ? 1 : 0) + (italic ? 2 : 0);
+}
+
+#endif
diff --git a/src/hittest.c b/src/hittest.c
new file mode 100644
index 0000000..7c732d4
--- /dev/null
+++ b/src/hittest.c
@@ -0,0 +1,176 @@
+#include "hittest.h"
+#include "clay.h"
+#include "utf8.h"
+#include <string.h>
+#include <float.h>
+
+#define MEASURE_BUF_SIZE 2048
+
+static float measure_width(Font *fonts, int fontId, int fontSize, const char *text, int start, int n) {
+    if (n <= 0) return 0.0f;
+    char buf[MEASURE_BUF_SIZE];
+    if (n >= MEASURE_BUF_SIZE) n = MEASURE_BUF_SIZE - 1;
+    memcpy(buf, text + start, (size_t)n);
+    buf[n] = '\0';
+    Vector2 sz = MeasureTextEx(fonts[fontId], buf, (float)fontSize, 0.0f);
+    return sz.x;
+}
+
+static Clay_ElementData get_line_data(int globalLineIndex) {
+    return Clay_GetElementData(CLAY_IDI("Line", globalLineIndex));
+}
+
+static Clay_ElementData get_seg_data(int globalSegIndex) {
+    return Clay_GetElementData(CLAY_IDI("Seg", globalSegIndex));
+}
+
+/* Finds the array index (into cache->lines) of the line that should own `offset` within `blockIndex`.
+   When `offset` sits exactly on the boundary between two lines, prefers the later one (start of the
+   next visual line), matching how a caret naturally rests after a wrapped word. */
+static int find_line_for_offset(LayoutCache *cache, int blockIndex, int offset) {
+    int best = -1;
+    for (int i = 0; i < cache->lineCount; i++) {
+        CachedLine *l = &cache->lines[i];
+        if (l->blockIndex != blockIndex) continue;
+        if (l->srcStart <= offset) best = i;
+    }
+    return best;
+}
+
+/* Finds the byte offset within `lineIdx` whose glyph is closest to screen-space `targetX`. */
+static int find_offset_in_line(LayoutCache *cache, Document *doc, Font *fonts, int lineIdx, float targetX) {
+    CachedLine *line = &cache->lines[lineIdx];
+    if (line->segStart == line->segEnd) return line->srcStart;
+
+    const char *text = doc->blocks[line->blockIndex].text.data;
+
+    int bestSeg = -1;
+    Clay_BoundingBox bestBox = {0};
+    for (int i = line->segStart; i < line->segEnd; i++) {
+        Clay_ElementData d = get_seg_data(cache->segs[i].globalSegIndex);
+        if (!d.found) continue;
+        if (bestSeg < 0) { bestSeg = i; bestBox = d.boundingBox; }
+        if (targetX >= d.boundingBox.x && targetX < d.boundingBox.x + d.boundingBox.width) {
+            bestSeg = i;
+            bestBox = d.boundingBox;
+            break;
+        }
+        if (targetX >= d.boundingBox.x) { bestSeg = i; bestBox = d.boundingBox; }
+    }
+    if (bestSeg < 0) return line->srcStart;
+
+    CachedSegment *seg = &cache->segs[bestSeg];
+    float localTarget = targetX - bestBox.x;
+    if (localTarget <= 0.0f) return seg->srcStart;
+
+    int pos = seg->srcStart;
+    while (pos < seg->srcEnd) {
+        int clen = utf8_next_len(text, pos, seg->srcEnd);
+        float wBefore = measure_width(fonts, seg->fontId, seg->fontSize, text, seg->srcStart, pos - seg->srcStart);
+        float wAfter = measure_width(fonts, seg->fontId, seg->fontSize, text, seg->srcStart, pos + clen - seg->srcStart);
+        if (wAfter >= localTarget) {
+            float mid = (wBefore + wAfter) * 0.5f;
+            return (localTarget < mid) ? pos : pos + clen;
+        }
+        pos += clen;
+    }
+    return seg->srcEnd;
+}
+
+HitResult hittest_point(LayoutCache *cache, Document *doc, Font *fonts, Vector2 point) {
+    HitResult r = {0, 0};
+    if (cache->lineCount == 0 || doc->count == 0) return r;
+
+    int bestLine = -1;
+    float bestDist = FLT_MAX;
+    for (int i = 0; i < cache->lineCount; i++) {
+        Clay_ElementData d = get_line_data(cache->lines[i].globalLineIndex);
+        if (!d.found) continue;
+        float top = d.boundingBox.y, bot = top + d.boundingBox.height;
+        if (point.y >= top && point.y < bot) { bestLine = i; bestDist = 0.0f; break; }
+        float dist = point.y < top ? (top - point.y) : (point.y - bot);
+        if (dist < bestDist) { bestDist = dist; bestLine = i; }
+    }
+    if (bestLine < 0) {
+        r.blockIndex = doc->count - 1;
+        r.offset = doc->blocks[r.blockIndex].text.len;
+        return r;
+    }
+
+    r.blockIndex = cache->lines[bestLine].blockIndex;
+    r.offset = find_offset_in_line(cache, doc, fonts, bestLine, point.x);
+    return r;
+}
+
+VerticalMoveResult hittest_vertical(LayoutCache *cache, Document *doc, Font *fonts, int blockIndex, int offset, float preferredX, int dir) {
+    VerticalMoveResult r = {0, 0, 0};
+    int curLine = find_line_for_offset(cache, blockIndex, offset);
+    if (curLine < 0) return r;
+    int targetLine = curLine + (dir < 0 ? -1 : 1);
+    if (targetLine < 0 || targetLine >= cache->lineCount) return r;
+
+    r.found = 1;
+    r.blockIndex = cache->lines[targetLine].blockIndex;
+    r.offset = find_offset_in_line(cache, doc, fonts, targetLine, preferredX);
+    return r;
+}
+
+/* Screen-space x of byte `offset` within visual line `line` (whose Clay element data,
+   `lineData`, the caller already has). Shared by hittest_caret and hittest_line_range_box. */
+static float x_at_offset_in_line(LayoutCache *cache, Document *doc, Font *fonts, CachedLine *line, Clay_ElementData lineData, int offset) {
+    if (line->segStart == line->segEnd) return lineData.boundingBox.x;
+
+    int bestSeg = -1;
+    for (int i = line->segStart; i < line->segEnd; i++) {
+        if (cache->segs[i].srcStart <= offset) bestSeg = i;
+    }
+    if (bestSeg < 0) bestSeg = line->segStart;
+
+    CachedSegment *seg = &cache->segs[bestSeg];
+    Clay_ElementData sd = get_seg_data(seg->globalSegIndex);
+    if (!sd.found) return lineData.boundingBox.x;
+
+    int within = offset - seg->srcStart;
+    if (within < 0) within = 0;
+    if (within > seg->srcEnd - seg->srcStart) within = seg->srcEnd - seg->srcStart;
+
+    const char *text = doc->blocks[line->blockIndex].text.data;
+    float w = measure_width(fonts, seg->fontId, seg->fontSize, text, seg->srcStart, within);
+    return sd.boundingBox.x + w;
+}
+
+int hittest_caret(LayoutCache *cache, Document *doc, Font *fonts, int blockIndex, int offset, float *outX, float *outY, float *outHeight) {
+    int lineIdx = find_line_for_offset(cache, blockIndex, offset);
+    if (lineIdx < 0) return 0;
+    CachedLine *line = &cache->lines[lineIdx];
+    Clay_ElementData ld = get_line_data(line->globalLineIndex);
+    if (!ld.found) return 0;
+
+    *outY = ld.boundingBox.y;
+    *outHeight = ld.boundingBox.height;
+    *outX = x_at_offset_in_line(cache, doc, fonts, line, ld, offset);
+    return 1;
+}
+
+int hittest_line_range_box(LayoutCache *cache, Document *doc, Font *fonts, int lineIdx,
+                            int rangeStart, int rangeEnd, float *outX, float *outY, float *outW, float *outHeight) {
+    if (lineIdx < 0 || lineIdx >= cache->lineCount) return 0;
+    CachedLine *line = &cache->lines[lineIdx];
+    int lo = rangeStart < line->srcStart ? line->srcStart : rangeStart;
+    int hi = rangeEnd > line->srcEnd ? line->srcEnd : rangeEnd;
+    if (hi < lo) return 0;
+
+    Clay_ElementData ld = get_line_data(line->globalLineIndex);
+    if (!ld.found) return 0;
+
+    float x0 = x_at_offset_in_line(cache, doc, fonts, line, ld, lo);
+    float x1 = x_at_offset_in_line(cache, doc, fonts, line, ld, hi);
+    float w = x1 - x0;
+    if (w < 4.0f) w = 4.0f; /* visible sliver for a fully-selected blank line */
+
+    *outX = x0;
+    *outY = ld.boundingBox.y;
+    *outW = w;
+    *outHeight = ld.boundingBox.height;
+    return 1;
+}
diff --git a/src/hittest.h b/src/hittest.h
new file mode 100644
index 0000000..571f24e
--- /dev/null
+++ b/src/hittest.h
@@ -0,0 +1,38 @@
+#ifndef HUSH_HITTEST_H
+#define HUSH_HITTEST_H
+
+#include "layout_cache.h"
+#include "document.h"
+#include "raylib.h"
+
+typedef struct {
+    int blockIndex;
+    int offset;
+} HitResult;
+
+/* Maps a screen-space point to the nearest (block, byte offset), using the Clay element
+   positions computed for the layout built during the previous frame. */
+HitResult hittest_point(LayoutCache *cache, Document *doc, Font *fonts, Vector2 point);
+
+typedef struct {
+    int found;
+    int blockIndex;
+    int offset;
+} VerticalMoveResult;
+
+/* Moves the cursor to the visual line above (dir<0) or below (dir>0), trying to land as
+   close as possible to `preferredX` (screen-space x). */
+VerticalMoveResult hittest_vertical(LayoutCache *cache, Document *doc, Font *fonts, int blockIndex, int offset, float preferredX, int dir);
+
+/* Fills the caret's screen box for (blockIndex, offset). Returns 0 if the layout has no
+   matching entry yet (e.g. very first frame). */
+int hittest_caret(LayoutCache *cache, Document *doc, Font *fonts, int blockIndex, int offset, float *outX, float *outY, float *outHeight);
+
+/* Fills the screen-space box covering [rangeStart, rangeEnd) (byte offsets within the line's
+   block) as visible on visual line `lineIdx`, clamped to that line's own byte range. Returns 0
+   if the range doesn't intersect the line or the layout has no matching entry yet. Used to draw
+   the selection highlight, one rectangle per intersected visual line. */
+int hittest_line_range_box(LayoutCache *cache, Document *doc, Font *fonts, int lineIdx,
+                            int rangeStart, int rangeEnd, float *outX, float *outY, float *outW, float *outHeight);
+
+#endif
diff --git a/src/inline_parse.c b/src/inline_parse.c
new file mode 100644
index 0000000..1d30b11
--- /dev/null
+++ b/src/inline_parse.c
@@ -0,0 +1,117 @@
+#include "inline_parse.h"
+#include <stdlib.h>
+
+void inline_runs_init(InlineRunList *list) {
+    list->runs = NULL;
+    list->count = 0;
+    list->cap = 0;
+}
+
+void inline_runs_free(InlineRunList *list) {
+    free(list->runs);
+    list->runs = NULL;
+    list->count = 0;
+    list->cap = 0;
+}
+
+static void push_run(InlineRunList *list, RunKind kind, int rawStart, int rawEnd, int contentStart, int contentEnd) {
+    if (list->count >= list->cap) {
+        list->cap = list->cap > 0 ? list->cap * 2 : 8;
+        list->runs = realloc(list->runs, (size_t)list->cap * sizeof(InlineRun));
+    }
+    InlineRun *r = &list->runs[list->count++];
+    r->kind = kind;
+    r->rawStart = rawStart;
+    r->rawEnd = rawEnd;
+    r->contentStart = contentStart;
+    r->contentEnd = contentEnd;
+}
+
+/* Finds the next unescaped occurrence of `c`, starting at `from`. -1 if not found. */
+static int find_char(const char *text, int len, int from, char c) {
+    for (int i = from; i < len; i++) {
+        if (text[i] == '\\' && i + 1 < len) { i++; continue; }
+        if (text[i] == c) return i;
+    }
+    return -1;
+}
+
+/* Finds the next unescaped occurrence of the two-char delimiter `cc`. -1 if not found. */
+static int find_double(const char *text, int len, int from, char c) {
+    for (int i = from; i < len - 1; i++) {
+        if (text[i] == '\\' && i + 1 < len) { i++; continue; }
+        if (text[i] == c && text[i + 1] == c) return i;
+    }
+    return -1;
+}
+
+void inline_parse(const char *text, int len, InlineRunList *out) {
+    inline_runs_init(out);
+    int i = 0;
+    int plainStart = 0;
+
+    while (i < len) {
+        char c = text[i];
+        int matched = 0;
+
+        if (c == '`') {
+            int j = find_char(text, len, i + 1, '`');
+            if (j >= 0 && j > i + 1) {
+                if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
+                push_run(out, RUN_CODE, i, j + 1, i + 1, j);
+                i = j + 1;
+                plainStart = i;
+                matched = 1;
+            }
+        } else if ((c == '*' || c == '_') && i + 2 < len && text[i + 1] == c && text[i + 2] == c) {
+            int j = -1;
+            for (int k = i + 3; k < len - 2; k++) {
+                if (text[k] == c && text[k + 1] == c && text[k + 2] == c) { j = k; break; }
+            }
+            if (j >= 0 && j > i + 3) {
+                if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
+                push_run(out, RUN_BOLD_ITALIC, i, j + 3, i + 3, j);
+                i = j + 3;
+                plainStart = i;
+                matched = 1;
+            }
+        } else if ((c == '*' || c == '_') && i + 1 < len && text[i + 1] == c) {
+            int j = find_double(text, len, i + 2, c);
+            if (j >= 0 && j > i + 2) {
+                if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
+                push_run(out, RUN_BOLD, i, j + 2, i + 2, j);
+                i = j + 2;
+                plainStart = i;
+                matched = 1;
+            }
+        } else if (c == '*' || c == '_') {
+            int j = find_char(text, len, i + 1, c);
+            if (j >= 0 && j > i + 1) {
+                if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
+                push_run(out, RUN_ITALIC, i, j + 1, i + 1, j);
+                i = j + 1;
+                plainStart = i;
+                matched = 1;
+            }
+        } else if (c == '[') {
+            int closeBracket = find_char(text, len, i + 1, ']');
+            if (closeBracket >= 0 && closeBracket > i + 1 && closeBracket + 1 < len && text[closeBracket + 1] == '(') {
+                int closeParen = find_char(text, len, closeBracket + 2, ')');
+                if (closeParen >= 0) {
+                    if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
+                    push_run(out, RUN_LINK, i, closeParen + 1, i + 1, closeBracket);
+                    i = closeParen + 1;
+                    plainStart = i;
+                    matched = 1;
+                }
+            }
+        } else if (c == '\\' && i + 1 < len) {
+            i += 2;
+            continue;
+        }
+
+        if (!matched) i++;
+    }
+
+    if (len > plainStart) push_run(out, RUN_PLAIN, plainStart, len, plainStart, len);
+}
diff --git a/src/inline_parse.h b/src/inline_parse.h
new file mode 100644
index 0000000..5583059
--- /dev/null
+++ b/src/inline_parse.h
@@ -0,0 +1,32 @@
+#ifndef HUSH_INLINE_PARSE_H
+#define HUSH_INLINE_PARSE_H
+
+typedef enum {
+    RUN_PLAIN,
+    RUN_BOLD,
+    RUN_ITALIC,
+    RUN_BOLD_ITALIC,
+    RUN_CODE,
+    RUN_LINK,
+} RunKind;
+
+typedef struct {
+    RunKind kind;
+    /* Byte range in the block's raw text, INCLUDING markdown markers (e.g. "**bold**"). */
+    int rawStart, rawEnd;
+    /* Byte range of the actual displayable content, EXCLUDING markers (e.g. "bold"). */
+    int contentStart, contentEnd;
+} InlineRun;
+
+typedef struct {
+    InlineRun *runs;
+    int count, cap;
+} InlineRunList;
+
+void inline_runs_init(InlineRunList *list);
+void inline_runs_free(InlineRunList *list);
+
+/* Parses `text[0..len)` into a sequence of runs covering the whole range with no gaps. */
+void inline_parse(const char *text, int len, InlineRunList *out);
+
+#endif
diff --git a/src/layout_cache.c b/src/layout_cache.c
new file mode 100644
index 0000000..fa5fe08
--- /dev/null
+++ b/src/layout_cache.c
@@ -0,0 +1,55 @@
+#include "layout_cache.h"
+#include <stdlib.h>
+
+void layout_cache_init(LayoutCache *c) {
+    c->lines = NULL; c->lineCount = 0; c->lineCap = 0;
+    c->segs = NULL; c->segCount = 0; c->segCap = 0;
+}
+
+void layout_cache_reset(LayoutCache *c) {
+    c->lineCount = 0;
+    c->segCount = 0;
+}
+
+void layout_cache_free(LayoutCache *c) {
+    free(c->lines);
+    free(c->segs);
+    layout_cache_init(c);
+}
+
+int layout_cache_add_line(LayoutCache *c, int blockIndex, int lineInBlock, int srcStart, int srcEnd) {
+    if (c->lineCount >= c->lineCap) {
+        c->lineCap = c->lineCap > 0 ? c->lineCap * 2 : 64;
+        c->lines = realloc(c->lines, (size_t)c->lineCap * sizeof(CachedLine));
+    }
+    int idx = c->lineCount++;
+    CachedLine *l = &c->lines[idx];
+    l->blockIndex = blockIndex;
+    l->lineInBlock = lineInBlock;
+    l->srcStart = srcStart;
+    l->srcEnd = srcEnd;
+    l->globalLineIndex = idx;
+    l->segStart = c->segCount;
+    l->segEnd = c->segCount;
+    return idx;
+}
+
+int layout_cache_add_seg(LayoutCache *c, int lineIndex, int srcStart, int srcEnd, int fontId, int fontSize) {
+    if (c->segCount >= c->segCap) {
+        c->segCap = c->segCap > 0 ? c->segCap * 2 : 128;
+        c->segs = realloc(c->segs, (size_t)c->segCap * sizeof(CachedSegment));
+    }
+    int idx = c->segCount++;
+    CachedSegment *s = &c->segs[idx];
+    s->lineIndex = lineIndex;
+    s->srcStart = srcStart;
+    s->srcEnd = srcEnd;
+    s->globalSegIndex = idx;
+    s->fontId = fontId;
+    s->fontSize = fontSize;
+    return idx;
+}
+
+void layout_cache_close_line(LayoutCache *c, int lineIndex) {
+    c->lines[lineIndex].segEnd = c->segCount;
+}
diff --git a/src/layout_cache.h b/src/layout_cache.h
new file mode 100644
index 0000000..d1a5e6e
--- /dev/null
+++ b/src/layout_cache.h
@@ -0,0 +1,38 @@
+#ifndef HUSH_LAYOUT_CACHE_H
+#define HUSH_LAYOUT_CACHE_H
+
+/* A rendered visual line, produced fresh every frame while building the Clay tree.
+   Used the *following* frame to resolve clicks and up/down cursor movement against
+   real, already-computed Clay element positions (queried via Clay_GetElementData). */
+typedef struct {
+    int blockIndex;
+    int lineInBlock;
+    int srcStart, srcEnd; /* byte range in the block's raw text covered by this line */
+    int globalLineIndex;  /* identifies this line's Clay element: CLAY_IDI("Line", globalLineIndex) */
+    int segStart, segEnd; /* range into LayoutCache.segs covering this line's segments */
+} CachedLine;
+
+typedef struct {
+    int lineIndex; /* index into LayoutCache.lines */
+    int srcStart, srcEnd;
+    int globalSegIndex; /* identifies this segment's Clay element: CLAY_IDI("Seg", globalSegIndex) */
+    int fontId;
+    int fontSize;
+} CachedSegment;
+
+typedef struct {
+    CachedLine *lines;
+    int lineCount, lineCap;
+    CachedSegment *segs;
+    int segCount, segCap;
+} LayoutCache;
+
+void layout_cache_init(LayoutCache *c);
+void layout_cache_reset(LayoutCache *c); /* clears contents but keeps capacity, for reuse each frame */
+void layout_cache_free(LayoutCache *c);
+
+int layout_cache_add_line(LayoutCache *c, int blockIndex, int lineInBlock, int srcStart, int srcEnd);
+int layout_cache_add_seg(LayoutCache *c, int lineIndex, int srcStart, int srcEnd, int fontId, int fontSize);
+void layout_cache_close_line(LayoutCache *c, int lineIndex); /* call after adding all segs for a line, to set segEnd */
+
+#endif
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..f6c7342
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,183 @@
+#include "raylib.h"
+#include "clay.h"
+#include "editor.h"
+#include "render.h"
+#include "layout_cache.h"
+#include "fonts.h"
+#include "theme.h"
+#include "app_mode.h"
+#include "config.h"
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+static void handle_clay_errors(Clay_ErrorData errorData) {
+    fprintf(stderr, "Clay error: %s\n", errorData.errorText.chars);
+}
+
+/* True on the frame a key was just pressed, and then again at a steady repeat rate while held. */
+static bool key_repeat(int key, double now, double *nextRepeatAt) {
+    if (IsKeyPressed(key)) { *nextRepeatAt = now + 0.4; return true; }
+    if (IsKeyDown(key) && now >= *nextRepeatAt) { *nextRepeatAt = now + 0.03; return true; }
+    if (!IsKeyDown(key)) *nextRepeatAt = 0.0;
+    return false;
+}
+
+static void update_window_title(EditorState *ed) {
+    char title[512];
+    snprintf(title, sizeof title, "%s%s \xe2\x80\x94 hush", ed->dirty ? "*" : "", editor_display_name(ed));
+    SetWindowTitle(title);
+}
+
+int main(int argc, char **argv) {
+    Config cfg;
+    config_load(&cfg); /* pure file I/O, no raylib dependency -- safe to run before window init */
+    theme_init_defaults();
+    g_theme.bg = cfg.bgColor;
+    g_theme.text = cfg.textColor;
+    g_theme.cursor = cfg.textColor; /* caret always tracks the configured text color */
+
+    Clay_Raylib_Initialize(1000, 800, "hush", FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE | FLAG_MSAA_4X_HINT);
+    SetExitKey(KEY_NULL);
+
+    uint64_t memSize = Clay_MinMemorySize();
+    Clay_Arena arena = Clay_CreateArenaWithCapacityAndMemory(memSize, malloc(memSize));
+    Clay_Initialize(arena, (Clay_Dimensions){ (float)GetScreenWidth(), (float)GetScreenHeight() }, (Clay_ErrorHandler){ handle_clay_errors, 0 });
+
+    Font fonts[FONT_COUNT];
+    fonts_load(fonts, cfg.fontPaths);
+    config_free(&cfg);
+    Clay_SetMeasureTextFunction(hush_measure_text, fonts);
+
+    EditorState ed;
+    editor_init(&ed);
+    if (argc >= 2) {
+        editor_load(&ed, argv[1]);
+    }
+    update_window_title(&ed);
+
+    LayoutCache cache;
+    layout_cache_init(&cache);
+
+    double blinkResetTime = GetTime();
+    int lastBlock = ed.cursorBlock, lastOffset = ed.cursorOffset;
+    bool lastDirty = ed.dirty;
+
+    double repLeft = 0, repRight = 0, repUp = 0, repDown = 0, repBackspace = 0, repDelete = 0;
+
+    bool windowClosing = false;
+    AppMode mode = MODE_EDITING;
+    /* True only for a mouse-down session whose *press* frame was itself handled in
+       MODE_EDITING -- prevents the tail end of a click that dismissed a modal (still
+       physically held down for a frame or two afterward) from being replayed as a
+       document drag-select once mode flips back to MODE_EDITING. */
+    bool mouseDownInEditor = false;
+
+    while (!windowClosing) {
+        double now = GetTime();
+        bool ctrl = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL);
+        bool shift = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
+
+        if (mode == MODE_EDITING) {
+            if (ctrl && IsKeyPressed(KEY_S)) {
+                editor_save(&ed);
+            } else if (ctrl && IsKeyPressed(KEY_V)) {
+                const char *clip = GetClipboardText();
+                if (clip) editor_paste(&ed, clip, (int)strlen(clip));
+            } else if (ctrl && IsKeyPressed(KEY_C)) {
+                int len;
+                char *text = editor_selection_to_text(&ed, &len);
+                if (text) { SetClipboardText(text); free(text); }
+            } else if (ctrl && shift && IsKeyPressed(KEY_Z)) {
+                editor_redo(&ed);
+            } else if (ctrl && IsKeyPressed(KEY_Z)) {
+                editor_undo(&ed);
+            } else if (ctrl && IsKeyPressed(KEY_X)) {
+                mode = ed.dirty ? MODE_QUIT_CONFIRM : MODE_EDITING;
+                if (!ed.dirty) windowClosing = true;
+            } else if ((ctrl && IsKeyPressed(KEY_SLASH)) || IsKeyPressed(KEY_F1)) {
+                /* Ctrl+/ is bound by physical scancode (matches the common editor convention,
+                   e.g. VS Code/Sublime's comment-toggle), which lands on a different printed
+                   character on non-US keyboard layouts (e.g. it's the "-/_" key on a Swedish
+                   layout, not "?"). F1 is a layout-independent fallback that's always reachable. */
+                mode = MODE_SHORTCUTS_HELP;
+            } else {
+                if (!ctrl) {
+                    int cp;
+                    while ((cp = GetCharPressed()) != 0) {
+                        int utf8Size = 0;
+                        const char *bytes = CodepointToUTF8(cp, &utf8Size);
+                        editor_insert_utf8(&ed, bytes, utf8Size);
+                    }
+                }
+                if (IsKeyPressed(KEY_ENTER) || IsKeyPressed(KEY_KP_ENTER)) editor_enter(&ed);
+                if (key_repeat(KEY_BACKSPACE, now, &repBackspace)) editor_backspace(&ed);
+                if (key_repeat(KEY_DELETE, now, &repDelete)) editor_delete_forward(&ed);
+                if (key_repeat(KEY_LEFT, now, &repLeft)) editor_move_left(&ed, shift);
+                if (key_repeat(KEY_RIGHT, now, &repRight)) editor_move_right(&ed, shift);
+                if (key_repeat(KEY_UP, now, &repUp)) editor_move_vertical(&ed, &cache, fonts, -1, shift);
+                if (key_repeat(KEY_DOWN, now, &repDown)) editor_move_vertical(&ed, &cache, fonts, 1, shift);
+                if (IsKeyPressed(KEY_HOME)) editor_move_home(&ed, shift);
+                if (IsKeyPressed(KEY_END)) editor_move_end(&ed, shift);
+            }
+
+            if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
+                editor_click(&ed, &cache, fonts, GetMousePosition());
+                mouseDownInEditor = true;
+            } else if (mouseDownInEditor && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
+                editor_drag_to(&ed, &cache, fonts, GetMousePosition());
+            }
+
+            /* raylib's WindowShouldClose() self-resets every frame (PollInputEvents re-derives
+               it from the GLFW window-close flag and immediately clears that flag), so this is
+               a clean one-shot edge per OS close-button click -- no extra latch needed. */
+            if (WindowShouldClose()) {
+                mode = ed.dirty ? MODE_QUIT_CONFIRM : MODE_EDITING;
+                if (!ed.dirty) windowClosing = true;
+            }
+        } else if (mode == MODE_QUIT_CONFIRM) {
+            if (IsKeyPressed(KEY_ESCAPE)) mode = MODE_EDITING;
+        } else if (mode == MODE_SHORTCUTS_HELP) {
+            if (IsKeyPressed(KEY_ESCAPE) || IsKeyPressed(KEY_F1) || (ctrl && IsKeyPressed(KEY_SLASH))) mode = MODE_EDITING;
+        }
+
+        if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) mouseDownInEditor = false;
+
+        if (ed.cursorBlock != lastBlock || ed.cursorOffset != lastOffset) {
+            blinkResetTime = now;
+            lastBlock = ed.cursorBlock;
+            lastOffset = ed.cursorOffset;
+        }
+        if (ed.dirty != lastDirty) {
+            lastDirty = ed.dirty;
+            update_window_title(&ed);
+        }
+
+        Clay_Vector2 mousePos = { GetMousePosition().x, GetMousePosition().y };
+        Clay_SetPointerState(mousePos, IsMouseButtonDown(0));
+        Clay_SetLayoutDimensions((Clay_Dimensions){ (float)GetScreenWidth(), (float)GetScreenHeight() });
+        Vector2 wheel = GetMouseWheelMoveV();
+        Clay_UpdateScrollContainers(false, (Clay_Vector2){ wheel.x, wheel.y * 40.0f }, GetFrameTime());
+
+        float caretBlinkT = (float)(now - blinkResetTime);
+
+        BeginDrawing();
+        ClearBackground(BLACK);
+        ModalAction action = render_frame(&ed, &cache, fonts, caretBlinkT, mode);
+        EndDrawing();
+
+        switch (action) {
+            case MODAL_ACTION_SAVE_QUIT:    editor_save(&ed); windowClosing = true; break;
+            case MODAL_ACTION_DISCARD_QUIT: windowClosing = true; break;
+            case MODAL_ACTION_CANCEL:       mode = MODE_EDITING; break;
+            case MODAL_ACTION_CLOSE_HELP:   mode = MODE_EDITING; break;
+            default: break;
+        }
+    }
+
+    layout_cache_free(&cache);
+    editor_free(&ed);
+    fonts_unload(fonts);
+    Clay_Raylib_Close();
+    return 0;
+}
diff --git a/src/markdown_io.c b/src/markdown_io.c
new file mode 100644
index 0000000..55d7ce4
--- /dev/null
+++ b/src/markdown_io.c
@@ -0,0 +1,167 @@
+#include "markdown_io.h"
+#include "strbuf.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+static int match_numbered_prefix(const char *line, int len, int *prefixLen) {
+    int i = 0;
+    while (i < len && isdigit((unsigned char)line[i])) i++;
+    if (i == 0 || i >= len || line[i] != '.') return 0;
+    i++;
+    if (i < len && line[i] == ' ') i++;
+    else if (i < len) return 0; /* digits+'.' followed by something other than a space: not a list marker */
+    *prefixLen = i;
+    return 1;
+}
+
+static int is_blank_line(const char *line, int len) {
+    for (int i = 0; i < len; i++) {
+        if (line[i] != ' ' && line[i] != '\t') return 0;
+    }
+    return 1;
+}
+
+static int is_code_fence(const char *line, int len) {
+    return len >= 3 && line[0] == '`' && line[1] == '`' && line[2] == '`';
+}
+
+bool document_load_file(Document *doc, const char *path) {
+    FILE *f = fopen(path, "rb");
+    if (!f) return false;
+
+    fseek(f, 0, SEEK_END);
+    long size = ftell(f);
+    fseek(f, 0, SEEK_SET);
+    if (size < 0) { fclose(f); return false; }
+
+    char *buf = malloc((size_t)size + 1);
+    size_t readN = fread(buf, 1, (size_t)size, f);
+    fclose(f);
+    buf[readN] = '\0';
+
+    for (int i = 0; i < doc->count; i++) sb_free(&doc->blocks[i].text);
+    doc->count = 0;
+
+    int pos = 0;
+    int len = (int)readN;
+    while (pos < len) {
+        int lineStart = pos;
+        while (pos < len && buf[pos] != '\n') pos++;
+        int lineEnd = pos;
+        if (lineEnd > lineStart && buf[lineEnd - 1] == '\r') lineEnd--;
+        if (pos < len) pos++; /* consume '\n' */
+
+        const char *line = buf + lineStart;
+        int lineLen = lineEnd - lineStart;
+
+        if (is_code_fence(line, lineLen)) {
+            StrBuf code;
+            sb_init(&code);
+            int first = 1;
+            while (pos < len) {
+                int s = pos;
+                while (pos < len && buf[pos] != '\n') pos++;
+                int e = pos;
+                if (e > s && buf[e - 1] == '\r') e--;
+                if (pos < len) pos++;
+                if (is_code_fence(buf + s, e - s)) break;
+                if (!first) sb_append_char(&code, '\n');
+                sb_append(&code, buf + s, e - s);
+                first = 0;
+            }
+            document_insert_block(doc, doc->count, BLOCK_CODE, code.data, code.len);
+            sb_free(&code);
+            continue;
+        }
+
+        if (is_blank_line(line, lineLen)) continue;
+
+        int prefixLen;
+        if (lineLen >= 2 && line[0] == '#' && line[1] == ' ') {
+            document_insert_block(doc, doc->count, BLOCK_H1, line + 2, lineLen - 2);
+        } else if (lineLen >= 3 && line[0] == '#' && line[1] == '#' && line[2] == ' ') {
+            document_insert_block(doc, doc->count, BLOCK_H2, line + 3, lineLen - 3);
+        } else if (lineLen >= 4 && line[0] == '#' && line[1] == '#' && line[2] == '#' && line[3] == ' ') {
+            document_insert_block(doc, doc->count, BLOCK_H3, line + 4, lineLen - 4);
+        } else if (lineLen >= 2 && (line[0] == '-' || line[0] == '*') && line[1] == ' ') {
+            document_insert_block(doc, doc->count, BLOCK_BULLET, line + 2, lineLen - 2);
+        } else if (lineLen >= 2 && line[0] == '>' && line[1] == ' ') {
+            document_insert_block(doc, doc->count, BLOCK_QUOTE, line + 2, lineLen - 2);
+        } else if (match_numbered_prefix(line, lineLen, &prefixLen)) {
+            document_insert_block(doc, doc->count, BLOCK_NUMBERED, line + prefixLen, lineLen - prefixLen);
+        } else {
+            document_insert_block(doc, doc->count, BLOCK_PARAGRAPH, line, lineLen);
+        }
+    }
+
+    free(buf);
+
+    if (doc->count == 0) {
+        document_insert_block(doc, 0, BLOCK_PARAGRAPH, "", 0);
+    }
+
+    return true;
+}
+
+void document_serialize(Document *doc, StrBuf *out) {
+    sb_clear(out);
+
+    int numberedRun = 0;
+    for (int i = 0; i < doc->count; i++) {
+        Block *b = &doc->blocks[i];
+        if (i > 0) sb_append(out, "\n\n", 2);
+
+        switch (b->type) {
+            case BLOCK_H1: sb_append(out, "# ", 2); sb_append(out, b->text.data, b->text.len); break;
+            case BLOCK_H2: sb_append(out, "## ", 3); sb_append(out, b->text.data, b->text.len); break;
+            case BLOCK_H3: sb_append(out, "### ", 4); sb_append(out, b->text.data, b->text.len); break;
+            case BLOCK_BULLET: sb_append(out, "- ", 2); sb_append(out, b->text.data, b->text.len); break;
+            case BLOCK_NUMBERED: {
+                if (i > 0 && doc->blocks[i - 1].type == BLOCK_NUMBERED) numberedRun++;
+                else numberedRun = 1;
+                char prefix[16];
+                int n = snprintf(prefix, sizeof prefix, "%d. ", numberedRun);
+                sb_append(out, prefix, n);
+                sb_append(out, b->text.data, b->text.len);
+                break;
+            }
+            case BLOCK_QUOTE: sb_append(out, "> ", 2); sb_append(out, b->text.data, b->text.len); break;
+            case BLOCK_CODE:
+                sb_append(out, "```\n", 4);
+                sb_append(out, b->text.data, b->text.len);
+                sb_append(out, "\n```", 4);
+                break;
+            case BLOCK_PARAGRAPH:
+            default:
+                sb_append(out, b->text.data, b->text.len);
+                break;
+        }
+    }
+    sb_append_char(out, '\n');
+}
+
+bool document_save_file(Document *doc, const char *path) {
+    StrBuf out;
+    sb_init(&out);
+    document_serialize(doc, &out);
+
+    FILE *f = fopen(path, "wb");
+    if (!f) { sb_free(&out); return false; }
+    size_t written = fwrite(out.data, 1, (size_t)out.len, f);
+    fclose(f);
+    sb_free(&out);
+    return written == (size_t)out.len || out.len == 0;
+}
+
+void document_compute_stats(Document *doc, DocStats *out) {
+    StrBuf tmp;
+    sb_init(&tmp);
+    document_serialize(doc, &tmp);
+    out->byteSize = tmp.len;
+    int lines = 0;
+    for (int i = 0; i < tmp.len; i++) if (tmp.data[i] == '\n') lines++;
+    out->lineCount = lines;
+    sb_free(&tmp);
+}
diff --git a/src/markdown_io.h b/src/markdown_io.h
new file mode 100644
index 0000000..f5dc4e9
--- /dev/null
+++ b/src/markdown_io.h
@@ -0,0 +1,26 @@
+#ifndef HUSH_MARKDOWN_IO_H
+#define HUSH_MARKDOWN_IO_H
+
+#include "document.h"
+#include <stdbool.h>
+
+/* Replaces the contents of `doc` with the parsed contents of the file at `path`.
+   Returns false (and leaves `doc` untouched) if the file could not be read. */
+bool document_load_file(Document *doc, const char *path);
+
+/* Serializes `doc` to markdown and writes it to `path`. Returns false on I/O failure. */
+bool document_save_file(Document *doc, const char *path);
+
+/* Serializes `doc` to markdown into `out` (cleared first). The single source of truth for
+   markdown serialization -- both document_save_file and document_compute_stats use it. */
+void document_serialize(Document *doc, StrBuf *out);
+
+typedef struct {
+    long byteSize;  /* length of the serialized markdown, in bytes */
+    int lineCount;  /* number of '\n' bytes in the serialized markdown (wc -l semantics) */
+} DocStats;
+
+/* Computes size/line-count stats for `doc` via document_serialize, without touching disk. */
+void document_compute_stats(Document *doc, DocStats *out);
+
+#endif
diff --git a/src/render.c b/src/render.c
new file mode 100644
index 0000000..675a516
--- /dev/null
+++ b/src/render.c
@@ -0,0 +1,507 @@
+#include "render.h"
+#include "clay.h"
+#include "theme.h"
+#include "fonts.h"
+#include "inline_parse.h"
+#include "wrap.h"
+#include "hittest.h"
+#include "markdown_io.h"
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+typedef enum { RK_PLAIN, RK_BOLD, RK_ITALIC, RK_BOLD_ITALIC, RK_CODE, RK_LINK, RK_MARKER } RenderKind;
+
+typedef struct {
+    RenderKind kind;
+    int start, end; /* byte range in the block's raw text to actually display */
+} RenderRun;
+
+typedef struct {
+    Font *fonts;
+    LayoutCache *cache;
+    float contentWidth;
+    int cursorBlock;
+    int cursorOffset;
+} RenderCtx;
+
+typedef struct {
+    Font *fonts;
+    RenderRun *runs;
+    bool boldBase;
+    int baseFontSize;
+} MeasureCtx;
+
+/* Small per-frame bump allocator for text that Clay needs to keep pointing at after this
+   function returns (e.g. a formatted "3." list marker) — Clay stores the Clay_String's
+   pointer, not a copy, so it must stay valid until this frame's render commands are drawn. */
+#define SCRATCH_SIZE (64 * 1024)
+static char g_scratch[SCRATCH_SIZE];
+static int g_scratchPos;
+
+static char *scratch_alloc(int n) {
+    if (g_scratchPos + n > SCRATCH_SIZE) n = SCRATCH_SIZE - g_scratchPos;
+    if (n < 0) n = 0;
+    char *p = g_scratch + g_scratchPos;
+    g_scratchPos += n;
+    return p;
+}
+
+static void resolve_style(RenderKind kind, bool boldBase, int baseFontSize, Clay_Color baseColor,
+                           int *fontId, int *fontSize, Clay_Color *color) {
+    bool mono = (kind == RK_CODE);
+    bool bold = boldBase || kind == RK_BOLD || kind == RK_BOLD_ITALIC;
+    bool italic = kind == RK_ITALIC || kind == RK_BOLD_ITALIC;
+    if (kind == RK_MARKER) { mono = false; bold = false; italic = false; }
+    *fontId = font_index(mono, bold, italic);
+    *fontSize = baseFontSize;
+    switch (kind) {
+        case RK_MARKER: *color = COL_MARKER; break;
+        case RK_LINK:   *color = COL_LINK; break;
+        case RK_CODE:   *color = COL_CODE_TEXT; break;
+        default:        *color = baseColor; break;
+    }
+}
+
+static float measure_cb(void *ctxV, int runIndex, const char *text, int start, int len) {
+    if (len <= 0) return 0.0f;
+    MeasureCtx *ctx = (MeasureCtx *)ctxV;
+    RenderKind kind = ctx->runs[runIndex].kind;
+    int fontId, fontSize; Clay_Color color;
+    resolve_style(kind, ctx->boldBase, ctx->baseFontSize, g_theme.text, &fontId, &fontSize, &color);
+
+    char buf[1024];
+    int n = len;
+    if (n >= (int)sizeof(buf)) n = sizeof(buf) - 1;
+    memcpy(buf, text + start, (size_t)n);
+    buf[n] = '\0';
+    Vector2 sz = MeasureTextEx(ctx->fonts[fontId], buf, (float)fontSize, 0.0f);
+    return sz.x;
+}
+
+/* Expands parsed inline runs into RenderRuns: styled markup is shown as plain rendered text
+   with markers hidden, unless the cursor sits inside that run, in which case the raw markers
+   are revealed (dimmed) around the still-styled content — WYSIWYG with an editable escape hatch. */
+static void expand_runs(InlineRunList *runs, int cursorBlock, int cursorOffset, int thisBlockIndex,
+                         RenderRun **outArr, int *outCount) {
+    int cap = runs->count * 3 + 1;
+    RenderRun *arr = malloc((size_t)cap * sizeof(RenderRun));
+    int n = 0;
+    bool hasCursor = (thisBlockIndex == cursorBlock);
+
+    for (int i = 0; i < runs->count; i++) {
+        InlineRun *r = &runs->runs[i];
+        if (r->kind == RUN_PLAIN) {
+            arr[n++] = (RenderRun){ RK_PLAIN, r->contentStart, r->contentEnd };
+            continue;
+        }
+        if (r->kind == RUN_CODE) {
+            /* Inline code markers are always hidden; the chip background carries the meaning. */
+            arr[n++] = (RenderRun){ RK_CODE, r->contentStart, r->contentEnd };
+            continue;
+        }
+
+        RenderKind styleKind = r->kind == RUN_BOLD ? RK_BOLD
+                              : r->kind == RUN_ITALIC ? RK_ITALIC
+                              : r->kind == RUN_BOLD_ITALIC ? RK_BOLD_ITALIC
+                              : RK_LINK;
+        bool focused = hasCursor && cursorOffset >= r->rawStart && cursorOffset <= r->rawEnd;
+        if (!focused) {
+            arr[n++] = (RenderRun){ styleKind, r->contentStart, r->contentEnd };
+        } else {
+            if (r->contentStart > r->rawStart) arr[n++] = (RenderRun){ RK_MARKER, r->rawStart, r->contentStart };
+            arr[n++] = (RenderRun){ styleKind, r->contentStart, r->contentEnd };
+            if (r->rawEnd > r->contentEnd) arr[n++] = (RenderRun){ RK_MARKER, r->contentEnd, r->rawEnd };
+        }
+    }
+
+    *outArr = arr;
+    *outCount = n;
+}
+
+static int compute_list_number(Document *doc, int blockIndex) {
+    int n = 1;
+    int i = blockIndex - 1;
+    while (i >= 0 && doc->blocks[i].type == BLOCK_NUMBERED) { n++; i--; }
+    return n;
+}
+
+static void emit_wrapped_lines(RenderCtx *ctx, int blockIndex, const char *text, RenderRun *runs, int runCount,
+                                float availWidth, int baseFontSize, bool boldBase, Clay_Color baseColor) {
+    WrapRunSpan *spans = malloc((size_t)(runCount > 0 ? runCount : 1) * sizeof(WrapRunSpan));
+    for (int i = 0; i < runCount; i++) { spans[i].start = runs[i].start; spans[i].end = runs[i].end; }
+
+    MeasureCtx mctx = { ctx->fonts, runs, boldBase, baseFontSize };
+    WrapResult wr;
+    wrap_layout(text, spans, runCount, availWidth, measure_cb, &mctx, &wr);
+    free(spans);
+
+    float lineHeight = (float)baseFontSize * LINE_HEIGHT_MULT;
+
+    for (int li = 0; li < wr.count; li++) {
+        WrapLine *wl = &wr.lines[li];
+        int srcStart = 0, srcEnd = 0;
+        if (wl->count > 0) { srcStart = wl->segs[0].start; srcEnd = wl->segs[wl->count - 1].end; }
+
+        int lineIdx = layout_cache_add_line(ctx->cache, blockIndex, li, srcStart, srcEnd);
+
+        CLAY(CLAY_IDI("Line", lineIdx), {
+            .layout = {
+                .layoutDirection = CLAY_LEFT_TO_RIGHT,
+                .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(lineHeight) },
+                .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
+            },
+        }) {
+            for (int si = 0; si < wl->count; si++) {
+                WrapSegment *ws = &wl->segs[si];
+                RenderKind kind = runs[ws->runIndex].kind;
+                int fontId, fontSize; Clay_Color color;
+                resolve_style(kind, boldBase, baseFontSize, baseColor, &fontId, &fontSize, &color);
+
+                int segIdx = layout_cache_add_seg(ctx->cache, lineIdx, ws->start, ws->end, fontId, fontSize);
+                Clay_String txt = { .isStaticallyAllocated = false, .length = ws->end - ws->start, .chars = text + ws->start };
+                Clay_TextElementConfig tc = { .fontId = (uint16_t)fontId, .fontSize = (uint16_t)fontSize, .textColor = color, .wrapMode = CLAY_TEXT_WRAP_NONE };
+
+                if (kind == RK_CODE) {
+                    CLAY(CLAY_IDI("Seg", segIdx), {
+                        .layout = { .padding = { 5, 5, 1, 1 } },
+                        .backgroundColor = COL_CODE_BG,
+                        .cornerRadius = CLAY_CORNER_RADIUS(4),
+                    }) {
+                        CLAY_TEXT(txt, tc);
+                    }
+                } else {
+                    CLAY(CLAY_IDI("Seg", segIdx), {
+                        .layout = { .sizing = { CLAY_SIZING_FIT(0), CLAY_SIZING_FIT(0) } },
+                    }) {
+                        CLAY_TEXT(txt, tc);
+                    }
+                }
+            }
+        }
+        layout_cache_close_line(ctx->cache, lineIdx);
+    }
+
+    wrap_result_free(&wr);
+}
+
+static void emit_code_block(RenderCtx *ctx, int blockIndex, const char *text, int len) {
+    int fontId = FONT_MONO_REGULAR;
+    int fontSize = FS_CODE;
+    float lineHeight = (float)fontSize * LINE_HEIGHT_MULT;
+    int lineInBlock = 0;
+    int pos = 0;
+
+    for (;;) {
+        int start = pos;
+        while (pos < len && text[pos] != '\n') pos++;
+        int end = pos;
+
+        int lineIdx = layout_cache_add_line(ctx->cache, blockIndex, lineInBlock, start, end);
+        CLAY(CLAY_IDI("Line", lineIdx), {
+            .layout = {
+                .layoutDirection = CLAY_LEFT_TO_RIGHT,
+                .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(lineHeight) },
+                .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
+            },
+        }) {
+            if (end > start) {
+                int segIdx = layout_cache_add_seg(ctx->cache, lineIdx, start, end, fontId, fontSize);
+                Clay_String txt = { .isStaticallyAllocated = false, .length = end - start, .chars = text + start };
+                CLAY(CLAY_IDI("Seg", segIdx), {
+                    .layout = { .sizing = { CLAY_SIZING_FIT(0), CLAY_SIZING_FIT(0) } },
+                }) {
+                    CLAY_TEXT(txt, CLAY_TEXT_CONFIG({ .fontId = (uint16_t)fontId, .fontSize = (uint16_t)fontSize, .textColor = COL_CODEBLOCK_TEXT, .wrapMode = CLAY_TEXT_WRAP_NONE }));
+                }
+            }
+        }
+        layout_cache_close_line(ctx->cache, lineIdx);
+
+        if (pos >= len) break;
+        pos++;
+        lineInBlock++;
+    }
+}
+
+static void emit_block(RenderCtx *ctx, Document *doc, int blockIndex) {
+    Block *b = &doc->blocks[blockIndex];
+
+    int baseFontSize = FS_BODY;
+    bool boldBase = false;
+    Clay_Color baseColor = g_theme.text;
+    int topPad = 3;
+
+    switch (b->type) {
+        case BLOCK_H1: baseFontSize = FS_H1; boldBase = true; topPad = 22; break;
+        case BLOCK_H2: baseFontSize = FS_H2; boldBase = true; topPad = 16; break;
+        case BLOCK_H3: baseFontSize = FS_H3; boldBase = true; topPad = 10; break;
+        case BLOCK_QUOTE: baseColor = COL_TEXT_QUOTE; break;
+        default: break;
+    }
+
+    Clay_ElementDeclaration decl = {0};
+    decl.layout.layoutDirection = CLAY_TOP_TO_BOTTOM;
+    decl.layout.sizing.width = CLAY_SIZING_FIXED(ctx->contentWidth);
+    decl.layout.sizing.height = CLAY_SIZING_FIT(0);
+    decl.layout.padding.top = (uint16_t)topPad;
+    decl.layout.padding.bottom = BLOCK_GAP;
+
+    if (b->type == BLOCK_QUOTE) {
+        decl.border.color = COL_QUOTE_BORDER;
+        decl.border.width.left = 4;
+        decl.layout.padding.left = QUOTE_INDENT;
+    }
+    if (b->type == BLOCK_CODE) {
+        decl.backgroundColor = COL_CODEBLOCK_BG;
+        decl.cornerRadius = CLAY_CORNER_RADIUS(6);
+        decl.layout.padding.left = 12;
+        decl.layout.padding.right = 12;
+        decl.layout.padding.top = (uint16_t)(topPad + 8);
+        decl.layout.padding.bottom = (uint16_t)(BLOCK_GAP + 8);
+    }
+
+    CLAY(CLAY_IDI("Block", blockIndex), decl) {
+        if (b->type == BLOCK_CODE) {
+            emit_code_block(ctx, blockIndex, b->text.data, b->text.len);
+        } else if (b->type == BLOCK_BULLET || b->type == BLOCK_NUMBERED) {
+            InlineRunList runs;
+            inline_parse(b->text.data, b->text.len, &runs);
+            RenderRun *rr; int rc;
+            expand_runs(&runs, ctx->cursorBlock, ctx->cursorOffset, blockIndex, &rr, &rc);
+            inline_runs_free(&runs);
+
+            CLAY_AUTO_ID({
+                .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT, .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIT(0) } },
+            }) {
+                CLAY_AUTO_ID({
+                    .layout = { .sizing = { CLAY_SIZING_FIXED(LIST_INDENT), CLAY_SIZING_FIT(0) } },
+                }) {
+                    Clay_String markerText;
+                    if (b->type == BLOCK_BULLET) {
+                        markerText = CLAY_STRING("\xE2\x80\xA2");
+                    } else {
+                        char *buf = scratch_alloc(16);
+                        int num = compute_list_number(doc, blockIndex);
+                        int n = snprintf(buf, 16, "%d.", num);
+                        markerText = (Clay_String){ .isStaticallyAllocated = false, .length = n, .chars = buf };
+                    }
+                    CLAY_TEXT(markerText, CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_REGULAR, .fontSize = (uint16_t)baseFontSize, .textColor = g_theme.text }));
+                }
+                CLAY_AUTO_ID({
+                    .layout = { .layoutDirection = CLAY_TOP_TO_BOTTOM, .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIT(0) } },
+                }) {
+                    emit_wrapped_lines(ctx, blockIndex, b->text.data, rr, rc, ctx->contentWidth - LIST_INDENT, baseFontSize, boldBase, baseColor);
+                }
+            }
+            free(rr);
+        } else {
+            InlineRunList runs;
+            inline_parse(b->text.data, b->text.len, &runs);
+            RenderRun *rr; int rc;
+            expand_runs(&runs, ctx->cursorBlock, ctx->cursorOffset, blockIndex, &rr, &rc);
+            inline_runs_free(&runs);
+
+            float avail = ctx->contentWidth - (b->type == BLOCK_QUOTE ? QUOTE_INDENT : 0);
+            emit_wrapped_lines(ctx, blockIndex, b->text.data, rr, rc, avail, baseFontSize, boldBase, baseColor);
+            free(rr);
+        }
+    }
+}
+
+static void modal_text_line(const char *text) {
+    Clay_String s = { .isStaticallyAllocated = true, .length = (int)strlen(text), .chars = text };
+    CLAY_TEXT(s, CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_REGULAR, .fontSize = FS_MODAL, .textColor = g_theme.text, .wrapMode = CLAY_TEXT_WRAP_NONE }));
+}
+
+/* A clickable modal button: highlights on hover, and sets *action to `thisAction` on click. */
+static void modal_button(Clay_ElementId id, const char *label, ModalAction *action, ModalAction thisAction) {
+    bool hovered = Clay_PointerOver(id);
+    CLAY(id, {
+        .layout = {
+            .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(36) },
+            .padding = { 12, 12, 0, 0 },
+            .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
+        },
+        .backgroundColor = hovered ? COL_BUTTON_HOVER : g_theme.bg,
+        .cornerRadius = CLAY_CORNER_RADIUS(4),
+        .border = { .color = COL_MODAL_BORDER, .width = CLAY_BORDER_OUTSIDE(1) },
+    }) {
+        Clay_String s = { .isStaticallyAllocated = true, .length = (int)strlen(label), .chars = label };
+        CLAY_TEXT(s, CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_REGULAR, .fontSize = FS_MODAL, .textColor = g_theme.text }));
+    }
+    if (hovered && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) *action = thisAction;
+}
+
+/* Draws the quit-confirm or shortcuts-help modal (whichever `mode` calls for) as a floating,
+   dimmed overlay on top of everything else, and returns the action the user's click resolved
+   to this frame (MODAL_ACTION_NONE if none). No-op when mode == MODE_EDITING. */
+static ModalAction emit_modal(AppMode mode, float winW, float winH) {
+    ModalAction action = MODAL_ACTION_NONE;
+    if (mode == MODE_EDITING) return action;
+
+    CLAY(CLAY_ID("ModalScrim"), {
+        .layout = {
+            .sizing = { CLAY_SIZING_FIXED(winW), CLAY_SIZING_FIXED(winH) },
+            .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
+        },
+        .backgroundColor = COL_MODAL_SCRIM,
+        .floating = { .attachTo = CLAY_ATTACH_TO_ROOT, .zIndex = 10 },
+    }) {
+        if (Clay_PointerOver(CLAY_ID("ModalScrim")) && !Clay_PointerOver(CLAY_ID("ModalBox")) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
+            action = (mode == MODE_QUIT_CONFIRM) ? MODAL_ACTION_CANCEL : MODAL_ACTION_CLOSE_HELP;
+        }
+
+        CLAY(CLAY_ID("ModalBox"), {
+            .layout = {
+                .layoutDirection = CLAY_TOP_TO_BOTTOM,
+                .sizing = { CLAY_SIZING_FIXED(mode == MODE_QUIT_CONFIRM ? 380 : 420), CLAY_SIZING_FIT(0) },
+                .padding = CLAY_PADDING_ALL(24),
+                .childGap = 12,
+            },
+            .backgroundColor = g_theme.bg,
+            .cornerRadius = CLAY_CORNER_RADIUS(8),
+            .border = { .color = COL_MODAL_BORDER, .width = CLAY_BORDER_OUTSIDE(1) },
+        }) {
+            if (mode == MODE_QUIT_CONFIRM) {
+                CLAY_TEXT(CLAY_STRING("Spara \xC3\xA4ndringar innan du avslutar?"),
+                          CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_BOLD, .fontSize = FS_MODAL, .textColor = g_theme.text }));
+                modal_button(CLAY_ID("BtnSaveQuit"), "Spara och avsluta", &action, MODAL_ACTION_SAVE_QUIT);
+                modal_button(CLAY_ID("BtnDiscardQuit"), "Avsluta utan att spara", &action, MODAL_ACTION_DISCARD_QUIT);
+                modal_button(CLAY_ID("BtnCancel"), "Avbryt", &action, MODAL_ACTION_CANCEL);
+            } else {
+                CLAY_TEXT(CLAY_STRING("Kortkommandon"),
+                          CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_BOLD, .fontSize = FS_MODAL, .textColor = g_theme.text }));
+                modal_text_line("Piltangenter \xE2\x80\x94 flytta mark\xC3\xB6ren");
+                modal_text_line("Shift + piltangenter \xE2\x80\x94 markera text");
+                modal_text_line("Musklick + drag \xE2\x80\x94 markera text");
+                modal_text_line("Ctrl+S \xE2\x80\x94 Spara");
+                modal_text_line("Ctrl+C / Ctrl+V \xE2\x80\x94 Kopiera / klistra in");
+                modal_text_line("Ctrl+Z / Ctrl+Shift+Z \xE2\x80\x94 \xC3\x85ngra / g\xC3\xB6r om");
+                modal_text_line("Ctrl+X \xE2\x80\x94 Avsluta");
+                modal_text_line("F1 \xE2\x80\x94 Den h\xC3\xA4r hj\xC3\xA4lpen");
+                modal_text_line("Enter \xE2\x80\x94 Ny rad / nytt block");
+                modal_text_line("Backspace / Delete \xE2\x80\x94 Ta bort tecken");
+                modal_text_line("Home / End \xE2\x80\x94 Radens b\xC3\xB6rjan/slut");
+                modal_button(CLAY_ID("BtnCloseHelp"), "St\xC3\xA4ng", &action, MODAL_ACTION_CLOSE_HELP);
+            }
+        }
+    }
+    return action;
+}
+
+Clay_Dimensions hush_measure_text(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) {
+    Font *fonts = (Font *)userData;
+    Font font = fonts[config->fontId];
+    if (font.glyphs == NULL) font = GetFontDefault();
+
+    char buf[1024];
+    int n = text.length;
+    if (n >= (int)sizeof(buf)) n = sizeof(buf) - 1;
+    if (n > 0) memcpy(buf, text.chars, (size_t)n);
+    buf[n] = '\0';
+
+    Vector2 sz = MeasureTextEx(font, buf, (float)config->fontSize, 0.0f);
+    return (Clay_Dimensions){ sz.x, sz.y > 0 ? sz.y : (float)config->fontSize };
+}
+
+ModalAction render_frame(EditorState *ed, LayoutCache *cache, Font *fonts, float caretBlinkT, AppMode mode) {
+    g_scratchPos = 0;
+    layout_cache_reset(cache);
+
+    float winW = (float)GetScreenWidth();
+    float winH = (float)GetScreenHeight();
+    float contentWidth = winW - 2.0f * CONTENT_SIDE_PADDING;
+    if (contentWidth > CONTENT_MAX_WIDTH) contentWidth = CONTENT_MAX_WIDTH;
+    if (contentWidth < 100.0f) contentWidth = 100.0f;
+
+    RenderCtx ctx = { fonts, cache, contentWidth, ed->cursorBlock, ed->cursorOffset };
+
+    Clay_BeginLayout();
+
+    CLAY(CLAY_ID("Root"), {
+        .layout = { .sizing = { CLAY_SIZING_FIXED(winW), CLAY_SIZING_FIXED(winH) }, .layoutDirection = CLAY_TOP_TO_BOTTOM },
+        .backgroundColor = g_theme.bg,
+    }) {
+        CLAY(CLAY_ID("ScrollArea"), {
+            .layout = {
+                .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0) },
+                .childAlignment = { .x = CLAY_ALIGN_X_CENTER },
+                .padding = { 0, 0, 28, 48 },
+            },
+            .clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() },
+        }) {
+            CLAY(CLAY_ID("ContentColumn"), {
+                .layout = { .layoutDirection = CLAY_TOP_TO_BOTTOM, .sizing = { CLAY_SIZING_FIXED(contentWidth), CLAY_SIZING_FIT(0) } },
+            }) {
+                for (int i = 0; i < ed->doc.count; i++) {
+                    emit_block(&ctx, &ed->doc, i);
+                }
+            }
+        }
+
+        CLAY(CLAY_ID("StatusBar"), {
+            .layout = {
+                .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(STATUSBAR_HEIGHT) },
+                .layoutDirection = CLAY_LEFT_TO_RIGHT,
+                .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
+                .padding = { STATUSBAR_PADDING, STATUSBAR_PADDING, 0, 0 },
+            },
+            .backgroundColor = COL_STATUSBAR_BG,
+        }) {
+            DocStats stats;
+            document_compute_stats(&ed->doc, &stats);
+            const char *name = editor_display_name(ed);
+
+            char tmp[512];
+            int n = snprintf(tmp, sizeof tmp, "%s%s  \xC2\xB7  %.1f KB  \xC2\xB7  %d line%s",
+                              ed->dirty ? "*" : "", name, stats.byteSize / 1024.0,
+                              stats.lineCount, stats.lineCount == 1 ? "" : "s");
+            if (n > (int)sizeof tmp) n = (int)sizeof tmp;
+            char *scratchBuf = scratch_alloc(n);
+            memcpy(scratchBuf, tmp, (size_t)n);
+            Clay_String statusText = { .isStaticallyAllocated = false, .length = n, .chars = scratchBuf };
+            CLAY_TEXT(statusText, CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_REGULAR, .fontSize = FS_STATUSBAR,
+                                                       .textColor = COL_STATUSBAR_TEXT, .wrapMode = CLAY_TEXT_WRAP_NONE }));
+        }
+    }
+
+    ModalAction modalAction = emit_modal(mode, winW, winH);
+
+    Clay_RenderCommandArray cmds = Clay_EndLayout(GetFrameTime());
+    Clay_Raylib_Render(cmds, fonts);
+
+    if (mode == MODE_EDITING && editor_has_selection(ed)) {
+        int sb, so, eb, eo;
+        editor_selection_range(ed, &sb, &so, &eb, &eo);
+        Color selColor = {
+            (unsigned char)roundf(COL_SELECTION.r), (unsigned char)roundf(COL_SELECTION.g),
+            (unsigned char)roundf(COL_SELECTION.b), (unsigned char)roundf(COL_SELECTION.a),
+        };
+        for (int i = 0; i < cache->lineCount; i++) {
+            CachedLine *line = &cache->lines[i];
+            if (line->blockIndex < sb || line->blockIndex > eb) continue;
+            int rangeStart = (line->blockIndex == sb) ? so : 0;
+            int rangeEnd = (line->blockIndex == eb) ? eo : ed->doc.blocks[line->blockIndex].text.len;
+            float x, y, w, h;
+            if (hittest_line_range_box(cache, &ed->doc, fonts, i, rangeStart, rangeEnd, &x, &y, &w, &h)) {
+                DrawRectangle((int)x, (int)y, (int)w, (int)h, selColor);
+            }
+        }
+    }
+
+    if (mode == MODE_EDITING) {
+        float cx, cy, ch;
+        if (hittest_caret(cache, &ed->doc, fonts, ed->cursorBlock, ed->cursorOffset, &cx, &cy, &ch)) {
+            if (fmodf(caretBlinkT, 1.0f) < 0.5f) {
+                Color cursorColor = {
+                    (unsigned char)roundf(g_theme.cursor.r), (unsigned char)roundf(g_theme.cursor.g),
+                    (unsigned char)roundf(g_theme.cursor.b), (unsigned char)roundf(g_theme.cursor.a),
+                };
+                DrawRectangle((int)cx, (int)cy, 2, (int)ch, cursorColor);
+            }
+        }
+    }
+
+    return modalAction;
+}
diff --git a/src/render.h b/src/render.h
new file mode 100644
index 0000000..ef2a958
--- /dev/null
+++ b/src/render.h
@@ -0,0 +1,29 @@
+#ifndef HUSH_RENDER_H
+#define HUSH_RENDER_H
+
+#include "editor.h"
+#include "layout_cache.h"
+#include "app_mode.h"
+#include "raylib.h"
+#include "clay.h"
+
+/* Declared (not defined) here because vendor/clay_renderer_raylib.c has no header of its own;
+   it is compiled once, into src/clay_impl.c. */
+void Clay_Raylib_Initialize(int width, int height, const char *title, unsigned int flags);
+void Clay_Raylib_Render(Clay_RenderCommandArray renderCommands, Font *fonts);
+void Clay_Raylib_Close(void);
+
+/* Registered with Clay_SetMeasureTextFunction (userData = the Font[FONT_COUNT] array).
+   Uses raylib's own UTF-8 aware MeasureTextEx, unlike the byte-indexed helper that ships
+   with Clay's raylib renderer, so accented characters (e.g. Swedish å ä ö) measure correctly. */
+Clay_Dimensions hush_measure_text(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData);
+
+/* Builds the Clay tree for the current editor state, populates `cache` fresh (consumed by
+   hit-testing on the *next* frame's input), issues the Clay render commands, and draws the
+   blinking caret on top. Call between BeginDrawing()/EndDrawing(), after
+   Clay_SetPointerState/Clay_SetLayoutDimensions/Clay_UpdateScrollContainers.
+   When `mode` is not MODE_EDITING, also draws the corresponding modal on top and returns
+   whichever action the user's click resolved to this frame (MODAL_ACTION_NONE otherwise). */
+ModalAction render_frame(EditorState *ed, LayoutCache *cache, Font *fonts, float caretBlinkT, AppMode mode);
+
+#endif
diff --git a/src/strbuf.c b/src/strbuf.c
new file mode 100644
index 0000000..4240556
--- /dev/null
+++ b/src/strbuf.c
@@ -0,0 +1,59 @@
+#include "strbuf.h"
+#include <stdlib.h>
+#include <string.h>
+
+void sb_init(StrBuf *sb) {
+    sb->data = NULL;
+    sb->len = 0;
+    sb->cap = 0;
+}
+
+void sb_free(StrBuf *sb) {
+    free(sb->data);
+    sb->data = NULL;
+    sb->len = 0;
+    sb->cap = 0;
+}
+
+void sb_reserve(StrBuf *sb, int extra) {
+    int needed = sb->len + extra + 1; /* keep a spare byte for a null terminator */
+    if (needed <= sb->cap) return;
+    int newCap = sb->cap > 0 ? sb->cap : 16;
+    while (newCap < needed) newCap *= 2;
+    sb->data = realloc(sb->data, (size_t)newCap);
+    sb->cap = newCap;
+}
+
+void sb_set(StrBuf *sb, const char *s, int n) {
+    sb->len = 0;
+    sb_insert(sb, 0, s, n);
+}
+
+void sb_insert(StrBuf *sb, int pos, const char *s, int n) {
+    if (n <= 0) return;
+    sb_reserve(sb, n);
+    memmove(sb->data + pos + n, sb->data + pos, (size_t)(sb->len - pos));
+    memcpy(sb->data + pos, s, (size_t)n);
+    sb->len += n;
+    sb->data[sb->len] = '\0';
+}
+
+void sb_delete(StrBuf *sb, int pos, int n) {
+    if (n <= 0) return;
+    memmove(sb->data + pos, sb->data + pos + n, (size_t)(sb->len - pos - n));
+    sb->len -= n;
+    if (sb->data) sb->data[sb->len] = '\0';
+}
+
+void sb_append(StrBuf *sb, const char *s, int n) {
+    sb_insert(sb, sb->len, s, n);
+}
+
+void sb_append_char(StrBuf *sb, char c) {
+    sb_append(sb, &c, 1);
+}
+
+void sb_clear(StrBuf *sb) {
+    sb->len = 0;
+    if (sb->data) sb->data[0] = '\0';
+}
diff --git a/src/strbuf.h b/src/strbuf.h
new file mode 100644
index 0000000..605ddbd
--- /dev/null
+++ b/src/strbuf.h
@@ -0,0 +1,20 @@
+#ifndef HUSH_STRBUF_H
+#define HUSH_STRBUF_H
+
+typedef struct {
+    char *data;
+    int len;
+    int cap;
+} StrBuf;
+
+void sb_init(StrBuf *sb);
+void sb_free(StrBuf *sb);
+void sb_reserve(StrBuf *sb, int extra);
+void sb_set(StrBuf *sb, const char *s, int n);
+void sb_insert(StrBuf *sb, int pos, const char *s, int n);
+void sb_delete(StrBuf *sb, int pos, int n);
+void sb_append(StrBuf *sb, const char *s, int n);
+void sb_append_char(StrBuf *sb, char c);
+void sb_clear(StrBuf *sb);
+
+#endif
diff --git a/src/test_main.c b/src/test_main.c
new file mode 100644
index 0000000..9e42c58
--- /dev/null
+++ b/src/test_main.c
@@ -0,0 +1,439 @@
+/* Standalone logic test for the document/editor/markdown-io layer, with no window or GPU
+   context required. Exercises editor.c the same way keystrokes would, and checks the
+   resulting Document state and markdown round-trip directly. */
+#include "editor.h"
+#include "markdown_io.h"
+#include "inline_parse.h"
+#include "undo.h"
+#include "config.h"
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+static int g_failures = 0;
+
+#define CHECK(cond, msg) do { \
+    if (!(cond)) { g_failures++; printf("FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); } \
+    else { printf("ok:   %s\n", msg); } \
+} while (0)
+
+static int text_eq(Block *b, const char *s) {
+    int n = (int)strlen(s);
+    return b->text.len == n && memcmp(b->text.data, s, (size_t)n) == 0;
+}
+
+static void type_str(EditorState *ed, const char *s) {
+    for (const char *p = s; *p; p++) editor_insert_utf8(ed, p, 1);
+}
+
+static void test_heading_autoformat(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "# ");
+    CHECK(ed.doc.blocks[0].type == BLOCK_H1, "typing '# ' converts block to H1");
+    CHECK(text_eq(&ed.doc.blocks[0], ""), "H1 marker text is stripped");
+    type_str(&ed, "Titel");
+    CHECK(text_eq(&ed.doc.blocks[0], "Titel"), "H1 block holds typed text");
+    editor_free(&ed);
+}
+
+static void test_bullet_list_continuation(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "- Item1");
+    CHECK(ed.doc.blocks[0].type == BLOCK_BULLET, "typing '- ' converts block to BULLET");
+    CHECK(text_eq(&ed.doc.blocks[0], "Item1"), "bullet block holds typed text");
+
+    editor_enter(&ed);
+    CHECK(ed.doc.count == 2, "Enter in a bullet item adds a new block");
+    CHECK(ed.doc.blocks[1].type == BLOCK_BULLET, "Enter continues the bullet list");
+    CHECK(ed.cursorBlock == 1 && ed.cursorOffset == 0, "cursor moves to the new empty bullet item");
+
+    /* Enter again on the still-empty bullet item exits the list. */
+    editor_enter(&ed);
+    CHECK(ed.doc.count == 2, "Enter on an empty list item does not create a new block");
+    CHECK(ed.doc.blocks[1].type == BLOCK_PARAGRAPH, "Enter on an empty list item exits back to a paragraph");
+    editor_free(&ed);
+}
+
+static void test_numbered_list_renumbers_on_save(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "1. First");
+    editor_enter(&ed);
+    type_str(&ed, "Second");
+    editor_enter(&ed);
+    type_str(&ed, "Third");
+    CHECK(ed.doc.count == 3 && ed.doc.blocks[2].type == BLOCK_NUMBERED, "three numbered items exist");
+
+    document_save_file(&ed.doc, "test_numbered.md");
+    FILE *f = fopen("test_numbered.md", "rb");
+    char buf[256] = {0};
+    size_t n = fread(buf, 1, sizeof buf - 1, f);
+    fclose(f);
+    (void)n;
+    CHECK(strstr(buf, "1. First") && strstr(buf, "2. Second") && strstr(buf, "3. Third"),
+          "saved file renumbers the list sequentially");
+    editor_free(&ed);
+}
+
+static void test_backspace_demotes_then_merges(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "## Heading");
+    editor_move_home(&ed, false);
+    CHECK(ed.doc.blocks[0].type == BLOCK_H2, "starts as H2");
+
+    editor_backspace(&ed);
+    CHECK(ed.doc.blocks[0].type == BLOCK_PARAGRAPH, "backspace at block start demotes H2 to paragraph first");
+    CHECK(text_eq(&ed.doc.blocks[0], "Heading"), "demotion keeps the text intact");
+
+    editor_move_end(&ed, false);
+    editor_enter(&ed);
+    type_str(&ed, "Second");
+    CHECK(ed.doc.count == 2, "now there are two paragraphs");
+
+    ed.cursorBlock = 1;
+    ed.cursorOffset = 0;
+    ed.selAnchorBlock = 1;
+    ed.selAnchorOffset = 0;
+    editor_backspace(&ed);
+    CHECK(ed.doc.count == 1, "backspace at start of a paragraph merges into the previous block");
+    CHECK(text_eq(&ed.doc.blocks[0], "HeadingSecond"), "merge concatenates the text");
+    editor_free(&ed);
+}
+
+static void test_code_fence(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "```");
+    editor_enter(&ed);
+    CHECK(ed.doc.blocks[0].type == BLOCK_CODE, "typing ``` then Enter opens a code block");
+    CHECK(text_eq(&ed.doc.blocks[0], ""), "code block starts empty");
+
+    type_str(&ed, "int main(void) {");
+    editor_enter(&ed);
+    type_str(&ed, "    return 0;");
+    editor_enter(&ed);
+    type_str(&ed, "}");
+    editor_enter(&ed);
+    type_str(&ed, "```");
+    editor_enter(&ed);
+
+    CHECK(ed.doc.blocks[0].type == BLOCK_CODE, "block stays CODE while inside the fence");
+    CHECK(text_eq(&ed.doc.blocks[0], "int main(void) {\n    return 0;\n}"), "embedded newlines preserved, closing fence stripped");
+    CHECK(ed.doc.count == 2 && ed.doc.blocks[1].type == BLOCK_PARAGRAPH, "closing ``` exits to a new paragraph");
+    editor_free(&ed);
+}
+
+static void test_utf8_backspace(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "kaf\xc3\xa9"); /* "café" */
+    CHECK(ed.doc.blocks[0].text.len == 5, "café is 5 bytes (é is 2 bytes in UTF-8)");
+    editor_backspace(&ed);
+    CHECK(text_eq(&ed.doc.blocks[0], "kaf"), "backspace removes the whole 'é' codepoint, not one byte");
+    editor_free(&ed);
+}
+
+static void test_inline_parse(void) {
+    const char *s = "a **b** c *d* e `f` g [h](i)";
+    InlineRunList runs;
+    inline_parse(s, (int)strlen(s), &runs);
+
+    int haveBold = 0, haveItalic = 0, haveCode = 0, haveLink = 0;
+    for (int i = 0; i < runs.count; i++) {
+        InlineRun *r = &runs.runs[i];
+        if (r->kind == RUN_BOLD && r->contentEnd - r->contentStart == 1 && s[r->contentStart] == 'b') haveBold = 1;
+        if (r->kind == RUN_ITALIC && s[r->contentStart] == 'd') haveItalic = 1;
+        if (r->kind == RUN_CODE && s[r->contentStart] == 'f') haveCode = 1;
+        if (r->kind == RUN_LINK && s[r->contentStart] == 'h') haveLink = 1;
+    }
+    CHECK(haveBold, "inline_parse finds **bold**");
+    CHECK(haveItalic, "inline_parse finds *italic*");
+    CHECK(haveCode, "inline_parse finds `code`");
+    CHECK(haveLink, "inline_parse finds [link](url)");
+    inline_runs_free(&runs);
+}
+
+static void test_save_load_roundtrip(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "# Titel");
+    editor_enter(&ed);
+    type_str(&ed, "En **fet** rad med \xc3\xa5\xc3\xa4\xc3\xb6.");
+    editor_enter(&ed);
+    type_str(&ed, "> Citat");
+    editor_enter(&ed);  /* continues the blockquote with a new, empty quote block */
+    editor_enter(&ed);  /* Enter on an empty quote item exits back to a paragraph */
+    type_str(&ed, "- Punkt");
+
+    CHECK(document_save_file(&ed.doc, "test_roundtrip.md"), "save succeeds");
+
+    EditorState ed2;
+    editor_init(&ed2);
+    CHECK(editor_load(&ed2, "test_roundtrip.md"), "load succeeds");
+    CHECK(ed2.doc.count == 4, "round-tripped document has 4 blocks");
+    CHECK(ed2.doc.blocks[0].type == BLOCK_H1 && text_eq(&ed2.doc.blocks[0], "Titel"), "H1 round-trips");
+    CHECK(ed2.doc.blocks[1].type == BLOCK_PARAGRAPH && text_eq(&ed2.doc.blocks[1], "En **fet** rad med \xc3\xa5\xc3\xa4\xc3\xb6."), "paragraph with inline markup and åäö round-trips byte-for-byte");
+    CHECK(ed2.doc.blocks[2].type == BLOCK_QUOTE && text_eq(&ed2.doc.blocks[2], "Citat"), "blockquote round-trips");
+    CHECK(ed2.doc.blocks[3].type == BLOCK_BULLET && text_eq(&ed2.doc.blocks[3], "Punkt"), "bullet round-trips");
+
+    editor_free(&ed);
+    editor_free(&ed2);
+}
+
+static void test_document_clone_deep_copy(void) {
+    Document doc;
+    document_init(&doc);
+    sb_insert(&doc.blocks[0].text, 0, "hello", 5);
+    document_insert_block(&doc, 1, BLOCK_H1, "world", 5);
+
+    Document clone;
+    document_clone(&clone, &doc);
+    CHECK(clone.count == 2, "clone has the same block count");
+    CHECK(text_eq(&clone.blocks[0], "hello") && text_eq(&clone.blocks[1], "world"), "clone has matching text");
+    CHECK(clone.blocks[1].type == BLOCK_H1, "clone has matching block types");
+
+    sb_insert(&doc.blocks[0].text, 5, "XXX", 3); /* mutate the original after cloning */
+    CHECK(text_eq(&clone.blocks[0], "hello"), "clone is a deep copy, unaffected by mutating the original afterward");
+
+    document_free(&doc);
+    document_free(&clone);
+}
+
+static void test_document_compute_stats(void) {
+    EditorState ed;
+    editor_init(&ed);
+    DocStats stats;
+    document_compute_stats(&ed.doc, &stats);
+    CHECK(stats.byteSize == 1, "a fresh empty document serializes to a single newline byte");
+    CHECK(stats.lineCount == 1, "a fresh empty document counts as 1 line");
+
+    type_str(&ed, "ab");
+    editor_enter(&ed);
+    type_str(&ed, "cd");
+    document_compute_stats(&ed.doc, &stats);
+    CHECK(stats.byteSize == 7, "two-paragraph doc serializes to \"ab\\n\\ncd\\n\" (7 bytes)");
+    CHECK(stats.lineCount == 3, "the blank separator line between blocks counts too (matches wc -l)");
+    editor_free(&ed);
+}
+
+static void test_undo_typing_coalesces(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "abc");
+    CHECK(editor_undo(&ed), "undo has history after typing");
+    CHECK(text_eq(&ed.doc.blocks[0], ""), "one undo removes all of a continuously-typed run at once");
+    CHECK(!editor_undo(&ed), "a second undo finds nothing left");
+    editor_free(&ed);
+}
+
+static void test_undo_kind_switch_starts_new_group(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "abc");
+    editor_backspace(&ed);
+    editor_backspace(&ed);
+    CHECK(text_eq(&ed.doc.blocks[0], "a"), "typing 'abc' then two backspaces leaves 'a'");
+    CHECK(editor_undo(&ed), "undo #1");
+    CHECK(text_eq(&ed.doc.blocks[0], "abc"), "undo #1 undoes both backspaces together, but not the typing before them");
+    CHECK(editor_undo(&ed), "undo #2");
+    CHECK(text_eq(&ed.doc.blocks[0], ""), "undo #2 undoes the typed 'abc' as its own group");
+    editor_free(&ed);
+}
+
+static void test_undo_enter_is_own_step(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "ab");
+    editor_enter(&ed);
+    type_str(&ed, "cd");
+    CHECK(ed.doc.count == 2 && text_eq(&ed.doc.blocks[1], "cd"), "'ab' / Enter / 'cd' produced two blocks");
+
+    CHECK(editor_undo(&ed), "undo #1");
+    CHECK(ed.doc.count == 2 && text_eq(&ed.doc.blocks[1], ""), "undo #1 removes only the typed 'cd', the Enter split survives");
+    CHECK(editor_undo(&ed), "undo #2");
+    CHECK(ed.doc.count == 1 && text_eq(&ed.doc.blocks[0], "ab"), "undo #2 undoes the Enter split on its own, merging back to one block");
+    CHECK(editor_undo(&ed), "undo #3");
+    CHECK(ed.doc.count == 1 && text_eq(&ed.doc.blocks[0], ""), "undo #3 removes the typed 'ab'");
+    CHECK(!editor_undo(&ed), "no more history");
+    editor_free(&ed);
+}
+
+static void test_undo_redo_roundtrip(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "abc");
+    CHECK(editor_undo(&ed), "undo the typed 'abc'");
+    CHECK(text_eq(&ed.doc.blocks[0], ""), "back to empty");
+    CHECK(editor_redo(&ed), "redo restores it");
+    CHECK(text_eq(&ed.doc.blocks[0], "abc"), "redo brings back 'abc'");
+    CHECK(!editor_redo(&ed), "nothing left to redo");
+
+    type_str(&ed, "d");
+    CHECK(text_eq(&ed.doc.blocks[0], "abcd"), "typing after redo appends at the right cursor position");
+    CHECK(!editor_redo(&ed), "a genuinely new edit clears the redo stack");
+    editor_free(&ed);
+}
+
+static void test_snapshot_stack_cap_eviction(void) {
+    SnapshotStack s;
+    snapshot_stack_init(&s, 3);
+
+    Document doc;
+    document_init(&doc);
+    for (int i = 0; i < 5; i++) {
+        sb_clear(&doc.blocks[0].text);
+        char buf[8];
+        int n = snprintf(buf, sizeof buf, "%d", i);
+        sb_append(&doc.blocks[0].text, buf, n);
+        snapshot_stack_push(&s, &doc, 0, i);
+    }
+    CHECK(s.count == 3, "a cap-3 stack holds only 3 entries after 5 pushes");
+
+    Snapshot top;
+    CHECK(snapshot_stack_pop(&s, &top) && top.cursorOffset == 4, "most recent push is on top");
+    document_free(&top.doc);
+    CHECK(snapshot_stack_pop(&s, &top) && top.cursorOffset == 3, "next most recent is below it");
+    document_free(&top.doc);
+    CHECK(snapshot_stack_pop(&s, &top) && top.cursorOffset == 2, "oldest surviving entry is #2 (0 and 1 were evicted)");
+    document_free(&top.doc);
+    CHECK(!snapshot_stack_pop(&s, &top), "stack is now empty");
+
+    document_free(&doc);
+    snapshot_stack_free(&s);
+}
+
+static void test_selection_range_normalization(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "ab");
+    editor_enter(&ed);
+    type_str(&ed, "cd");
+
+    ed.cursorBlock = 0; ed.cursorOffset = 1;
+    ed.selAnchorBlock = 1; ed.selAnchorOffset = 1;
+    CHECK(editor_has_selection(&ed), "anchor != cursor counts as a selection");
+
+    int sb, so, eb, eo;
+    editor_selection_range(&ed, &sb, &so, &eb, &eo);
+    CHECK(sb == 0 && so == 1 && eb == 1 && eo == 1, "a backward selection (anchor after cursor) normalizes to forward document order");
+    editor_free(&ed);
+}
+
+static void test_selection_to_text_across_blocks(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "hello");
+    editor_enter(&ed);
+    type_str(&ed, "world");
+    editor_enter(&ed);
+    type_str(&ed, "again");
+
+    int len;
+    CHECK(editor_selection_to_text(&ed, &len) == NULL, "no active selection yields NULL");
+
+    ed.selAnchorBlock = 0; ed.selAnchorOffset = 3;
+    ed.cursorBlock = 2; ed.cursorOffset = 2;
+    char *text = editor_selection_to_text(&ed, &len);
+    CHECK(text != NULL, "selection_to_text returns text once a selection exists");
+    CHECK(text && strcmp(text, "lo\nworld\nag") == 0, "cross-block selection joins blocks with '\\n'");
+    free(text);
+    editor_free(&ed);
+}
+
+static void test_selection_replace_on_type(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "hello world");
+    ed.selAnchorBlock = 0; ed.selAnchorOffset = 0;
+    ed.cursorBlock = 0; ed.cursorOffset = 5;
+    editor_insert_utf8(&ed, "HI", 2);
+    CHECK(text_eq(&ed.doc.blocks[0], "HI world"), "typing over a selection replaces it");
+    CHECK(!editor_has_selection(&ed), "selection collapses after the replace");
+    CHECK(ed.cursorOffset == 2, "cursor lands right after the inserted text");
+    editor_free(&ed);
+}
+
+static void test_selection_replace_on_backspace_across_blocks(void) {
+    EditorState ed;
+    editor_init(&ed);
+    type_str(&ed, "## Heading");
+    editor_enter(&ed);
+    type_str(&ed, "world");
+    CHECK(ed.doc.count == 2 && ed.doc.blocks[0].type == BLOCK_H2, "setup: H2 'Heading' then paragraph 'world'");
+
+    ed.selAnchorBlock = 0; ed.selAnchorOffset = 3;
+    ed.cursorBlock = 1; ed.cursorOffset = 2;
+    editor_backspace(&ed);
+
+    CHECK(ed.doc.count == 1, "backspace over a cross-block selection merges into a single block");
+    CHECK(ed.doc.blocks[0].type == BLOCK_H2, "the merged block keeps the start block's type");
+    CHECK(text_eq(&ed.doc.blocks[0], "Hearld"), "merged text is start-before-selection + end-after-selection");
+    CHECK(!editor_has_selection(&ed), "selection is cleared after the merge");
+    editor_free(&ed);
+}
+
+static void test_config_parse_line(void) {
+    Config cfg = {0};
+
+    CHECK(!config_parse_line("# a comment", 11, &cfg), "a comment line returns false");
+    CHECK(!config_parse_line("", 0, &cfg), "a blank line returns false");
+    CHECK(!config_parse_line("   ", 3, &cfg), "a whitespace-only line returns false");
+
+    const char *l1 = "text_color = #1a2b3c";
+    CHECK(config_parse_line(l1, (int)strlen(l1), &cfg), "a valid hex color line returns true");
+    CHECK(cfg.textColor.r == 0x1a && cfg.textColor.g == 0x2b && cfg.textColor.b == 0x3c && cfg.textColor.a == 255,
+          "text_color parses each hex byte correctly");
+
+    const char *l2 = "bg_color=#ffffff";
+    CHECK(config_parse_line(l2, (int)strlen(l2), &cfg), "no spaces around '=' still parses");
+    CHECK(cfg.bgColor.r == 255 && cfg.bgColor.g == 255 && cfg.bgColor.b == 255, "bg_color parsed as white");
+
+    Clay_Color before = cfg.textColor;
+    const char *l3 = "text_color = not-a-color";
+    CHECK(!config_parse_line(l3, (int)strlen(l3), &cfg), "an invalid hex value returns false");
+    CHECK(cfg.textColor.r == before.r && cfg.textColor.g == before.g && cfg.textColor.b == before.b,
+          "an invalid color line leaves the previous value untouched");
+
+    const char *l4 = "unknown_key = foo";
+    CHECK(!config_parse_line(l4, (int)strlen(l4), &cfg), "an unrecognized key returns false and does not crash");
+
+    const char *l5 = "font_sans_bold = /some/path.ttf";
+    CHECK(config_parse_line(l5, (int)strlen(l5), &cfg), "a font-path key returns true");
+    CHECK(cfg.fontPaths[FONT_SANS_BOLD] != NULL && strcmp(cfg.fontPaths[FONT_SANS_BOLD], "/some/path.ttf") == 0,
+          "the font path is copied into the matching slot");
+
+    const char *l6 = "text_color = #a1b2c3\r";
+    CHECK(config_parse_line(l6, (int)strlen(l6), &cfg), "a trailing \\r is trimmed and the line still parses");
+    CHECK(cfg.textColor.r == 0xa1, "value from the \\r-trimmed line is correct");
+
+    config_free(&cfg);
+}
+
+int main(void) {
+    test_heading_autoformat();
+    test_bullet_list_continuation();
+    test_numbered_list_renumbers_on_save();
+    test_backspace_demotes_then_merges();
+    test_code_fence();
+    test_utf8_backspace();
+    test_inline_parse();
+    test_save_load_roundtrip();
+    test_document_clone_deep_copy();
+    test_document_compute_stats();
+    test_undo_typing_coalesces();
+    test_undo_kind_switch_starts_new_group();
+    test_undo_enter_is_own_step();
+    test_undo_redo_roundtrip();
+    test_snapshot_stack_cap_eviction();
+    test_selection_range_normalization();
+    test_selection_to_text_across_blocks();
+    test_selection_replace_on_type();
+    test_selection_replace_on_backspace_across_blocks();
+    test_config_parse_line();
+
+    printf("\n%s (%d failure%s)\n", g_failures == 0 ? "ALL PASS" : "SOME FAILED", g_failures, g_failures == 1 ? "" : "s");
+    return g_failures == 0 ? 0 : 1;
+}
diff --git a/src/theme.c b/src/theme.c
new file mode 100644
index 0000000..307f4c0
--- /dev/null
+++ b/src/theme.c
@@ -0,0 +1,9 @@
+#include "theme.h"
+
+Theme g_theme;
+
+void theme_init_defaults(void) {
+    g_theme.bg = DEFAULT_COL_BG;
+    g_theme.text = DEFAULT_COL_TEXT;
+    g_theme.cursor = DEFAULT_COL_CURSOR;
+}
diff --git a/src/theme.h b/src/theme.h
new file mode 100644
index 0000000..16e0636
--- /dev/null
+++ b/src/theme.h
@@ -0,0 +1,57 @@
+#ifndef HUSH_THEME_H
+#define HUSH_THEME_H
+
+#include "clay.h"
+
+/* bg/text/cursor are runtime-configurable (see Theme below, populated from the user's config
+   file); these three macros are only their built-in defaults. Every other color here stays a
+   compile-time constant -- intentionally out of config-file scope. */
+#define DEFAULT_COL_BG     (Clay_Color){253, 253, 251, 255}
+#define DEFAULT_COL_TEXT   (Clay_Color){31, 35, 40, 255}
+#define DEFAULT_COL_CURSOR (Clay_Color){31, 35, 40, 255}
+
+typedef struct {
+    Clay_Color bg;
+    Clay_Color text;
+    Clay_Color cursor;
+} Theme;
+
+extern Theme g_theme;
+void theme_init_defaults(void);
+
+#define COL_TEXT_QUOTE  (Clay_Color){90, 97, 105, 255}
+#define COL_MARKER      (Clay_Color){178, 178, 172, 255}
+#define COL_LINK        (Clay_Color){37, 99, 235, 255}
+#define COL_CODE_BG     (Clay_Color){240, 240, 236, 255}
+#define COL_CODE_TEXT   (Clay_Color){160, 40, 90, 255}
+#define COL_CODEBLOCK_BG (Clay_Color){247, 247, 244, 255}
+#define COL_CODEBLOCK_TEXT (Clay_Color){55, 55, 60, 255}
+#define COL_QUOTE_BORDER (Clay_Color){206, 206, 200, 255}
+#define COL_SCROLLBAR   (Clay_Color){210, 210, 205, 180}
+#define COL_SELECTION   (Clay_Color){96, 165, 250, 90}
+#define COL_STATUSBAR_BG   (Clay_Color){241, 241, 237, 255}
+#define COL_STATUSBAR_TEXT (Clay_Color){120, 126, 133, 255}
+#define COL_MODAL_SCRIM (Clay_Color){0, 0, 0, 120}
+#define COL_MODAL_BORDER (Clay_Color){206, 206, 200, 255}
+#define COL_BUTTON_HOVER (Clay_Color){235, 235, 231, 255}
+
+#define FS_H1 28
+#define FS_H2 23
+#define FS_H3 19
+#define FS_BODY 16
+#define FS_CODE 15
+#define FS_STATUSBAR 12
+#define FS_MODAL 15
+
+#define LINE_HEIGHT_MULT 1.5f
+
+#define CONTENT_MAX_WIDTH 760.0f
+#define CONTENT_SIDE_PADDING 32.0f
+#define BLOCK_GAP 4
+#define LIST_INDENT 26
+#define QUOTE_INDENT 14
+
+#define STATUSBAR_HEIGHT 24
+#define STATUSBAR_PADDING 10
+
+#endif
diff --git a/src/undo.c b/src/undo.c
new file mode 100644
index 0000000..4802d20
--- /dev/null
+++ b/src/undo.c
@@ -0,0 +1,82 @@
+#include "undo.h"
+#include <stdlib.h>
+#include <string.h>
+
+void snapshot_stack_init(SnapshotStack *s, int limit) {
+    s->items = NULL;
+    s->count = 0;
+    s->cap = 0;
+    s->limit = limit;
+}
+
+void snapshot_stack_clear(SnapshotStack *s) {
+    for (int i = 0; i < s->count; i++) document_free(&s->items[i].doc);
+    s->count = 0;
+}
+
+void snapshot_stack_free(SnapshotStack *s) {
+    snapshot_stack_clear(s);
+    free(s->items);
+    s->items = NULL;
+    s->cap = 0;
+}
+
+void snapshot_stack_push(SnapshotStack *s, const Document *doc, int cursorBlock, int cursorOffset) {
+    if (s->count == s->cap) {
+        int newCap = s->cap > 0 ? s->cap * 2 : 8;
+        s->items = realloc(s->items, (size_t)newCap * sizeof(Snapshot));
+        s->cap = newCap;
+    }
+    Snapshot *slot = &s->items[s->count++];
+    document_clone(&slot->doc, doc);
+    slot->cursorBlock = cursorBlock;
+    slot->cursorOffset = cursorOffset;
+
+    if (s->count > s->limit) {
+        /* Evict the oldest entry, shifting the rest down. */
+        document_free(&s->items[0].doc);
+        memmove(&s->items[0], &s->items[1], (size_t)(s->count - 1) * sizeof(Snapshot));
+        s->count--;
+    }
+}
+
+bool snapshot_stack_pop(SnapshotStack *s, Snapshot *out) {
+    if (s->count == 0) return false;
+    *out = s->items[--s->count];
+    return true;
+}
+
+void undo_manager_init(UndoManager *m, int limit) {
+    snapshot_stack_init(&m->undo, limit);
+    snapshot_stack_init(&m->redo, limit);
+    m->lastKind = EDIT_NONE;
+    m->lastBlock = -1;
+    m->lastOffset = -1;
+}
+
+void undo_manager_free(UndoManager *m) {
+    snapshot_stack_free(&m->undo);
+    snapshot_stack_free(&m->redo);
+}
+
+void undo_manager_reset(UndoManager *m) {
+    snapshot_stack_clear(&m->undo);
+    snapshot_stack_clear(&m->redo);
+    m->lastKind = EDIT_NONE;
+    m->lastBlock = -1;
+    m->lastOffset = -1;
+}
+
+void undo_manager_before_edit(UndoManager *m, EditKind kind, const Document *doc, int cursorBlock, int cursorOffset) {
+    bool coalesce = (kind == m->lastKind) && (cursorBlock == m->lastBlock) && (cursorOffset == m->lastOffset);
+    if (!coalesce) {
+        snapshot_stack_push(&m->undo, doc, cursorBlock, cursorOffset);
+        snapshot_stack_clear(&m->redo);
+    }
+}
+
+void undo_manager_after_edit(UndoManager *m, EditKind kind, int cursorBlock, int cursorOffset) {
+    m->lastKind = kind;
+    m->lastBlock = cursorBlock;
+    m->lastOffset = cursorOffset;
+}
diff --git a/src/undo.h b/src/undo.h
new file mode 100644
index 0000000..8041016
--- /dev/null
+++ b/src/undo.h
@@ -0,0 +1,47 @@
+#ifndef HUSH_UNDO_H
+#define HUSH_UNDO_H
+
+#include "document.h"
+#include <stdbool.h>
+
+typedef struct {
+    Document doc;
+    int cursorBlock, cursorOffset;
+} Snapshot;
+
+typedef struct {
+    Snapshot *items;
+    int count, cap;
+    int limit; /* max entries kept; oldest evicted beyond this */
+} SnapshotStack;
+
+void snapshot_stack_init(SnapshotStack *s, int limit);
+void snapshot_stack_free(SnapshotStack *s);
+void snapshot_stack_clear(SnapshotStack *s);
+void snapshot_stack_push(SnapshotStack *s, const Document *doc, int cursorBlock, int cursorOffset);
+/* Pops the most recent snapshot into *out (transfers Document ownership -- caller must
+   eventually document_free it). Returns false if the stack is empty. */
+bool snapshot_stack_pop(SnapshotStack *s, Snapshot *out);
+
+typedef enum { EDIT_NONE, EDIT_TYPE, EDIT_BACKSPACE, EDIT_DELETE_FWD, EDIT_ENTER, EDIT_PASTE } EditKind;
+
+typedef struct {
+    SnapshotStack undo, redo;
+    EditKind lastKind;
+    int lastBlock, lastOffset;
+} UndoManager;
+
+void undo_manager_init(UndoManager *m, int limit);
+void undo_manager_free(UndoManager *m);
+/* Clears both stacks and coalescing state -- call when a brand new document is loaded. */
+void undo_manager_reset(UndoManager *m);
+
+/* Call BEFORE mutating the document. Pushes a pre-edit snapshot (and clears the redo stack)
+   unless this edit coalesces with the immediately preceding one (same kind, cursor unmoved
+   since the last edit). */
+void undo_manager_before_edit(UndoManager *m, EditKind kind, const Document *doc, int cursorBlock, int cursorOffset);
+/* Call AFTER mutating, with the new cursor position, so the next call's coalescing check has
+   something to compare against. */
+void undo_manager_after_edit(UndoManager *m, EditKind kind, int cursorBlock, int cursorOffset);
+
+#endif
diff --git a/src/utf8.h b/src/utf8.h
new file mode 100644
index 0000000..4d14bee
--- /dev/null
+++ b/src/utf8.h
@@ -0,0 +1,24 @@
+#ifndef HUSH_UTF8_H
+#define HUSH_UTF8_H
+
+/* Byte length of the UTF-8 codepoint starting at text[pos], clamped so it never runs past `len`. */
+static inline int utf8_next_len(const char *text, int pos, int len) {
+    unsigned char c = (unsigned char)text[pos];
+    int n;
+    if ((c & 0x80) == 0x00) n = 1;
+    else if ((c & 0xE0) == 0xC0) n = 2;
+    else if ((c & 0xF0) == 0xE0) n = 3;
+    else if ((c & 0xF8) == 0xF0) n = 4;
+    else n = 1;
+    if (pos + n > len) n = len - pos;
+    return n;
+}
+
+/* Byte offset of the start of the codepoint immediately before `pos`. */
+static inline int utf8_prev_start(const char *text, int pos) {
+    int p = pos - 1;
+    while (p > 0 && ((unsigned char)text[p] & 0xC0) == 0x80) p--;
+    return p;
+}
+
+#endif
diff --git a/src/wrap.c b/src/wrap.c
new file mode 100644
index 0000000..aef6885
--- /dev/null
+++ b/src/wrap.c
@@ -0,0 +1,147 @@
+#include "wrap.h"
+#include <stdlib.h>
+
+typedef struct {
+    int runIndex;
+    int start, end;
+    float width;
+    int isSpace;
+} Atom;
+
+typedef struct {
+    int atomStart, atomEnd; /* [atomStart, atomEnd) into the atoms array */
+} Unit;
+
+void wrap_result_init(WrapResult *wr) {
+    wr->lines = NULL;
+    wr->count = 0;
+    wr->cap = 0;
+}
+
+void wrap_result_free(WrapResult *wr) {
+    for (int i = 0; i < wr->count; i++) free(wr->lines[i].segs);
+    free(wr->lines);
+    wr->lines = NULL;
+    wr->count = 0;
+    wr->cap = 0;
+}
+
+static WrapLine *push_line(WrapResult *wr) {
+    if (wr->count >= wr->cap) {
+        wr->cap = wr->cap > 0 ? wr->cap * 2 : 4;
+        wr->lines = realloc(wr->lines, (size_t)wr->cap * sizeof(WrapLine));
+    }
+    WrapLine *l = &wr->lines[wr->count++];
+    l->segs = NULL;
+    l->count = 0;
+    l->cap = 0;
+    return l;
+}
+
+static void push_seg(WrapLine *l, int runIndex, int start, int end, float width) {
+    if (l->count >= l->cap) {
+        l->cap = l->cap > 0 ? l->cap * 2 : 4;
+        l->segs = realloc(l->segs, (size_t)l->cap * sizeof(WrapSegment));
+    }
+    WrapSegment *s = &l->segs[l->count++];
+    s->runIndex = runIndex;
+    s->start = start;
+    s->end = end;
+    s->width = width;
+}
+
+static void finalize_line(WrapResult *out, const Atom *atoms, int atomStart, int atomEnd) {
+    WrapLine *line = push_line(out);
+    if (atomEnd <= atomStart) return; /* empty line, no segments */
+    int segRun = atoms[atomStart].runIndex;
+    int segStart = atoms[atomStart].start;
+    int segEnd = atoms[atomStart].end;
+    float segWidth = atoms[atomStart].width;
+    for (int i = atomStart + 1; i < atomEnd; i++) {
+        if (atoms[i].runIndex == segRun) {
+            segEnd = atoms[i].end;
+            segWidth += atoms[i].width;
+        } else {
+            push_seg(line, segRun, segStart, segEnd, segWidth);
+            segRun = atoms[i].runIndex;
+            segStart = atoms[i].start;
+            segEnd = atoms[i].end;
+            segWidth = atoms[i].width;
+        }
+    }
+    push_seg(line, segRun, segStart, segEnd, segWidth);
+}
+
+void wrap_layout(const char *text, const WrapRunSpan *runs, int runCount, float maxWidth,
+                  WrapMeasureFn measure, void *measureCtx, WrapResult *out) {
+    wrap_result_init(out);
+
+    /* Step A: split each run's text into word/space atoms. */
+    Atom *atoms = NULL;
+    int atomCount = 0, atomCap = 0;
+    for (int r = 0; r < runCount; r++) {
+        int i = runs[r].start;
+        int end = runs[r].end;
+        while (i < end) {
+            int isSpace = (text[i] == ' ');
+            int j = i + 1;
+            while (j < end && (text[j] == ' ') == isSpace) j++;
+            if (atomCount >= atomCap) {
+                atomCap = atomCap > 0 ? atomCap * 2 : 16;
+                atoms = realloc(atoms, (size_t)atomCap * sizeof(Atom));
+            }
+            Atom *a = &atoms[atomCount++];
+            a->runIndex = r;
+            a->start = i;
+            a->end = j;
+            a->isSpace = isSpace;
+            a->width = measure(measureCtx, r, text, i, j - i);
+            i = j;
+        }
+    }
+
+    if (atomCount == 0) {
+        finalize_line(out, atoms, 0, 0);
+        free(atoms);
+        return;
+    }
+
+    /* Step B: group atoms into unbreakable units (word plus its trailing space, if any). */
+    Unit *units = NULL;
+    int unitCount = 0, unitCap = 0;
+    int a = 0;
+    while (a < atomCount) {
+        int unitStart = a;
+        while (a < atomCount) {
+            int wasSpace = atoms[a].isSpace;
+            a++;
+            if (wasSpace) break;
+        }
+        if (unitCount >= unitCap) {
+            unitCap = unitCap > 0 ? unitCap * 2 : 16;
+            units = realloc(units, (size_t)unitCap * sizeof(Unit));
+        }
+        units[unitCount].atomStart = unitStart;
+        units[unitCount].atomEnd = a;
+        unitCount++;
+    }
+
+    /* Step C: greedily pack units onto lines. */
+    int lineStartUnit = 0;
+    float lineWidth = 0.0f;
+    for (int u = 0; u < unitCount; u++) {
+        float unitWidth = 0.0f;
+        for (int i = units[u].atomStart; i < units[u].atomEnd; i++) unitWidth += atoms[i].width;
+
+        if (u > lineStartUnit && lineWidth + unitWidth > maxWidth) {
+            finalize_line(out, atoms, units[lineStartUnit].atomStart, units[u - 1].atomEnd);
+            lineStartUnit = u;
+            lineWidth = 0.0f;
+        }
+        lineWidth += unitWidth;
+    }
+    finalize_line(out, atoms, units[lineStartUnit].atomStart, units[unitCount - 1].atomEnd);
+
+    free(units);
+    free(atoms);
+}
diff --git a/src/wrap.h b/src/wrap.h
new file mode 100644
index 0000000..3514db7
--- /dev/null
+++ b/src/wrap.h
@@ -0,0 +1,38 @@
+#ifndef HUSH_WRAP_H
+#define HUSH_WRAP_H
+
+/* A contiguous byte range belonging to one styled render run, in source order. */
+typedef struct {
+    int start, end;
+} WrapRunSpan;
+
+typedef float (*WrapMeasureFn)(void *ctx, int runIndex, const char *text, int start, int len);
+
+/* One styled piece of text placed on a single visual line. `runIndex` refers back
+   into the WrapRunSpan array passed to wrap_layout (i.e. the caller's render-run list). */
+typedef struct {
+    int runIndex;
+    int start, end;
+    float width;
+} WrapSegment;
+
+typedef struct {
+    WrapSegment *segs;
+    int count, cap;
+} WrapLine;
+
+typedef struct {
+    WrapLine *lines;
+    int count, cap;
+} WrapResult;
+
+void wrap_result_init(WrapResult *wr);
+void wrap_result_free(WrapResult *wr);
+
+/* Greedily word-wraps `text` (using the styled run spans in `runs`) to fit within `maxWidth`.
+   Always produces at least one (possibly empty) line. Breaks only occur at space characters;
+   a run of atoms glued together with no space between them is never split across lines. */
+void wrap_layout(const char *text, const WrapRunSpan *runs, int runCount, float maxWidth,
+                  WrapMeasureFn measure, void *measureCtx, WrapResult *out);
+
+#endif
diff --git a/vendor/clay.h b/vendor/clay.h
new file mode 100644
index 0000000..7d9cf4c
--- /dev/null
+++ b/vendor/clay.h
@@ -0,0 +1,5058 @@
+// VERSION: 0.14
+
+/*
+    NOTE: In order to use this library you must define
+    the following macro in exactly one file, _before_ including clay.h:
+
+    #define CLAY_IMPLEMENTATION
+    #include "clay.h"
+
+    See the examples folder for details.
+*/
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+// SIMD includes on supported platforms
+#if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64))
+#include <emmintrin.h>
+#elif !defined(CLAY_DISABLE_SIMD) && defined(__aarch64__)
+#include <arm_neon.h>
+#endif
+#if __CLION_IDE__
+#define CLAY_IMPLEMENTATION
+#endif
+
+// -----------------------------------------
+// HEADER DECLARATIONS ---------------------
+// -----------------------------------------
+
+#ifndef CLAY_HEADER
+#define CLAY_HEADER
+
+#if !( \
+    (defined(__cplusplus) && __cplusplus >= 202002L) || \
+    (defined(__STDC__) && __STDC__ == 1 && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || \
+    defined(_MSC_VER) || \
+    defined(__OBJC__) \
+)
+#error "Clay requires C99, C++20, or MSVC"
+#endif
+
+#ifdef CLAY_WASM
+#define CLAY_WASM_EXPORT(name) __attribute__((export_name(name)))
+#else
+#define CLAY_WASM_EXPORT(null)
+#endif
+
+#ifdef CLAY_DLL
+#define CLAY_DLL_EXPORT __declspec(dllexport) __stdcall
+#else
+#define CLAY_DLL_EXPORT
+#endif
+
+// Public Macro API ------------------------
+
+#define CLAY__MAX(x, y) (((x) > (y)) ? (x) : (y))
+#define CLAY__MIN(x, y) (((x) < (y)) ? (x) : (y))
+
+#define CLAY_TEXT_CONFIG(...) __VA_ARGS__
+
+#define CLAY_BORDER_OUTSIDE(widthValue) {widthValue, widthValue, widthValue, widthValue, 0}
+
+#define CLAY_BORDER_ALL(widthValue) {widthValue, widthValue, widthValue, widthValue, widthValue}
+
+#define CLAY_CORNER_RADIUS(radius) (CLAY__INIT(Clay_CornerRadius) { radius, radius, radius, radius })
+
+#define CLAY_PADDING_ALL(padding) CLAY__CONFIG_WRAPPER(Clay_Padding, { padding, padding, padding, padding })
+
+#define CLAY_SIZING_FIT(...) (CLAY__INIT(Clay_SizingAxis) { .size = { .minMax = { __VA_ARGS__ } }, .type = CLAY__SIZING_TYPE_FIT })
+
+#define CLAY_SIZING_GROW(...) (CLAY__INIT(Clay_SizingAxis) { .size = { .minMax = { __VA_ARGS__ } }, .type = CLAY__SIZING_TYPE_GROW })
+
+#define CLAY_SIZING_FIXED(fixedSize) (CLAY__INIT(Clay_SizingAxis) { .size = { .minMax = { fixedSize, fixedSize } }, .type = CLAY__SIZING_TYPE_FIXED })
+
+#define CLAY_SIZING_PERCENT(percentOfParent) (CLAY__INIT(Clay_SizingAxis) { .size = { .percent = (percentOfParent) }, .type = CLAY__SIZING_TYPE_PERCENT })
+
+// Note: If a compile error led you here, you might be trying to use CLAY_ID with something other than a string literal. To construct an ID with a dynamic string, use CLAY_SID instead.
+#define CLAY_ID(label) CLAY_SID(CLAY_STRING(label))
+
+#define CLAY_SID(label) Clay__HashString(label, 0)
+
+// Note: If a compile error led you here, you might be trying to use CLAY_IDI with something other than a string literal. To construct an ID with a dynamic string, use CLAY_SIDI instead.
+#define CLAY_IDI(label, index) CLAY_SIDI(CLAY_STRING(label), index)
+
+#define CLAY_SIDI(label, index) Clay__HashStringWithOffset(label, index, 0)
+
+// Note: If a compile error led you here, you might be trying to use CLAY_ID_LOCAL with something other than a string literal. To construct an ID with a dynamic string, use CLAY_SID_LOCAL instead.
+#define CLAY_ID_LOCAL(label) CLAY_SID_LOCAL(CLAY_STRING(label))
+
+#define CLAY_SID_LOCAL(label) Clay__HashString(label, Clay_GetOpenElementId())
+
+// Note: If a compile error led you here, you might be trying to use CLAY_IDI_LOCAL with something other than a string literal. To construct an ID with a dynamic string, use CLAY_SIDI_LOCAL instead.
+#define CLAY_IDI_LOCAL(label, index) CLAY_SIDI_LOCAL(CLAY_STRING(label), index)
+
+#define CLAY_SIDI_LOCAL(label, index) Clay__HashStringWithOffset(label, index, Clay_GetOpenElementId())
+
+#define CLAY__STRING_LENGTH(s) ((sizeof(s) / sizeof((s)[0])) - sizeof((s)[0]))
+
+#define CLAY__ENSURE_STRING_LITERAL(x) ("" x "")
+
+// Note: If an error led you here, it's because CLAY_STRING can only be used with string literals, i.e. CLAY_STRING("SomeString") and not CLAY_STRING(yourString)
+#define CLAY_STRING(string) (CLAY__INIT(Clay_String) { .isStaticallyAllocated = true, .length = CLAY__STRING_LENGTH(CLAY__ENSURE_STRING_LITERAL(string)), .chars = (string) })
+
+#define CLAY_STRING_CONST(string) { .isStaticallyAllocated = true, .length = CLAY__STRING_LENGTH(CLAY__ENSURE_STRING_LITERAL(string)), .chars = (string) }
+
+static uint8_t CLAY__ELEMENT_DEFINITION_LATCH;
+
+// GCC marks the above CLAY__ELEMENT_DEFINITION_LATCH as an unused variable for files that include clay.h but don't declare any layout
+// This is to suppress that warning
+static inline void Clay__SuppressUnusedLatchDefinitionVariableWarning(void) { (void) CLAY__ELEMENT_DEFINITION_LATCH; }
+
+// Publicly visible layout element macros -----------------------------------------------------
+
+/* This macro looks scary on the surface, but is actually quite simple.
+  It turns a macro call like this:
+
+  CLAY({
+    .id = CLAY_ID("Container"),
+    .backgroundColor = { 255, 200, 200, 255 }
+  }) {
+      ...children declared here
+  }
+
+  Into calls like this:
+
+  Clay__OpenElement();
+  Clay__ConfigureOpenElement((Clay_ElementDeclaration) {
+    .id = CLAY_ID("Container"),
+    .backgroundColor = { 255, 200, 200, 255 }
+  });
+  ...children declared here
+  Clay__CloseElement();
+
+  The for loop will only ever run a single iteration, putting Clay__CloseElement() in the increment of the loop
+  means that it will run after the body - where the children are declared. It just exists to make sure you don't forget
+  to call Clay_CloseElement().
+*/
+#define CLAY_AUTO_ID(...)                                                                                                                                   \
+    for (                                                                                                                                                   \
+        CLAY__ELEMENT_DEFINITION_LATCH = (Clay__OpenElement(), Clay__ConfigureOpenElement(CLAY__CONFIG_WRAPPER(Clay_ElementDeclaration, __VA_ARGS__)), 0);  \
+        CLAY__ELEMENT_DEFINITION_LATCH < 1;                                                                                                                 \
+        CLAY__ELEMENT_DEFINITION_LATCH=1, Clay__CloseElement()                                                                                              \
+    )
+
+#define CLAY(id, ...)                                                                                                                                               \
+    for (                                                                                                                                                           \
+        CLAY__ELEMENT_DEFINITION_LATCH = (Clay__OpenElementWithId(id), Clay__ConfigureOpenElement(CLAY__CONFIG_WRAPPER(Clay_ElementDeclaration, __VA_ARGS__)), 0);  \
+        CLAY__ELEMENT_DEFINITION_LATCH < 1;                                                                                                                         \
+        CLAY__ELEMENT_DEFINITION_LATCH=1, Clay__CloseElement()                                                                                                      \
+    )
+
+// These macros exist to allow the CLAY() macro to be called both with an inline struct definition, such as
+// CLAY({ .id = something... });
+// As well as by passing a predefined declaration struct
+// Clay_ElementDeclaration declarationStruct = ...
+// CLAY(declarationStruct);
+#define CLAY__WRAPPER_TYPE(type) Clay__##type##Wrapper
+#define CLAY__WRAPPER_STRUCT(type) typedef struct { type wrapped; } CLAY__WRAPPER_TYPE(type)
+#define CLAY__CONFIG_WRAPPER(type, ...) (CLAY__INIT(CLAY__WRAPPER_TYPE(type)) { __VA_ARGS__ }).wrapped
+
+#define CLAY_TEXT(text, ...) Clay__OpenTextElement(text, CLAY__CONFIG_WRAPPER(Clay_TextElementConfig, __VA_ARGS__))
+
+#ifdef __cplusplus
+
+#define CLAY__INIT(type) type
+
+#define CLAY_PACKED_ENUM enum : uint8_t
+
+#define CLAY__DEFAULT_STRUCT {}
+
+#else
+
+#define CLAY__INIT(type) (type)
+
+#if defined(_MSC_VER) && !defined(__clang__)
+#define CLAY_PACKED_ENUM __pragma(pack(push, 1)) enum __pragma(pack(pop))
+#else
+#define CLAY_PACKED_ENUM enum __attribute__((__packed__))
+#endif
+
+#if __STDC_VERSION__ >= 202311L
+#define CLAY__DEFAULT_STRUCT {}
+#else
+#define CLAY__DEFAULT_STRUCT {0}
+#endif
+
+#endif // __cplusplus
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Utility Structs -------------------------
+
+// Note: Clay_String is not guaranteed to be null terminated. It may be if created from a literal C string,
+// but it is also used to represent slices.
+typedef struct Clay_String {
+    // Set this boolean to true if the char* data underlying this string will live for the entire lifetime of the program.
+    // This will automatically be set for strings created with CLAY_STRING, as the macro requires a string literal.
+    bool isStaticallyAllocated;
+    int32_t length;
+    // The underlying character memory. Note: this will not be copied and will not extend the lifetime of the underlying memory.
+    const char *chars;
+} Clay_String;
+
+// Clay_StringSlice is used to represent non owning string slices, and includes
+// a baseChars field which points to the string this slice is derived from.
+typedef struct Clay_StringSlice {
+    int32_t length;
+    const char *chars;
+    const char *baseChars; // The source string / char* that this slice was derived from
+} Clay_StringSlice;
+
+typedef struct Clay_Context Clay_Context;
+
+// Clay_Arena is a memory arena structure that is used by clay to manage its internal allocations.
+// Rather than creating it by hand, it's easier to use Clay_CreateArenaWithCapacityAndMemory()
+typedef struct Clay_Arena {
+    uintptr_t nextAllocation;
+    size_t capacity;
+    char *memory;
+} Clay_Arena;
+
+typedef struct Clay_Dimensions {
+    float width, height;
+} Clay_Dimensions;
+
+typedef struct Clay_Vector2 {
+    float x, y;
+} Clay_Vector2;
+
+// Internally clay conventionally represents colors as 0-255, but interpretation is up to the renderer.
+typedef struct Clay_Color {
+    float r, g, b, a;
+} Clay_Color;
+
+typedef struct Clay_BoundingBox {
+    float x, y, width, height;
+} Clay_BoundingBox;
+
+// Primarily created via the CLAY_ID(), CLAY_IDI(), CLAY_ID_LOCAL() and CLAY_IDI_LOCAL() macros.
+// Represents a hashed string ID used for identifying and finding specific clay UI elements, required
+// by functions such as Clay_PointerOver() and Clay_GetElementData().
+typedef struct Clay_ElementId {
+    uint32_t id; // The resulting hash generated from the other fields.
+    uint32_t offset; // A numerical offset applied after computing the hash from stringId.
+    uint32_t baseId; // A base hash value to start from, for example the parent element ID is used when calculating CLAY_ID_LOCAL().
+    Clay_String stringId; // The string id to hash.
+} Clay_ElementId;
+
+// A sized array of Clay_ElementId.
+typedef struct
+{
+    int32_t capacity;
+    int32_t length;
+    Clay_ElementId *internalArray;
+} Clay_ElementIdArray;
+
+// Controls the "radius", or corner rounding of elements, including rectangles, borders and images.
+// The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels.
+typedef struct Clay_CornerRadius {
+    float topLeft;
+    float topRight;
+    float bottomLeft;
+    float bottomRight;
+} Clay_CornerRadius;
+
+// Element Configs ---------------------------
+
+// Controls the direction in which child elements will be automatically laid out.
+typedef CLAY_PACKED_ENUM {
+    // (Default) Lays out child elements from left to right with increasing x.
+    CLAY_LEFT_TO_RIGHT,
+    // Lays out child elements from top to bottom with increasing y.
+    CLAY_TOP_TO_BOTTOM,
+} Clay_LayoutDirection;
+
+// Controls the alignment along the x axis (horizontal) of child elements.
+typedef CLAY_PACKED_ENUM {
+    // (Default) Aligns child elements to the left hand side of this element, offset by padding.width.left
+    CLAY_ALIGN_X_LEFT,
+    // Aligns child elements to the right hand side of this element, offset by padding.width.right
+    CLAY_ALIGN_X_RIGHT,
+    // Aligns child elements horizontally to the center of this element
+    CLAY_ALIGN_X_CENTER,
+} Clay_LayoutAlignmentX;
+
+// Controls the alignment along the y axis (vertical) of child elements.
+typedef CLAY_PACKED_ENUM {
+    // (Default) Aligns child elements to the top of this element, offset by padding.width.top
+    CLAY_ALIGN_Y_TOP,
+    // Aligns child elements to the bottom of this element, offset by padding.width.bottom
+    CLAY_ALIGN_Y_BOTTOM,
+    // Aligns child elements vertically to the center of this element
+    CLAY_ALIGN_Y_CENTER,
+} Clay_LayoutAlignmentY;
+
+// Controls how the element takes up space inside its parent container.
+typedef CLAY_PACKED_ENUM {
+    // (default) Wraps tightly to the size of the element's contents.
+    CLAY__SIZING_TYPE_FIT,
+    // Expands along this axis to fill available space in the parent element, sharing it with other GROW elements.
+    CLAY__SIZING_TYPE_GROW,
+    // Expects 0-1 range. Clamps the axis size to a percent of the parent container's axis size minus padding and child gaps.
+    CLAY__SIZING_TYPE_PERCENT,
+    // Clamps the axis size to an exact size in pixels.
+    CLAY__SIZING_TYPE_FIXED,
+} Clay__SizingType;
+
+// Controls how child elements are aligned on each axis.
+typedef struct Clay_ChildAlignment {
+    Clay_LayoutAlignmentX x; // Controls alignment of children along the x axis.
+    Clay_LayoutAlignmentY y; // Controls alignment of children along the y axis.
+} Clay_ChildAlignment;
+
+// Controls the minimum and maximum size in pixels that this element is allowed to grow or shrink to,
+// overriding sizing types such as FIT or GROW.
+typedef struct Clay_SizingMinMax {
+    float min; // The smallest final size of the element on this axis will be this value in pixels.
+    float max; // The largest final size of the element on this axis will be this value in pixels.
+} Clay_SizingMinMax;
+
+// Controls the sizing of this element along one axis inside its parent container.
+typedef struct Clay_SizingAxis {
+    union {
+        Clay_SizingMinMax minMax; // Controls the minimum and maximum size in pixels that this element is allowed to grow or shrink to, overriding sizing types such as FIT or GROW.
+        float percent; // Expects 0-1 range. Clamps the axis size to a percent of the parent container's axis size minus padding and child gaps.
+    } size;
+    Clay__SizingType type; // Controls how the element takes up space inside its parent container.
+} Clay_SizingAxis;
+
+// Controls the sizing of this element along one axis inside its parent container.
+typedef struct Clay_Sizing {
+    Clay_SizingAxis width; // Controls the width sizing of the element, along the x axis.
+    Clay_SizingAxis height;  // Controls the height sizing of the element, along the y axis.
+} Clay_Sizing;
+
+// Controls "padding" in pixels, which is a gap between the bounding box of this element and where its children
+// will be placed.
+typedef struct Clay_Padding {
+    uint16_t left;
+    uint16_t right;
+    uint16_t top;
+    uint16_t bottom;
+} Clay_Padding;
+
+CLAY__WRAPPER_STRUCT(Clay_Padding);
+
+// Controls various settings that affect the size and position of an element, as well as the sizes and positions
+// of any child elements.
+typedef struct Clay_LayoutConfig {
+    Clay_Sizing sizing; // Controls the sizing of this element inside it's parent container, including FIT, GROW, PERCENT and FIXED sizing.
+    Clay_Padding padding; // Controls "padding" in pixels, which is a gap between the bounding box of this element and where its children will be placed.
+    uint16_t childGap; // Controls the gap in pixels between child elements along the layout axis (horizontal gap for LEFT_TO_RIGHT, vertical gap for TOP_TO_BOTTOM).
+    Clay_ChildAlignment childAlignment; // Controls how child elements are aligned on each axis.
+    Clay_LayoutDirection layoutDirection; // Controls the direction in which child elements will be automatically laid out.
+} Clay_LayoutConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_LayoutConfig);
+
+extern Clay_LayoutConfig CLAY_LAYOUT_DEFAULT;
+
+// Controls how text "wraps", that is how it is broken into multiple lines when there is insufficient horizontal space.
+typedef CLAY_PACKED_ENUM {
+    // (default) breaks on whitespace characters.
+    CLAY_TEXT_WRAP_WORDS,
+    // Don't break on space characters, only on newlines.
+    CLAY_TEXT_WRAP_NEWLINES,
+    // Disable text wrapping entirely.
+    CLAY_TEXT_WRAP_NONE,
+} Clay_TextElementConfigWrapMode;
+
+// Controls how wrapped lines of text are horizontally aligned within the outer text bounding box.
+typedef CLAY_PACKED_ENUM {
+    // (default) Horizontally aligns wrapped lines of text to the left hand side of their bounding box.
+    CLAY_TEXT_ALIGN_LEFT,
+    // Horizontally aligns wrapped lines of text to the center of their bounding box.
+    CLAY_TEXT_ALIGN_CENTER,
+    // Horizontally aligns wrapped lines of text to the right hand side of their bounding box.
+    CLAY_TEXT_ALIGN_RIGHT,
+} Clay_TextAlignment;
+
+// Controls various functionality related to text elements.
+typedef struct Clay_TextElementConfig {
+    // A pointer that will be transparently passed through to the resulting render command.
+    void *userData;
+    // The RGBA color of the font to render, conventionally specified as 0-255.
+    Clay_Color textColor;
+    // An integer transparently passed to Clay_MeasureText to identify the font to use.
+    // The debug view will pass fontId = 0 for its internal text.
+    uint16_t fontId;
+    // Controls the size of the font. Handled by the function provided to Clay_MeasureText.
+    uint16_t fontSize;
+    // Controls extra horizontal spacing between characters. Handled by the function provided to Clay_MeasureText.
+    uint16_t letterSpacing;
+    // Controls additional vertical space between wrapped lines of text.
+    uint16_t lineHeight;
+    // Controls how text "wraps", that is how it is broken into multiple lines when there is insufficient horizontal space.
+    // CLAY_TEXT_WRAP_WORDS (default) breaks on whitespace characters.
+    // CLAY_TEXT_WRAP_NEWLINES doesn't break on space characters, only on newlines.
+    // CLAY_TEXT_WRAP_NONE disables wrapping entirely.
+    Clay_TextElementConfigWrapMode wrapMode;
+    // Controls how wrapped lines of text are horizontally aligned within the outer text bounding box.
+    // CLAY_TEXT_ALIGN_LEFT (default) - Horizontally aligns wrapped lines of text to the left hand side of their bounding box.
+    // CLAY_TEXT_ALIGN_CENTER - Horizontally aligns wrapped lines of text to the center of their bounding box.
+    // CLAY_TEXT_ALIGN_RIGHT - Horizontally aligns wrapped lines of text to the right hand side of their bounding box.
+    Clay_TextAlignment textAlignment;
+} Clay_TextElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_TextElementConfig);
+
+// Aspect Ratio --------------------------------
+
+// Controls various settings related to aspect ratio scaling element.
+typedef struct Clay_AspectRatioElementConfig {
+    float aspectRatio; // A float representing the target "Aspect ratio" for an element, which is its final width divided by its final height.
+} Clay_AspectRatioElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_AspectRatioElementConfig);
+
+// Image --------------------------------
+
+// Controls various settings related to image elements.
+typedef struct Clay_ImageElementConfig {
+    void* imageData; // A transparent pointer used to pass image data through to the renderer.
+} Clay_ImageElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_ImageElementConfig);
+
+// Floating -----------------------------
+
+// Controls where a floating element is offset relative to its parent element.
+// Note: see https://github.com/user-attachments/assets/b8c6dfaa-c1b1-41a4-be55-013473e4a6ce for a visual explanation.
+typedef CLAY_PACKED_ENUM {
+    CLAY_ATTACH_POINT_LEFT_TOP,
+    CLAY_ATTACH_POINT_LEFT_CENTER,
+    CLAY_ATTACH_POINT_LEFT_BOTTOM,
+    CLAY_ATTACH_POINT_CENTER_TOP,
+    CLAY_ATTACH_POINT_CENTER_CENTER,
+    CLAY_ATTACH_POINT_CENTER_BOTTOM,
+    CLAY_ATTACH_POINT_RIGHT_TOP,
+    CLAY_ATTACH_POINT_RIGHT_CENTER,
+    CLAY_ATTACH_POINT_RIGHT_BOTTOM,
+} Clay_FloatingAttachPointType;
+
+// Controls where a floating element is offset relative to its parent element.
+typedef struct Clay_FloatingAttachPoints {
+    Clay_FloatingAttachPointType element; // Controls the origin point on a floating element that attaches to its parent.
+    Clay_FloatingAttachPointType parent; // Controls the origin point on the parent element that the floating element attaches to.
+} Clay_FloatingAttachPoints;
+
+// Controls how mouse pointer events like hover and click are captured or passed through to elements underneath a floating element.
+typedef CLAY_PACKED_ENUM {
+    // (default) "Capture" the pointer event and don't allow events like hover and click to pass through to elements underneath.
+    CLAY_POINTER_CAPTURE_MODE_CAPTURE,
+    //    CLAY_POINTER_CAPTURE_MODE_PARENT, TODO pass pointer through to attached parent
+    // Transparently pass through pointer events like hover and click to elements underneath the floating element.
+    CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH,
+} Clay_PointerCaptureMode;
+
+// Controls which element a floating element is "attached" to (i.e. relative offset from).
+typedef CLAY_PACKED_ENUM {
+    // (default) Disables floating for this element.
+    CLAY_ATTACH_TO_NONE,
+    // Attaches this floating element to its parent, positioned based on the .attachPoints and .offset fields.
+    CLAY_ATTACH_TO_PARENT,
+    // Attaches this floating element to an element with a specific ID, specified with the .parentId field. positioned based on the .attachPoints and .offset fields.
+    CLAY_ATTACH_TO_ELEMENT_WITH_ID,
+    // Attaches this floating element to the root of the layout, which combined with the .offset field provides functionality similar to "absolute positioning".
+    CLAY_ATTACH_TO_ROOT,
+} Clay_FloatingAttachToElement;
+
+// Controls whether or not a floating element is clipped to the same clipping rectangle as the element it's attached to.
+typedef CLAY_PACKED_ENUM {
+    // (default) - The floating element does not inherit clipping.
+    CLAY_CLIP_TO_NONE,
+    // The floating element is clipped to the same clipping rectangle as the element it's attached to.
+    CLAY_CLIP_TO_ATTACHED_PARENT
+} Clay_FloatingClipToElement;
+
+// Controls various settings related to "floating" elements, which are elements that "float" above other elements, potentially overlapping their boundaries,
+// and not affecting the layout of sibling or parent elements.
+typedef struct Clay_FloatingElementConfig {
+    // Offsets this floating element by the provided x,y coordinates from its attachPoints.
+    Clay_Vector2 offset;
+    // Expands the boundaries of the outer floating element without affecting its children.
+    Clay_Dimensions expand;
+    // When used in conjunction with .attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID, attaches this floating element to the element in the hierarchy with the provided ID.
+    // Hint: attach the ID to the other element with .id = CLAY_ID("yourId"), and specify the id the same way, with .parentId = CLAY_ID("yourId").id
+    uint32_t parentId;
+    // Controls the z index of this floating element and all its children. Floating elements are sorted in ascending z order before output.
+    // zIndex is also passed to the renderer for all elements contained within this floating element.
+    int16_t zIndex;
+    // Controls how mouse pointer events like hover and click are captured or passed through to elements underneath / behind a floating element.
+    // Enum is of the form CLAY_ATTACH_POINT_foo_bar. See Clay_FloatingAttachPoints for more details.
+    // Note: see <img src="https://github.com/user-attachments/assets/b8c6dfaa-c1b1-41a4-be55-013473e4a6ce />
+    // and <img src="https://github.com/user-attachments/assets/ebe75e0d-1904-46b0-982d-418f929d1516 /> for a visual explanation.
+    Clay_FloatingAttachPoints attachPoints;
+    // Controls how mouse pointer events like hover and click are captured or passed through to elements underneath a floating element.
+    // CLAY_POINTER_CAPTURE_MODE_CAPTURE (default) - "Capture" the pointer event and don't allow events like hover and click to pass through to elements underneath.
+    // CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH - Transparently pass through pointer events like hover and click to elements underneath the floating element.
+    Clay_PointerCaptureMode pointerCaptureMode;
+    // Controls which element a floating element is "attached" to (i.e. relative offset from).
+    // CLAY_ATTACH_TO_NONE (default) - Disables floating for this element.
+    // CLAY_ATTACH_TO_PARENT - Attaches this floating element to its parent, positioned based on the .attachPoints and .offset fields.
+    // CLAY_ATTACH_TO_ELEMENT_WITH_ID - Attaches this floating element to an element with a specific ID, specified with the .parentId field. positioned based on the .attachPoints and .offset fields.
+    // CLAY_ATTACH_TO_ROOT - Attaches this floating element to the root of the layout, which combined with the .offset field provides functionality similar to "absolute positioning".
+    Clay_FloatingAttachToElement attachTo;
+    // Controls whether or not a floating element is clipped to the same clipping rectangle as the element it's attached to.
+    // CLAY_CLIP_TO_NONE (default) - The floating element does not inherit clipping.
+    // CLAY_CLIP_TO_ATTACHED_PARENT - The floating element is clipped to the same clipping rectangle as the element it's attached to.
+    Clay_FloatingClipToElement clipTo;
+} Clay_FloatingElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_FloatingElementConfig);
+
+// Custom -----------------------------
+
+// Controls various settings related to custom elements.
+typedef struct Clay_CustomElementConfig {
+    // A transparent pointer through which you can pass custom data to the renderer.
+    // Generates CUSTOM render commands.
+    void* customData;
+} Clay_CustomElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_CustomElementConfig);
+
+// Scroll -----------------------------
+
+// Controls the axis on which an element switches to "scrolling", which clips the contents and allows scrolling in that direction.
+typedef struct Clay_ClipElementConfig {
+    bool horizontal; // Clip overflowing elements on the X axis.
+    bool vertical; // Clip overflowing elements on the Y axis.
+    Clay_Vector2 childOffset; // Offsets the x,y positions of all child elements. Used primarily for scrolling containers.
+} Clay_ClipElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_ClipElementConfig);
+
+// Border -----------------------------
+
+// Controls the widths of individual element borders.
+typedef struct Clay_BorderWidth {
+    uint16_t left;
+    uint16_t right;
+    uint16_t top;
+    uint16_t bottom;
+    // Creates borders between each child element, depending on the .layoutDirection.
+    // e.g. for LEFT_TO_RIGHT, borders will be vertical lines, and for TOP_TO_BOTTOM borders will be horizontal lines.
+    // .betweenChildren borders will result in individual RECTANGLE render commands being generated.
+    uint16_t betweenChildren;
+} Clay_BorderWidth;
+
+// Controls settings related to element borders.
+typedef struct Clay_BorderElementConfig {
+    Clay_Color color; // Controls the color of all borders with width > 0. Conventionally represented as 0-255, but interpretation is up to the renderer.
+    Clay_BorderWidth width; // Controls the widths of individual borders. At least one of these should be > 0 for a BORDER render command to be generated.
+} Clay_BorderElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_BorderElementConfig);
+
+typedef struct {
+    Clay_BoundingBox boundingBox;
+    Clay_Color backgroundColor;
+    Clay_Color overlayColor;
+    Clay_Color borderColor;
+    Clay_BorderWidth borderWidth;
+} Clay_TransitionData;
+
+typedef enum {
+    CLAY_TRANSITION_STATE_IDLE,
+    CLAY_TRANSITION_STATE_ENTERING,
+    CLAY_TRANSITION_STATE_TRANSITIONING,
+    CLAY_TRANSITION_STATE_EXITING,
+} Clay_TransitionState;
+
+typedef enum {
+    CLAY_TRANSITION_PROPERTY_NONE = 0,
+    CLAY_TRANSITION_PROPERTY_X = 1,
+    CLAY_TRANSITION_PROPERTY_Y = 2,
+    CLAY_TRANSITION_PROPERTY_POSITION = CLAY_TRANSITION_PROPERTY_X | CLAY_TRANSITION_PROPERTY_Y,
+    CLAY_TRANSITION_PROPERTY_WIDTH = 4,
+    CLAY_TRANSITION_PROPERTY_HEIGHT = 8,
+    CLAY_TRANSITION_PROPERTY_DIMENSIONS = CLAY_TRANSITION_PROPERTY_WIDTH | CLAY_TRANSITION_PROPERTY_HEIGHT,
+    CLAY_TRANSITION_PROPERTY_BOUNDING_BOX = CLAY_TRANSITION_PROPERTY_POSITION | CLAY_TRANSITION_PROPERTY_DIMENSIONS,
+    CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR = 16,
+    CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR = 32,
+    CLAY_TRANSITION_PROPERTY_CORNER_RADIUS = 64,
+    CLAY_TRANSITION_PROPERTY_BORDER_COLOR = 128,
+    CLAY_TRANSITION_PROPERTY_BORDER_WIDTH = 256,
+    CLAY_TRANSITION_PROPERTY_BORDER = CLAY_TRANSITION_PROPERTY_BORDER_COLOR | CLAY_TRANSITION_PROPERTY_BORDER_WIDTH
+} Clay_TransitionProperty;
+
+typedef struct {
+    Clay_TransitionState transitionState;
+    Clay_TransitionData initial;
+    Clay_TransitionData *current;
+    Clay_TransitionData target;
+    float elapsedTime;
+    float duration;
+    Clay_TransitionProperty properties;
+} Clay_TransitionCallbackArguments;
+
+typedef CLAY_PACKED_ENUM {
+    CLAY_TRANSITION_ENTER_SKIP_ON_FIRST_PARENT_FRAME,
+    CLAY_TRANSITION_ENTER_TRIGGER_ON_FIRST_PARENT_FRAME,
+} Clay_TransitionEnterTriggerType;
+
+typedef CLAY_PACKED_ENUM {
+    CLAY_TRANSITION_EXIT_SKIP_WHEN_PARENT_EXITS,
+    CLAY_TRANSITION_EXIT_TRIGGER_WHEN_PARENT_EXITS,
+} Clay_TransitionExitTriggerType;
+
+typedef CLAY_PACKED_ENUM {
+    CLAY_TRANSITION_DISABLE_INTERACTIONS_WHILE_TRANSITIONING_POSITION,
+    CLAY_TRANSITION_ALLOW_INTERACTIONS_WHILE_TRANSITIONING_POSITION,
+} Clay_TransitionInteractionHandlingType;
+
+typedef CLAY_PACKED_ENUM {
+    CLAY_EXIT_TRANSITION_ORDERING_UNDERNEATH_SIBLINGS,
+    CLAY_EXIT_TRANSITION_ORDERING_NATURAL_ORDER,
+    CLAY_EXIT_TRANSITION_ORDERING_ABOVE_SIBLINGS,
+} Clay_ExitTransitionSiblingOrdering;
+
+// Controls settings related to transitions
+typedef struct Clay_TransitionElementConfig {
+    bool (*handler)(Clay_TransitionCallbackArguments arguments);
+    float duration;
+    Clay_TransitionProperty properties;
+    Clay_TransitionInteractionHandlingType interactionHandling;
+    struct {
+        Clay_TransitionData (*setInitialState)(Clay_TransitionData targetState, Clay_TransitionProperty properties);
+        Clay_TransitionEnterTriggerType trigger;
+    } enter;
+    struct {
+        Clay_TransitionData (*setFinalState)(Clay_TransitionData initialState, Clay_TransitionProperty properties);
+        Clay_TransitionExitTriggerType trigger;
+        Clay_ExitTransitionSiblingOrdering siblingOrdering;
+    } exit;
+} Clay_TransitionElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_TransitionElementConfig);
+
+// Render Command Data -----------------------------
+
+// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_TEXT
+typedef struct Clay_TextRenderData {
+    // A string slice containing the text to be rendered.
+    // Note: this is not guaranteed to be null terminated.
+    Clay_StringSlice stringContents;
+    // Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer.
+    Clay_Color textColor;
+    // An integer representing the font to use to render this text, transparently passed through from the text declaration.
+    uint16_t fontId;
+    uint16_t fontSize;
+    // Specifies the extra whitespace gap in pixels between each character.
+    uint16_t letterSpacing;
+    // The height of the bounding box for this line of text.
+    uint16_t lineHeight;
+} Clay_TextRenderData;
+
+// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_RECTANGLE
+typedef struct Clay_RectangleRenderData {
+    // The solid background color to fill this rectangle with. Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer.
+    Clay_Color backgroundColor;
+    // Controls the "radius", or corner rounding of elements, including rectangles, borders and images.
+    // The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels.
+    Clay_CornerRadius cornerRadius;
+} Clay_RectangleRenderData;
+
+// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_IMAGE
+typedef struct Clay_ImageRenderData {
+    // The tint color for this image. Note that the default value is 0,0,0,0 and should likely be interpreted
+    // as "untinted".
+    // Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer.
+    Clay_Color backgroundColor;
+    // Controls the "radius", or corner rounding of this image.
+    // The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels.
+    Clay_CornerRadius cornerRadius;
+    // A pointer transparently passed through from the original element definition, typically used to represent image data.
+    void* imageData;
+} Clay_ImageRenderData;
+
+// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_CUSTOM
+typedef struct Clay_CustomRenderData {
+    // Passed through from .backgroundColor in the original element declaration.
+    // Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer.
+    Clay_Color backgroundColor;
+    // Controls the "radius", or corner rounding of this custom element.
+    // The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels.
+    Clay_CornerRadius cornerRadius;
+    // A pointer transparently passed through from the original element definition.
+    void* customData;
+} Clay_CustomRenderData;
+
+// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_SCISSOR_START || commandType == CLAY_RENDER_COMMAND_TYPE_SCISSOR_END
+typedef struct Clay_ClipRenderData {
+    bool horizontal;
+    bool vertical;
+} Clay_ClipRenderData;
+
+// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START || commandType == CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_END
+typedef struct Clay_OverlayColorRenderData {
+    Clay_Color color;
+} Clay_OverlayColorRenderData;
+
+// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_BORDER
+typedef struct Clay_BorderRenderData {
+    // Controls a shared color for all this element's borders.
+    // Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer.
+    Clay_Color color;
+    // Specifies the "radius", or corner rounding of this border element.
+    // The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels.
+    Clay_CornerRadius cornerRadius;
+    // Controls individual border side widths.
+    Clay_BorderWidth width;
+} Clay_BorderRenderData;
+
+// A struct union containing data specific to this command's .commandType
+typedef union Clay_RenderData {
+    // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_RECTANGLE
+    Clay_RectangleRenderData rectangle;
+    // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_TEXT
+    Clay_TextRenderData text;
+    // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_IMAGE
+    Clay_ImageRenderData image;
+    // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_CUSTOM
+    Clay_CustomRenderData custom;
+    // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_BORDER
+    Clay_BorderRenderData border;
+    // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_SCISSOR_START|END
+    Clay_ClipRenderData clip;
+    // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START|END
+    Clay_OverlayColorRenderData overlayColor;
+} Clay_RenderData;
+
+// Miscellaneous Structs & Enums ---------------------------------
+
+// Data representing the current internal state of a scrolling element.
+typedef struct Clay_ScrollContainerData {
+    // Note: This is a pointer to the real internal scroll position, mutating it may cause a change in final layout.
+    // Intended for use with external functionality that modifies scroll position, such as scroll bars or auto scrolling.
+    Clay_Vector2 *scrollPosition;
+    // The bounding box of the scroll element.
+    Clay_Dimensions scrollContainerDimensions;
+    // The outer dimensions of the inner scroll container content, including the padding of the parent scroll container.
+    Clay_Dimensions contentDimensions;
+    // The config that was originally passed to the clip element.
+    Clay_ClipElementConfig config;
+    // Indicates whether an actual scroll container matched the provided ID or if the default struct was returned.
+    bool found;
+} Clay_ScrollContainerData;
+
+// Bounding box and other data for a specific UI element.
+typedef struct Clay_ElementData {
+    // The rectangle that encloses this UI element, with the position relative to the root of the layout.
+    Clay_BoundingBox boundingBox;
+    // Indicates whether an actual Element matched the provided ID or if the default struct was returned.
+    bool found;
+} Clay_ElementData;
+
+// Used by renderers to determine specific handling for each render command.
+typedef CLAY_PACKED_ENUM {
+    // This command type should be skipped.
+    CLAY_RENDER_COMMAND_TYPE_NONE,
+    // The renderer should draw a solid color rectangle.
+    CLAY_RENDER_COMMAND_TYPE_RECTANGLE,
+    // The renderer should draw a colored border inset into the bounding box.
+    CLAY_RENDER_COMMAND_TYPE_BORDER,
+    // The renderer should draw text.
+    CLAY_RENDER_COMMAND_TYPE_TEXT,
+    // The renderer should draw an image.
+    CLAY_RENDER_COMMAND_TYPE_IMAGE,
+    // The renderer should begin clipping all future draw commands, only rendering content that falls within the provided boundingBox.
+    CLAY_RENDER_COMMAND_TYPE_SCISSOR_START,
+    // The renderer should finish any previously active clipping, and begin rendering elements in full again.
+    CLAY_RENDER_COMMAND_TYPE_SCISSOR_END,
+    // The renderer should begin performing a "color overlay" on all subsequent render commands until disabled again.
+    CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START,
+    // The renderer should disable any previously active "color overlay" and render elements with their standard colors again.
+    CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_END,
+    // The renderer should provide a custom implementation for handling this render command based on its .customData
+    CLAY_RENDER_COMMAND_TYPE_CUSTOM,
+} Clay_RenderCommandType;
+
+typedef struct Clay_RenderCommand {
+    // A rectangular box that fully encloses this UI element, with the position relative to the root of the layout.
+    Clay_BoundingBox boundingBox;
+    // A struct union containing data specific to this command's commandType.
+    Clay_RenderData renderData;
+    // A pointer transparently passed through from the original element declaration.
+    void *userData;
+    // The id of this element, transparently passed through from the original element declaration.
+    uint32_t id;
+    // The z order required for drawing this command correctly.
+    // Note: the render command array is already sorted in ascending order, and will produce correct results if drawn in naive order.
+    // This field is intended for use in batching renderers for improved performance.
+    int16_t zIndex;
+    // Specifies how to handle rendering of this command.
+    // CLAY_RENDER_COMMAND_TYPE_RECTANGLE - The renderer should draw a solid color rectangle.
+    // CLAY_RENDER_COMMAND_TYPE_BORDER - The renderer should draw a colored border inset into the bounding box.
+    // CLAY_RENDER_COMMAND_TYPE_TEXT - The renderer should draw text.
+    // CLAY_RENDER_COMMAND_TYPE_IMAGE - The renderer should draw an image.
+    // CLAY_RENDER_COMMAND_TYPE_SCISSOR_START - The renderer should begin clipping all future draw commands, only rendering content that falls within the provided boundingBox.
+    // CLAY_RENDER_COMMAND_TYPE_SCISSOR_END - The renderer should finish any previously active clipping, and begin rendering elements in full again.
+    // CLAY_RENDER_COMMAND_TYPE_CUSTOM - The renderer should provide a custom implementation for handling this render command based on its .customData
+    Clay_RenderCommandType commandType;
+} Clay_RenderCommand;
+
+// A sized array of render commands.
+typedef struct Clay_RenderCommandArray {
+    // The underlying max capacity of the array, not necessarily all initialized.
+    int32_t capacity;
+    // The number of initialized elements in this array. Used for loops and iteration.
+    int32_t length;
+    // A pointer to the first element in the internal array.
+    Clay_RenderCommand* internalArray;
+} Clay_RenderCommandArray;
+
+// Represents the current state of interaction with clay this frame.
+typedef CLAY_PACKED_ENUM {
+    // A left mouse click, or touch occurred this frame.
+    CLAY_POINTER_DATA_PRESSED_THIS_FRAME,
+    // The left mouse button click or touch happened at some point in the past, and is still currently held down this frame.
+    CLAY_POINTER_DATA_PRESSED,
+    // The left mouse button click or touch was released this frame.
+    CLAY_POINTER_DATA_RELEASED_THIS_FRAME,
+    // The left mouse button click or touch is not currently down / was released at some point in the past.
+    CLAY_POINTER_DATA_RELEASED,
+} Clay_PointerDataInteractionState;
+
+// Information on the current state of pointer interactions this frame.
+typedef struct Clay_PointerData {
+    // The position of the mouse / touch / pointer relative to the root of the layout.
+    Clay_Vector2 position;
+    // Represents the current state of interaction with clay this frame.
+    // CLAY_POINTER_DATA_PRESSED_THIS_FRAME - A left mouse click, or touch occurred this frame.
+    // CLAY_POINTER_DATA_PRESSED - The left mouse button click or touch happened at some point in the past, and is still currently held down this frame.
+    // CLAY_POINTER_DATA_RELEASED_THIS_FRAME - The left mouse button click or touch was released this frame.
+    // CLAY_POINTER_DATA_RELEASED - The left mouse button click or touch is not currently down / was released at some point in the past.
+    Clay_PointerDataInteractionState state;
+} Clay_PointerData;
+
+typedef struct Clay_ElementDeclaration {
+    // Controls various settings that affect the size and position of an element, as well as the sizes and positions of any child elements.
+    Clay_LayoutConfig layout;
+    // Controls the background color of the resulting element.
+    // By convention specified as 0-255, but interpretation is up to the renderer.
+    // If no other config is specified, .backgroundColor will generate a RECTANGLE render command, otherwise it will be passed as a property to IMAGE or CUSTOM render commands.
+    Clay_Color backgroundColor;
+    // Perform an image editing style "Color Overlay" on this element and all its children, equivalent to
+    // glsl mix(elementColor, overlayColor.rgb, overlayColor.a)
+    Clay_Color overlayColor;
+    // Controls the "radius", or corner rounding of elements, including rectangles, borders and images.
+    Clay_CornerRadius cornerRadius;
+    // Controls settings related to aspect ratio scaling.
+    Clay_AspectRatioElementConfig aspectRatio;
+    // Controls settings related to image elements.
+    Clay_ImageElementConfig image;
+    // Controls whether and how an element "floats", which means it layers over the top of other elements in z order, and doesn't affect the position and size of siblings or parent elements.
+    // Note: in order to activate floating, .floating.attachTo must be set to something other than the default value.
+    Clay_FloatingElementConfig floating;
+    // Used to create CUSTOM render commands, usually to render element types not supported by Clay.
+    Clay_CustomElementConfig custom;
+    // Controls whether an element should clip its contents, as well as providing child x,y offset configuration for scrolling.
+    Clay_ClipElementConfig clip;
+    // Controls settings related to element borders, and will generate BORDER render commands.
+    Clay_BorderElementConfig border;
+    Clay_TransitionElementConfig transition;
+    // A pointer that will be transparently passed through to resulting render commands.
+    void *userData;
+} Clay_ElementDeclaration;
+
+CLAY__WRAPPER_STRUCT(Clay_ElementDeclaration);
+
+// Represents the type of error clay encountered while computing layout.
+typedef CLAY_PACKED_ENUM {
+    // A text measurement function wasn't provided using Clay_SetMeasureTextFunction(), or the provided function was null.
+    CLAY_ERROR_TYPE_TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED,
+    // Clay attempted to allocate its internal data structures but ran out of space.
+    // The arena passed to Clay_Initialize was created with a capacity smaller than that required by Clay_MinMemorySize().
+    CLAY_ERROR_TYPE_ARENA_CAPACITY_EXCEEDED,
+    // Clay ran out of capacity in its internal array for storing elements. This limit can be increased with Clay_SetMaxElementCount().
+    CLAY_ERROR_TYPE_ELEMENTS_CAPACITY_EXCEEDED,
+    // Clay ran out of capacity in its internal array for storing elements. This limit can be increased with Clay_SetMaxMeasureTextCacheWordCount().
+    CLAY_ERROR_TYPE_TEXT_MEASUREMENT_CAPACITY_EXCEEDED,
+    // Two elements were declared with exactly the same ID within one layout.
+    CLAY_ERROR_TYPE_DUPLICATE_ID,
+    // A floating element was declared using CLAY_ATTACH_TO_ELEMENT_ID and either an invalid .parentId was provided or no element with the provided .parentId was found.
+    CLAY_ERROR_TYPE_FLOATING_CONTAINER_PARENT_NOT_FOUND,
+    // An element was declared that using CLAY_SIZING_PERCENT but the percentage value was over 1. Percentage values are expected to be in the 0-1 range.
+    CLAY_ERROR_TYPE_PERCENTAGE_OVER_1,
+    // Clay encountered an internal error. It would be wonderful if you could report this so we can fix it!
+    CLAY_ERROR_TYPE_INTERNAL_ERROR,
+    // Clay__OpenElement was called more times than Clay__CloseElement, so there were still remaining open elements when the layout ended.
+    CLAY_ERROR_TYPE_UNBALANCED_OPEN_CLOSE,
+    CLAY_ERROR_TYPE_HASH_MAP_CAPACITY_EXCEEDED
+} Clay_ErrorType;
+
+// Data to identify the error that clay has encountered.
+typedef struct Clay_ErrorData {
+    // Represents the type of error clay encountered while computing layout.
+    // CLAY_ERROR_TYPE_TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED - A text measurement function wasn't provided using Clay_SetMeasureTextFunction(), or the provided function was null.
+    // CLAY_ERROR_TYPE_ARENA_CAPACITY_EXCEEDED - Clay attempted to allocate its internal data structures but ran out of space. The arena passed to Clay_Initialize was created with a capacity smaller than that required by Clay_MinMemorySize().
+    // CLAY_ERROR_TYPE_ELEMENTS_CAPACITY_EXCEEDED - Clay ran out of capacity in its internal array for storing elements. This limit can be increased with Clay_SetMaxElementCount().
+    // CLAY_ERROR_TYPE_TEXT_MEASUREMENT_CAPACITY_EXCEEDED - Clay ran out of capacity in its internal array for storing elements. This limit can be increased with Clay_SetMaxMeasureTextCacheWordCount().
+    // CLAY_ERROR_TYPE_DUPLICATE_ID - Two elements were declared with exactly the same ID within one layout.
+    // CLAY_ERROR_TYPE_FLOATING_CONTAINER_PARENT_NOT_FOUND - A floating element was declared using CLAY_ATTACH_TO_ELEMENT_ID and either an invalid .parentId was provided or no element with the provided .parentId was found.
+    // CLAY_ERROR_TYPE_PERCENTAGE_OVER_1 - An element was declared that using CLAY_SIZING_PERCENT but the percentage value was over 1. Percentage values are expected to be in the 0-1 range.
+    // CLAY_ERROR_TYPE_INTERNAL_ERROR - Clay encountered an internal error. It would be wonderful if you could report this so we can fix it!
+    // CLAY_ERROR_TYPE_UNBALANCED_OPEN_CLOSE - Clay__OpenElement was called more times than Clay__CloseElement, so there were still remaining open elements when the layout ended.
+    // CLAY_ERROR_TYPE_HASH_MAP_CAPACITY_EXCEEDED - Clay ran out of capacity in its internal hash map for storing element IDs -> elements. This limit can be increased with Clay_SetMaxElementCount().
+    Clay_ErrorType errorType;
+    // A string containing human-readable error text that explains the error in more detail.
+    Clay_String errorText;
+    // A transparent pointer passed through from when the error handler was first provided.
+    void *userData;
+} Clay_ErrorData;
+
+// A wrapper struct around Clay's error handler function.
+typedef struct {
+    // A user provided function to call when Clay encounters an error during layout.
+    void (*errorHandlerFunction)(Clay_ErrorData errorText);
+    // A pointer that will be transparently passed through to the error handler when it is called.
+    void *userData;
+} Clay_ErrorHandler;
+
+// Function Forward Declarations ---------------------------------
+
+// Public API functions ------------------------------------------
+
+// Returns the size, in bytes, of the minimum amount of memory Clay requires to operate at its current settings.
+CLAY_DLL_EXPORT uint32_t Clay_MinMemorySize(void);
+// Creates an arena for clay to use for its internal allocations, given a certain capacity in bytes and a pointer to an allocation of at least that size.
+// Intended to be used with Clay_MinMemorySize in the following way:
+// uint32_t minMemoryRequired = Clay_MinMemorySize();
+// Clay_Arena clayMemory = Clay_CreateArenaWithCapacityAndMemory(minMemoryRequired, malloc(minMemoryRequired));
+CLAY_DLL_EXPORT Clay_Arena Clay_CreateArenaWithCapacityAndMemory(size_t capacity, void *memory);
+// Sets the state of the "pointer" (i.e. the mouse or touch) in Clay's internal data. Used for detecting and responding to mouse events in the debug view,
+// as well as for Clay_Hovered() and scroll element handling.
+CLAY_DLL_EXPORT void Clay_SetPointerState(Clay_Vector2 position, bool pointerDown);
+// Returns the state of the "pointer" (i.e. the mouse or touch) which was set via Clay_SetPointerState().
+CLAY_DLL_EXPORT Clay_PointerData Clay_GetPointerState(void);
+// Initialize Clay's internal arena and setup required data before layout can begin. Only needs to be called once.
+// - arena can be created using Clay_CreateArenaWithCapacityAndMemory()
+// - layoutDimensions are the initial bounding dimensions of the layout (i.e. the screen width and height for a full screen layout)
+// - errorHandler is used by Clay to inform you if something has gone wrong in configuration or layout.
+CLAY_DLL_EXPORT Clay_Context* Clay_Initialize(Clay_Arena arena, Clay_Dimensions layoutDimensions, Clay_ErrorHandler errorHandler);
+// Returns the Context that clay is currently using. Used when using multiple instances of clay simultaneously.
+CLAY_DLL_EXPORT Clay_Context* Clay_GetCurrentContext(void);
+// Sets the context that clay will use to compute the layout.
+// Used to restore a context saved from Clay_GetCurrentContext when using multiple instances of clay simultaneously.
+CLAY_DLL_EXPORT void Clay_SetCurrentContext(Clay_Context* context);
+// Updates the state of Clay's internal scroll data, updating scroll content positions if scrollDelta is non zero, and progressing momentum scrolling.
+// - enableDragScrolling when set to true will enable mobile device like "touch drag" scroll of scroll containers, including momentum scrolling after the touch has ended.
+// - scrollDelta is the amount to scroll this frame on each axis in pixels.
+// - deltaTime is the time in seconds since the last "frame" (scroll update)
+CLAY_DLL_EXPORT void Clay_UpdateScrollContainers(bool enableDragScrolling, Clay_Vector2 scrollDelta, float deltaTime);
+// Returns the internally stored scroll offset for the currently open element.
+// Generally intended for use with clip elements to create scrolling containers.
+CLAY_DLL_EXPORT Clay_Vector2 Clay_GetScrollOffset(void);
+// Updates the layout dimensions in response to the window or outer container being resized.
+CLAY_DLL_EXPORT void Clay_SetLayoutDimensions(Clay_Dimensions dimensions);
+// Returns the current dimensions set by Clay_SetLayoutDimensions.
+CLAY_DLL_EXPORT Clay_Dimensions Clay_GetLayoutDimensions(void);
+// Called before starting any layout declarations.
+CLAY_DLL_EXPORT void Clay_BeginLayout(void);
+// Called when all layout declarations are finished.
+// Computes the layout and generates and returns the array of render commands to draw.
+CLAY_DLL_EXPORT Clay_RenderCommandArray Clay_EndLayout(float deltaTime);
+// Gets the ID of the currently open element, useful for retrieving IDs generated by CLAY_AUTO_ID()
+CLAY_DLL_EXPORT uint32_t Clay_GetOpenElementId(void);
+// Calculates a hash ID from the given idString.
+// Generally only used for dynamic strings when CLAY_ID("stringLiteral") can't be used.
+CLAY_DLL_EXPORT Clay_ElementId Clay_GetElementId(Clay_String idString);
+// Calculates a hash ID from the given idString and index.
+// - index is used to avoid constructing dynamic ID strings in loops.
+// Generally only used for dynamic strings when CLAY_IDI("stringLiteral", index) can't be used.
+CLAY_DLL_EXPORT Clay_ElementId Clay_GetElementIdWithIndex(Clay_String idString, uint32_t index);
+// Returns layout data such as the final calculated bounding box for an element with a given ID.
+// The returned Clay_ElementData contains a `found` bool that will be true if an element with the provided ID was found.
+// This ID can be calculated either with CLAY_ID() for string literal IDs, or Clay_GetElementId for dynamic strings.
+CLAY_DLL_EXPORT Clay_ElementData Clay_GetElementData(Clay_ElementId id);
+// Returns true if the pointer position provided by Clay_SetPointerState is within the current element's bounding box.
+// Works during element declaration, e.g. CLAY({ .backgroundColor = Clay_Hovered() ? BLUE : RED });
+CLAY_DLL_EXPORT bool Clay_Hovered(void);
+// Bind a callback that will be called when the pointer position provided by Clay_SetPointerState is within the current element's bounding box.
+// - onHoverFunction is a function pointer to a user defined function.
+// - userData is a pointer that will be transparently passed through when the onHoverFunction is called.
+CLAY_DLL_EXPORT void Clay_OnHover(void (*onHoverFunction)(Clay_ElementId elementId, Clay_PointerData pointerData, void *userData), void *userData);
+// An imperative function that returns true if the pointer position provided by Clay_SetPointerState is within the element with the provided ID's bounding box.
+// This ID can be calculated either with CLAY_ID() for string literal IDs, or Clay_GetElementId for dynamic strings.
+CLAY_DLL_EXPORT bool Clay_PointerOver(Clay_ElementId elementId);
+// Returns the array of element IDs that the pointer is currently over.
+CLAY_DLL_EXPORT Clay_ElementIdArray Clay_GetPointerOverIds(void);
+// Returns data representing the state of the scrolling element with the provided ID.
+// The returned Clay_ScrollContainerData contains a `found` bool that will be true if a scroll element was found with the provided ID.
+// An imperative function that returns true if the pointer position provided by Clay_SetPointerState is within the element with the provided ID's bounding box.
+// This ID can be calculated either with CLAY_ID() for string literal IDs, or Clay_GetElementId for dynamic strings.
+CLAY_DLL_EXPORT Clay_ScrollContainerData Clay_GetScrollContainerData(Clay_ElementId id);
+// Binds a callback function that Clay will call to determine the dimensions of a given string slice.
+// - measureTextFunction is a user provided function that adheres to the interface Clay_Dimensions (Clay_StringSlice text, Clay_TextElementConfig *config, void *userData);
+// - userData is a pointer that will be transparently passed through when the measureTextFunction is called.
+CLAY_DLL_EXPORT void Clay_SetMeasureTextFunction(Clay_Dimensions (*measureTextFunction)(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData), void *userData);
+// Experimental - Used in cases where Clay needs to integrate with a system that manages its own scrolling containers externally.
+// Please reach out if you plan to use this function, as it may be subject to change.
+CLAY_DLL_EXPORT void Clay_SetQueryScrollOffsetFunction(Clay_Vector2 (*queryScrollOffsetFunction)(uint32_t elementId, void *userData), void *userData);
+// A bounds-checked "get" function for the Clay_RenderCommandArray returned from Clay_EndLayout().
+CLAY_DLL_EXPORT Clay_RenderCommand * Clay_RenderCommandArray_Get(Clay_RenderCommandArray* array, int32_t index);
+// Enables and disables Clay's internal debug tools.
+// This state is retained and does not need to be set each frame.
+CLAY_DLL_EXPORT void Clay_SetDebugModeEnabled(bool enabled);
+// Returns true if Clay's internal debug tools are currently enabled.
+CLAY_DLL_EXPORT bool Clay_IsDebugModeEnabled(void);
+// Enables and disables visibility culling. By default, Clay will not generate render commands for elements whose bounding box is entirely outside the screen.
+CLAY_DLL_EXPORT void Clay_SetCullingEnabled(bool enabled);
+// Returns the maximum number of UI elements supported by Clay's current configuration.
+CLAY_DLL_EXPORT int32_t Clay_GetMaxElementCount(void);
+// Modifies the maximum number of UI elements supported by Clay's current configuration.
+// This may require reallocating additional memory, and re-calling Clay_Initialize();
+CLAY_DLL_EXPORT void Clay_SetMaxElementCount(int32_t maxElementCount);
+// Returns the maximum number of measured "words" (whitespace seperated runs of characters) that Clay can store in its internal text measurement cache.
+CLAY_DLL_EXPORT int32_t Clay_GetMaxMeasureTextCacheWordCount(void);
+// Modifies the maximum number of measured "words" (whitespace seperated runs of characters) that Clay can store in its internal text measurement cache.
+// This may require reallocating additional memory, and re-calling Clay_Initialize();
+CLAY_DLL_EXPORT void Clay_SetMaxMeasureTextCacheWordCount(int32_t maxMeasureTextCacheWordCount);
+// Resets Clay's internal text measurement cache. Useful if font mappings have changed or fonts have been reloaded.
+CLAY_DLL_EXPORT void Clay_ResetMeasureTextCache(void);
+// A built in transition function that uses the "Ease Out" curve
+CLAY_DLL_EXPORT bool Clay_EaseOut(Clay_TransitionCallbackArguments arguments);
+
+// Internal API functions required by macros ----------------------
+
+CLAY_DLL_EXPORT void Clay__OpenElement(void);
+CLAY_DLL_EXPORT void Clay__OpenElementWithId(Clay_ElementId elementId);
+CLAY_DLL_EXPORT void Clay__ConfigureOpenElement(const Clay_ElementDeclaration config);
+CLAY_DLL_EXPORT void Clay__ConfigureOpenElementPtr(const Clay_ElementDeclaration *config);
+CLAY_DLL_EXPORT void Clay__CloseElement(void);
+CLAY_DLL_EXPORT Clay_ElementId Clay__HashString(Clay_String key, uint32_t seed);
+CLAY_DLL_EXPORT Clay_ElementId Clay__HashStringWithOffset(Clay_String key, uint32_t offset, uint32_t seed);
+CLAY_DLL_EXPORT void Clay__OpenTextElement(Clay_String text, Clay_TextElementConfig textConfig);
+
+extern Clay_Color Clay__debugViewHighlightColor;
+extern uint32_t Clay__debugViewWidth;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // CLAY_HEADER
+
+// -----------------------------------------
+// IMPLEMENTATION --------------------------
+// -----------------------------------------
+#ifdef CLAY_IMPLEMENTATION
+#undef CLAY_IMPLEMENTATION
+
+#ifndef CLAY__NULL
+#define CLAY__NULL 0
+#endif
+
+#ifndef CLAY__MAXFLOAT
+#define CLAY__MAXFLOAT 3.40282346638528859812e+38F
+#endif
+
+Clay_LayoutConfig CLAY_LAYOUT_DEFAULT = CLAY__DEFAULT_STRUCT;
+
+Clay_Color Clay__Color_DEFAULT = CLAY__DEFAULT_STRUCT;
+Clay_CornerRadius Clay__CornerRadius_DEFAULT = CLAY__DEFAULT_STRUCT;
+Clay_BorderWidth Clay__BorderWidth_DEFAULT = CLAY__DEFAULT_STRUCT;
+
+// The below functions define array bounds checking and convenience functions for a provided type.
+#define CLAY__ARRAY_DEFINE_FUNCTIONS(typeName, arrayName)                                                       \
+                                                                                                                \
+typedef struct                                                                                                  \
+{                                                                                                               \
+    int32_t length;                                                                                             \
+    typeName *internalArray;                                                                                    \
+} arrayName##Slice;                                                                                             \
+                                                                                                                \
+typeName typeName##_DEFAULT = CLAY__DEFAULT_STRUCT;                                                             \
+                                                                                                                \
+arrayName arrayName##_Allocate_Arena(int32_t capacity, Clay_Arena *arena) {                                     \
+    return CLAY__INIT(arrayName){.capacity = capacity, .length = 0,                                             \
+        .internalArray = (typeName *)Clay__Array_Allocate_Arena(capacity, sizeof(typeName), arena)};            \
+}                                                                                                               \
+                                                                                                                \
+typeName *arrayName##_Get(arrayName *array, int32_t index) {                                                    \
+    return Clay__Array_RangeCheck(index, array->length) ? &array->internalArray[index] : &typeName##_DEFAULT;   \
+}                                                                                                               \
+                                                                                                                \
+typeName arrayName##_GetValue(arrayName *array, int32_t index) {                                                \
+    return Clay__Array_RangeCheck(index, array->length) ? array->internalArray[index] : typeName##_DEFAULT;     \
+}                                                                                                               \
+                                                                                                                \
+typeName *arrayName##_GetCheckCapacity(arrayName *array, int32_t index) {                                                    \
+    return Clay__Array_RangeCheck(index, array->capacity) ? &array->internalArray[index] : &typeName##_DEFAULT;   \
+}                                                                                                               \
+                                                                                                                \
+typeName *arrayName##_Add(arrayName *array, typeName item) {                                                    \
+    if (Clay__Array_AddCapacityCheck(array->length, array->capacity)) {                                         \
+        array->internalArray[array->length++] = item;                                                           \
+        return &array->internalArray[array->length - 1];                                                        \
+    }                                                                                                           \
+    return &typeName##_DEFAULT;                                                                                 \
+}                                                                                                               \
+                                                                                                                \
+typeName *arrayName##Slice_Get(arrayName##Slice *slice, int32_t index) {                                        \
+    return Clay__Array_RangeCheck(index, slice->length) ? &slice->internalArray[index] : &typeName##_DEFAULT;   \
+}                                                                                                               \
+                                                                                                                \
+typeName arrayName##_RemoveSwapback(arrayName *array, int32_t index) {                                          \
+	if (Clay__Array_RangeCheck(index, array->length)) {                                                         \
+		array->length--;                                                                                        \
+		typeName removed = array->internalArray[index];                                                         \
+		array->internalArray[index] = array->internalArray[array->length];                                      \
+		return removed;                                                                                         \
+	}                                                                                                           \
+	return typeName##_DEFAULT;                                                                                  \
+}                                                                                                               \
+                                                                                                                \
+typeName* arrayName##_Set(arrayName *array, int32_t index, typeName value) {                                    \
+	if (Clay__Array_RangeCheck(index, array->capacity)) {                                                       \
+		array->internalArray[index] = value;                                                                    \
+		array->length = index < array->length ? array->length : index + 1;                                      \
+        return &array->internalArray[index];\
+	}                                                                                                           \
+    return NULL;\
+}                                                                                                               \
+                                                                                                                \
+typeName* arrayName##_Set_DontTouchLength(arrayName *array, int32_t index, typeName value) {                                    \
+	if (Clay__Array_RangeCheck(index, array->capacity)) {                                                       \
+		array->internalArray[index] = value;                                                                    \
+        return &array->internalArray[index];\
+	}                                                                                                           \
+    return NULL;\
+}  \
+
+#define CLAY__ARRAY_DEFINE(typeName, arrayName)     \
+typedef struct                                      \
+{                                                   \
+    int32_t capacity;                               \
+    int32_t length;                                 \
+    typeName *internalArray;                        \
+} arrayName;                                        \
+                                                    \
+CLAY__ARRAY_DEFINE_FUNCTIONS(typeName, arrayName)   \
+
+Clay_Context *Clay__currentContext;
+int32_t Clay__defaultMaxElementCount = 8192;
+int32_t Clay__defaultMaxMeasureTextWordCacheCount = 16384;
+
+void Clay__ErrorHandlerFunctionDefault(Clay_ErrorData errorText) {
+    (void) errorText;
+}
+
+Clay_String CLAY__SPACECHAR = { .length = 1, .chars = " " };
+Clay_String CLAY__STRING_DEFAULT = { .length = 0, .chars = NULL };
+
+typedef struct {
+    bool maxElementsExceeded;
+    bool maxRenderCommandsExceeded;
+    bool maxTextMeasureCacheExceeded;
+    bool textMeasurementFunctionNotSet;
+    bool hashMapCapacityExceeded;
+} Clay_BooleanWarnings;
+
+typedef struct {
+    Clay_String baseMessage;
+    Clay_String dynamicMessage;
+} Clay__Warning;
+
+Clay__Warning CLAY__WARNING_DEFAULT = CLAY__DEFAULT_STRUCT;
+
+typedef struct {
+    int32_t capacity;
+    int32_t length;
+    Clay__Warning *internalArray;
+} Clay__WarningArray;
+
+Clay__WarningArray Clay__WarningArray_Allocate_Arena(int32_t capacity, Clay_Arena *arena);
+Clay__Warning *Clay__WarningArray_Add(Clay__WarningArray *array, Clay__Warning item);
+void* Clay__Array_Allocate_Arena(int32_t capacity, uint32_t itemSize, Clay_Arena *arena);
+bool Clay__Array_RangeCheck(int32_t index, int32_t length);
+bool Clay__Array_AddCapacityCheck(int32_t length, int32_t capacity);
+
+CLAY__ARRAY_DEFINE(bool, Clay__boolArray)
+CLAY__ARRAY_DEFINE(int32_t, Clay__int32_tArray)
+CLAY__ARRAY_DEFINE(char, Clay__charArray)
+CLAY__ARRAY_DEFINE_FUNCTIONS(Clay_ElementId, Clay_ElementIdArray)
+CLAY__ARRAY_DEFINE(Clay_String, Clay__StringArray)
+CLAY__ARRAY_DEFINE_FUNCTIONS(Clay_RenderCommand, Clay_RenderCommandArray)
+
+typedef struct {
+    Clay_Dimensions dimensions;
+    Clay_String line;
+} Clay__WrappedTextLine;
+
+CLAY__ARRAY_DEFINE(Clay__WrappedTextLine, Clay__WrappedTextLineArray)
+
+typedef struct {
+    Clay_String text;
+    Clay_Dimensions preferredDimensions;
+    Clay__WrappedTextLineArraySlice wrappedLines;
+} Clay__TextElementData;
+
+typedef struct {
+    int32_t *elements;
+    uint16_t length;
+} Clay__LayoutElementChildren;
+
+typedef struct Clay_LayoutElement {
+    Clay__LayoutElementChildren children;
+    Clay_Dimensions dimensions;
+    Clay_Dimensions minDimensions;
+    union {
+        Clay_ElementDeclaration config;
+        struct {
+            Clay_TextElementConfig textConfig;
+            Clay__TextElementData textElementData;
+        };
+    };
+    uint32_t id;
+    uint16_t floatingChildrenCount;
+    bool isTextElement;
+    // True if the element is currently in an exit transition, and is "synthetic"
+    // i.e. data was retained from previous frames
+    bool exiting;
+} Clay_LayoutElement;
+
+CLAY__ARRAY_DEFINE(Clay_LayoutElement, Clay_LayoutElementArray)
+
+typedef struct {
+    Clay_LayoutElement *layoutElement;
+    Clay_BoundingBox boundingBox;
+    Clay_Dimensions contentSize;
+    Clay_Vector2 scrollOrigin;
+    Clay_Vector2 pointerOrigin;
+    Clay_Vector2 scrollMomentum;
+    Clay_Vector2 scrollPosition;
+    Clay_Vector2 previousDelta;
+    float momentumTime;
+    uint32_t elementId;
+    bool openThisFrame;
+    bool pointerScrollActive;
+} Clay__ScrollContainerDataInternal;
+
+CLAY__ARRAY_DEFINE(Clay__ScrollContainerDataInternal, Clay__ScrollContainerDataInternalArray)
+
+// Data representing the current internal state of a transition element.
+typedef struct Clay__TransitionDataInternal {
+    Clay_TransitionData initialState;
+    Clay_TransitionData currentState;
+    Clay_TransitionData targetState;
+    Clay_LayoutElement* elementThisFrame;
+    Clay_Vector2 oldParentRelativePosition;
+    uint32_t elementId;
+    uint32_t parentId;
+    uint32_t siblingIndex;
+    float elapsedTime;
+    Clay_TransitionState state;
+    bool transitionOut;
+    bool reparented;
+    Clay_TransitionProperty activeProperties;
+} Clay__TransitionDataInternal;
+
+CLAY__ARRAY_DEFINE(Clay__TransitionDataInternal, Clay__TransitionDataInternalArray)
+
+typedef struct { // todo get this struct into a single cache line
+    Clay_BoundingBox boundingBox;
+    Clay_ElementId elementId;
+    Clay_LayoutElement* layoutElement;
+    void (*onHoverFunction)(Clay_ElementId elementId, Clay_PointerData pointerInfo, void *userData);
+    void *hoverFunctionUserData;
+    int32_t nextIndex;
+    uint32_t generation;
+    bool appearedThisFrame;
+    struct {
+        bool collision;
+        bool collapsed;
+    } debugData;
+} Clay_LayoutElementHashMapItem;
+
+CLAY__ARRAY_DEFINE(Clay_LayoutElementHashMapItem, Clay__LayoutElementHashMapItemArray)
+
+typedef struct {
+    int32_t startOffset;
+    int32_t length;
+    float width;
+    int32_t next;
+} Clay__MeasuredWord;
+
+CLAY__ARRAY_DEFINE(Clay__MeasuredWord, Clay__MeasuredWordArray)
+
+typedef struct {
+    Clay_Dimensions unwrappedDimensions;
+    int32_t measuredWordsStartIndex;
+    float minWidth;
+    bool containsNewlines;
+    // Hash map data
+    uint32_t id;
+    int32_t nextIndex;
+    uint32_t generation;
+} Clay__MeasureTextCacheItem;
+
+CLAY__ARRAY_DEFINE(Clay__MeasureTextCacheItem, Clay__MeasureTextCacheItemArray)
+
+typedef struct {
+    Clay_LayoutElement *layoutElement;
+    Clay_Vector2 position;
+    Clay_Vector2 nextChildOffset;
+    bool parentMovedThisFramed; // Used to relativise transitions
+} Clay__LayoutElementTreeNode;
+
+CLAY__ARRAY_DEFINE(Clay__LayoutElementTreeNode, Clay__LayoutElementTreeNodeArray)
+
+typedef struct {
+    int32_t layoutElementIndex;
+    uint32_t parentId; // This can be zero in the case of the root layout tree
+    uint32_t clipElementId; // This can be zero if there is no clip element
+    int16_t zIndex;
+    Clay_Vector2 pointerOffset; // Only used when scroll containers are managed externally
+} Clay__LayoutElementTreeRoot;
+
+CLAY__ARRAY_DEFINE(Clay__LayoutElementTreeRoot, Clay__LayoutElementTreeRootArray)
+
+struct Clay_Context {
+    int32_t maxElementCount;
+    int32_t maxMeasureTextCacheWordCount;
+    int32_t exitingElementsLength;
+    int32_t exitingElementsChildrenLength;
+    bool warningsEnabled;
+    bool rootResizedLastFrame;
+    Clay_ErrorHandler errorHandler;
+    Clay_BooleanWarnings booleanWarnings;
+    Clay__WarningArray warnings;
+
+    Clay_PointerData pointerInfo;
+    Clay_Dimensions layoutDimensions;
+    Clay_ElementId dynamicElementIndexBaseHash;
+    uint32_t dynamicElementIndex;
+    bool debugModeEnabled;
+    bool disableCulling;
+    bool externalScrollHandlingEnabled;
+    uint32_t debugSelectedElementId;
+    uint32_t generation;
+    uintptr_t arenaResetOffset;
+    void *measureTextUserData;
+    void *queryScrollOffsetUserData;
+    Clay_Arena internalArena;
+    // Layout Elements / Render Commands
+    Clay_LayoutElementArray layoutElements;
+    Clay_RenderCommandArray renderCommands;
+    Clay__int32_tArray openLayoutElementStack;
+    Clay__int32_tArray layoutElementChildren;
+    Clay__int32_tArray layoutElementChildrenBuffer;
+    Clay__int32_tArray reusableElementIndexBuffer;
+    Clay__int32_tArray layoutElementClipElementIds;
+    // Misc Data Structures
+    Clay__StringArray layoutElementIdStrings;
+    Clay__WrappedTextLineArray wrappedTextLines;
+    Clay__LayoutElementTreeNodeArray layoutElementTreeNodeArray1;
+    Clay__LayoutElementTreeRootArray layoutElementTreeRoots;
+    Clay__LayoutElementHashMapItemArray layoutElementsHashMapInternal;
+    Clay__int32_tArray layoutElementsHashMap;
+    Clay__int32_tArray layoutElementsHashMapFreeList;
+    Clay__MeasureTextCacheItemArray measureTextHashMapInternal;
+    Clay__int32_tArray measureTextHashMapInternalFreeList;
+    Clay__int32_tArray measureTextHashMap;
+    Clay__MeasuredWordArray measuredWords;
+    Clay__int32_tArray measuredWordsFreeList;
+    Clay__int32_tArray openClipElementStack;
+    Clay_ElementIdArray pointerOverIds;
+    Clay__ScrollContainerDataInternalArray scrollContainerDatas;
+    Clay__TransitionDataInternalArray transitionDatas;
+    Clay__boolArray treeNodeVisited;
+    Clay__charArray dynamicStringData;
+};
+
+Clay_Context* Clay__Context_Allocate_Arena(Clay_Arena *arena) {
+    size_t totalSizeBytes = sizeof(Clay_Context);
+    if (totalSizeBytes > arena->capacity)
+    {
+        return NULL;
+    }
+    arena->nextAllocation += totalSizeBytes;
+    return (Clay_Context*)(arena->memory);
+}
+
+Clay_String Clay__WriteStringToCharBuffer(Clay__charArray *buffer, Clay_String string) {
+    for (int32_t i = 0; i < string.length; i++) {
+        buffer->internalArray[buffer->length + i] = string.chars[i];
+    }
+    buffer->length += string.length;
+    return CLAY__INIT(Clay_String) { .length = string.length, .chars = (const char *)(buffer->internalArray + buffer->length - string.length) };
+}
+
+#ifdef CLAY_WASM
+    __attribute__((import_module("clay"), import_name("measureTextFunction"))) Clay_Dimensions Clay__MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData);
+    __attribute__((import_module("clay"), import_name("queryScrollOffsetFunction"))) Clay_Vector2 Clay__QueryScrollOffset(uint32_t elementId, void *userData);
+#else
+    Clay_Dimensions (*Clay__MeasureText)(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData);
+    Clay_Vector2 (*Clay__QueryScrollOffset)(uint32_t elementId, void *userData);
+#endif
+
+Clay_LayoutElement* Clay__GetOpenLayoutElement(void) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    return Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 1));
+}
+
+Clay_LayoutElement* Clay__GetParentElement(void) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    return Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 2));
+}
+
+uint32_t Clay__GetParentElementId(void) {
+    return Clay__GetParentElement()->id;
+}
+
+bool Clay__BorderHasAnyWidth(Clay_BorderElementConfig* borderConfig) {
+    return borderConfig->width.betweenChildren > 0 || borderConfig->width.left > 0 || borderConfig->width.right > 0 || borderConfig->width.top > 0 || borderConfig->width.bottom > 0;
+}
+
+Clay_ElementId Clay__HashNumber(const uint32_t offset, const uint32_t seed) {
+    uint32_t hash = seed;
+    hash += (offset + 48);
+    hash += (hash << 10);
+    hash ^= (hash >> 6);
+
+    hash += (hash << 3);
+    hash ^= (hash >> 11);
+    hash += (hash << 15);
+    return CLAY__INIT(Clay_ElementId) { .id = hash + 1, .offset = offset, .baseId = seed, .stringId = CLAY__STRING_DEFAULT }; // Reserve the hash result of zero as "null id"
+}
+
+Clay_ElementId Clay__HashString(Clay_String key, const uint32_t seed) {
+    uint32_t hash = seed;
+
+    for (int32_t i = 0; i < key.length; i++) {
+        hash += key.chars[i];
+        hash += (hash << 10);
+        hash ^= (hash >> 6);
+    }
+
+    hash += (hash << 3);
+    hash ^= (hash >> 11);
+    hash += (hash << 15);
+    return CLAY__INIT(Clay_ElementId) { .id = hash + 1, .offset = 0, .baseId = hash + 1, .stringId = key }; // Reserve the hash result of zero as "null id"
+}
+
+Clay_ElementId Clay__HashStringWithOffset(Clay_String key, const uint32_t offset, const uint32_t seed) {
+    uint32_t hash = 0;
+    uint32_t base = seed;
+
+    for (int32_t i = 0; i < key.length; i++) {
+        base += key.chars[i];
+        base += (base << 10);
+        base ^= (base >> 6);
+    }
+    hash = base;
+    hash += offset;
+    hash += (hash << 10);
+    hash ^= (hash >> 6);
+
+    hash += (hash << 3);
+    base += (base << 3);
+    hash ^= (hash >> 11);
+    base ^= (base >> 11);
+    hash += (hash << 15);
+    base += (base << 15);
+    return CLAY__INIT(Clay_ElementId) { .id = hash + 1, .offset = offset, .baseId = base + 1, .stringId = key }; // Reserve the hash result of zero as "null id"
+}
+
+#if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64))
+static inline __m128i Clay__SIMDRotateLeft(__m128i x, int r) {
+    return _mm_or_si128(_mm_slli_epi64(x, r), _mm_srli_epi64(x, 64 - r));
+}
+
+static inline void Clay__SIMDARXMix(__m128i* a, __m128i* b) {
+    *a = _mm_add_epi64(*a, *b);
+    *b = _mm_xor_si128(Clay__SIMDRotateLeft(*b, 17), *a);
+}
+
+uint64_t Clay__HashData(const uint8_t* data, size_t length) {
+    // Pinched these constants from the BLAKE implementation
+    __m128i v0 = _mm_set1_epi64x(0x6a09e667f3bcc908ULL);
+    __m128i v1 = _mm_set1_epi64x(0xbb67ae8584caa73bULL);
+    __m128i v2 = _mm_set1_epi64x(0x3c6ef372fe94f82bULL);
+    __m128i v3 = _mm_set1_epi64x(0xa54ff53a5f1d36f1ULL);
+
+    uint8_t overflowBuffer[16] = { 0 };  // Temporary buffer for small inputs
+
+    while (length > 0) {
+        __m128i msg;
+        if (length >= 16) {
+            msg = _mm_loadu_si128((const __m128i*)data);
+            data += 16;
+            length -= 16;
+        }
+        else {
+            for (size_t i = 0; i < length; i++) {
+                overflowBuffer[i] = data[i];
+            }
+            msg = _mm_loadu_si128((const __m128i*)overflowBuffer);
+            length = 0;
+        }
+
+        v0 = _mm_xor_si128(v0, msg);
+        Clay__SIMDARXMix(&v0, &v1);
+        Clay__SIMDARXMix(&v2, &v3);
+
+        v0 = _mm_add_epi64(v0, v2);
+        v1 = _mm_add_epi64(v1, v3);
+    }
+
+    Clay__SIMDARXMix(&v0, &v1);
+    Clay__SIMDARXMix(&v2, &v3);
+    v0 = _mm_add_epi64(v0, v2);
+    v1 = _mm_add_epi64(v1, v3);
+    v0 = _mm_add_epi64(v0, v1);
+
+    uint64_t result[2];
+    _mm_storeu_si128((__m128i*)result, v0);
+
+    return result[0] ^ result[1];
+}
+#elif !defined(CLAY_DISABLE_SIMD) && defined(__aarch64__)
+static inline void Clay__SIMDARXMix(uint64x2_t* a, uint64x2_t* b) {
+    *a = vaddq_u64(*a, *b);
+    *b = veorq_u64(vorrq_u64(vshlq_n_u64(*b, 17), vshrq_n_u64(*b, 64 - 17)), *a);
+}
+
+uint64_t Clay__HashData(const uint8_t* data, size_t length) {
+    // Pinched these constants from the BLAKE implementation
+    uint64x2_t v0 = vdupq_n_u64(0x6a09e667f3bcc908ULL);
+    uint64x2_t v1 = vdupq_n_u64(0xbb67ae8584caa73bULL);
+    uint64x2_t v2 = vdupq_n_u64(0x3c6ef372fe94f82bULL);
+    uint64x2_t v3 = vdupq_n_u64(0xa54ff53a5f1d36f1ULL);
+
+    uint8_t overflowBuffer[8] = { 0 };
+
+    while (length > 0) {
+        uint64x2_t msg;
+        if (length > 16) {
+            msg = vld1q_u64((const uint64_t*)data);
+            data += 16;
+            length -= 16;
+        }
+        else if (length > 8) {
+            msg = vcombine_u64(vld1_u64((const uint64_t*)data), vdup_n_u64(0));
+            data += 8;
+            length -= 8;
+        }
+        else {
+            for (size_t i = 0; i < length; i++) {
+                overflowBuffer[i] = data[i];
+            }
+            uint8x8_t lower = vld1_u8(overflowBuffer);
+            msg = vreinterpretq_u64_u8(vcombine_u8(lower, vdup_n_u8(0)));
+            length = 0;
+        }
+        v0 = veorq_u64(v0, msg);
+        Clay__SIMDARXMix(&v0, &v1);
+        Clay__SIMDARXMix(&v2, &v3);
+
+        v0 = vaddq_u64(v0, v2);
+        v1 = vaddq_u64(v1, v3);
+    }
+
+    Clay__SIMDARXMix(&v0, &v1);
+    Clay__SIMDARXMix(&v2, &v3);
+    v0 = vaddq_u64(v0, v2);
+    v1 = vaddq_u64(v1, v3);
+    v0 = vaddq_u64(v0, v1);
+
+    uint64_t result[2];
+    vst1q_u64(result, v0);
+
+    return result[0] ^ result[1];
+}
+#else
+uint64_t Clay__HashData(const uint8_t* data, size_t length) {
+    uint64_t hash = 0;
+
+    for (size_t i = 0; i < length; i++) {
+        hash += data[i];
+        hash += (hash << 10);
+        hash ^= (hash >> 6);
+    }
+    return hash;
+}
+#endif
+
+uint32_t Clay__HashStringContentsWithConfig(Clay_String *text, Clay_TextElementConfig *config) {
+    uint32_t hash = 0;
+    if (text->isStaticallyAllocated) {
+        hash += (uintptr_t)text->chars;
+        hash += (hash << 10);
+        hash ^= (hash >> 6);
+        hash += text->length;
+        hash += (hash << 10);
+        hash ^= (hash >> 6);
+    } else {
+        hash = Clay__HashData((const uint8_t *)text->chars, text->length) % UINT32_MAX;
+    }
+
+    hash += config->fontId;
+    hash += (hash << 10);
+    hash ^= (hash >> 6);
+
+    hash += config->fontSize;
+    hash += (hash << 10);
+    hash ^= (hash >> 6);
+
+    hash += config->letterSpacing;
+    hash += (hash << 10);
+    hash ^= (hash >> 6);
+
+    hash += (hash << 3);
+    hash ^= (hash >> 11);
+    hash += (hash << 15);
+    return hash + 1; // Reserve the hash result of zero as "null id"
+}
+
+Clay__MeasuredWord *Clay__AddMeasuredWord(Clay__MeasuredWord word, Clay__MeasuredWord *previousWord) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    if (context->measuredWordsFreeList.length > 0) {
+        uint32_t newItemIndex = Clay__int32_tArray_GetValue(&context->measuredWordsFreeList, (int)context->measuredWordsFreeList.length - 1);
+        context->measuredWordsFreeList.length--;
+        Clay__MeasuredWordArray_Set(&context->measuredWords, (int)newItemIndex, word);
+        previousWord->next = (int32_t)newItemIndex;
+        return Clay__MeasuredWordArray_Get(&context->measuredWords, (int)newItemIndex);
+    } else {
+        previousWord->next = (int32_t)context->measuredWords.length;
+        return Clay__MeasuredWordArray_Add(&context->measuredWords, word);
+    }
+}
+
+Clay__MeasureTextCacheItem *Clay__MeasureTextCached(Clay_String *text, Clay_TextElementConfig *config) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    #ifndef CLAY_WASM
+    if (!Clay__MeasureText) {
+        if (!context->booleanWarnings.textMeasurementFunctionNotSet) {
+            context->booleanWarnings.textMeasurementFunctionNotSet = true;
+            context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+                    .errorType = CLAY_ERROR_TYPE_TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED,
+                    .errorText = CLAY_STRING("Clay's internal MeasureText function is null. You may have forgotten to call Clay_SetMeasureTextFunction(), or passed a NULL function pointer by mistake."),
+                    .userData = context->errorHandler.userData });
+        }
+        return &Clay__MeasureTextCacheItem_DEFAULT;
+    }
+    #endif
+    uint32_t id = Clay__HashStringContentsWithConfig(text, config);
+    uint32_t hashBucket = id % (context->maxMeasureTextCacheWordCount / 32);
+    int32_t elementIndexPrevious = 0;
+    int32_t elementIndex = context->measureTextHashMap.internalArray[hashBucket];
+    while (elementIndex != 0) {
+        Clay__MeasureTextCacheItem *hashEntry = Clay__MeasureTextCacheItemArray_Get(&context->measureTextHashMapInternal, elementIndex);
+        if (hashEntry->id == id) {
+            hashEntry->generation = context->generation;
+            return hashEntry;
+        }
+        // This element hasn't been seen in a few frames, delete the hash map item
+        if (context->generation - hashEntry->generation > 2) {
+            // Add all the measured words that were included in this measurement to the freelist
+            int32_t nextWordIndex = hashEntry->measuredWordsStartIndex;
+            while (nextWordIndex != -1) {
+                Clay__MeasuredWord *measuredWord = Clay__MeasuredWordArray_Get(&context->measuredWords, nextWordIndex);
+                Clay__int32_tArray_Add(&context->measuredWordsFreeList, nextWordIndex);
+                nextWordIndex = measuredWord->next;
+            }
+
+            int32_t nextIndex = hashEntry->nextIndex;
+            Clay__MeasureTextCacheItemArray_Set(&context->measureTextHashMapInternal, elementIndex, CLAY__INIT(Clay__MeasureTextCacheItem) { .measuredWordsStartIndex = -1 });
+            Clay__int32_tArray_Add(&context->measureTextHashMapInternalFreeList, elementIndex);
+            if (elementIndexPrevious == 0) {
+                context->measureTextHashMap.internalArray[hashBucket] = nextIndex;
+            } else {
+                Clay__MeasureTextCacheItem *previousHashEntry = Clay__MeasureTextCacheItemArray_Get(&context->measureTextHashMapInternal, elementIndexPrevious);
+                previousHashEntry->nextIndex = nextIndex;
+            }
+            elementIndex = nextIndex;
+        } else {
+            elementIndexPrevious = elementIndex;
+            elementIndex = hashEntry->nextIndex;
+        }
+    }
+
+    int32_t newItemIndex = 0;
+    Clay__MeasureTextCacheItem newCacheItem = { .measuredWordsStartIndex = -1, .id = id, .generation = context->generation };
+    Clay__MeasureTextCacheItem *measured = NULL;
+    if (context->measureTextHashMapInternalFreeList.length > 0) {
+        newItemIndex = Clay__int32_tArray_GetValue(&context->measureTextHashMapInternalFreeList, context->measureTextHashMapInternalFreeList.length - 1);
+        context->measureTextHashMapInternalFreeList.length--;
+        Clay__MeasureTextCacheItemArray_Set(&context->measureTextHashMapInternal, newItemIndex, newCacheItem);
+        measured = Clay__MeasureTextCacheItemArray_Get(&context->measureTextHashMapInternal, newItemIndex);
+    } else {
+        if (context->measureTextHashMapInternal.length == context->measureTextHashMapInternal.capacity - 1) {
+            if (!context->booleanWarnings.maxTextMeasureCacheExceeded) {
+                context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+                        .errorType = CLAY_ERROR_TYPE_ELEMENTS_CAPACITY_EXCEEDED,
+                        .errorText = CLAY_STRING("Clay ran out of capacity while attempting to measure text elements. Try using Clay_SetMaxElementCount() with a higher value."),
+                        .userData = context->errorHandler.userData });
+                context->booleanWarnings.maxTextMeasureCacheExceeded = true;
+            }
+            return &Clay__MeasureTextCacheItem_DEFAULT;
+        }
+        measured = Clay__MeasureTextCacheItemArray_Add(&context->measureTextHashMapInternal, newCacheItem);
+        newItemIndex = context->measureTextHashMapInternal.length - 1;
+    }
+
+    int32_t start = 0;
+    int32_t end = 0;
+    float lineWidth = 0;
+    float measuredWidth = 0;
+    float measuredHeight = 0;
+    float spaceWidth = Clay__MeasureText(CLAY__INIT(Clay_StringSlice) { .length = 1, .chars = CLAY__SPACECHAR.chars, .baseChars = CLAY__SPACECHAR.chars }, config, context->measureTextUserData).width;
+    Clay__MeasuredWord tempWord = { .next = -1 };
+    Clay__MeasuredWord *previousWord = &tempWord;
+    while (end < text->length) {
+        if (context->measuredWords.length == context->measuredWords.capacity - 1) {
+            if (!context->booleanWarnings.maxTextMeasureCacheExceeded) {
+                context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+                    .errorType = CLAY_ERROR_TYPE_TEXT_MEASUREMENT_CAPACITY_EXCEEDED,
+                    .errorText = CLAY_STRING("Clay has run out of space in it's internal text measurement cache. Try using Clay_SetMaxMeasureTextCacheWordCount() (default 16384, with 1 unit storing 1 measured word)."),
+                    .userData = context->errorHandler.userData });
+                context->booleanWarnings.maxTextMeasureCacheExceeded = true;
+            }
+            return &Clay__MeasureTextCacheItem_DEFAULT;
+        }
+        char current = text->chars[end];
+        if (current == ' ' || current == '\n') {
+            int32_t length = end - start;
+            Clay_Dimensions dimensions = CLAY__DEFAULT_STRUCT;
+            if (length > 0) {
+                dimensions = Clay__MeasureText(CLAY__INIT(Clay_StringSlice) {.length = length, .chars = &text->chars[start], .baseChars = text->chars}, config, context->measureTextUserData);
+            }
+            measured->minWidth = CLAY__MAX(dimensions.width, measured->minWidth);
+            measuredHeight = CLAY__MAX(measuredHeight, dimensions.height);
+            if (current == ' ') {
+                dimensions.width += spaceWidth;
+                previousWord = Clay__AddMeasuredWord(CLAY__INIT(Clay__MeasuredWord) { .startOffset = start, .length = length + 1, .width = dimensions.width, .next = -1 }, previousWord);
+                lineWidth += dimensions.width;
+            }
+            if (current == '\n') {
+                if (length > 0) {
+                    previousWord = Clay__AddMeasuredWord(CLAY__INIT(Clay__MeasuredWord) { .startOffset = start, .length = length, .width = dimensions.width, .next = -1 }, previousWord);
+                }
+                previousWord = Clay__AddMeasuredWord(CLAY__INIT(Clay__MeasuredWord) { .startOffset = end + 1, .length = 0, .width = 0, .next = -1 }, previousWord);
+                lineWidth += dimensions.width;
+                measuredWidth = CLAY__MAX(lineWidth, measuredWidth);
+                measured->containsNewlines = true;
+                lineWidth = 0;
+            }
+            start = end + 1;
+        }
+        end++;
+    }
+    if (end - start > 0) {
+        Clay_Dimensions dimensions = Clay__MeasureText(CLAY__INIT(Clay_StringSlice) { .length = end - start, .chars = &text->chars[start], .baseChars = text->chars }, config, context->measureTextUserData);
+        Clay__AddMeasuredWord(CLAY__INIT(Clay__MeasuredWord) { .startOffset = start, .length = end - start, .width = dimensions.width, .next = -1 }, previousWord);
+        lineWidth += dimensions.width;
+        measuredHeight = CLAY__MAX(measuredHeight, dimensions.height);
+        measured->minWidth = CLAY__MAX(dimensions.width, measured->minWidth);
+    }
+    measuredWidth = CLAY__MAX(lineWidth, measuredWidth) - config->letterSpacing;
+
+    measured->measuredWordsStartIndex = tempWord.next;
+    measured->unwrappedDimensions.width = measuredWidth;
+    measured->unwrappedDimensions.height = measuredHeight;
+
+    if (elementIndexPrevious != 0) {
+        Clay__MeasureTextCacheItemArray_Get(&context->measureTextHashMapInternal, elementIndexPrevious)->nextIndex = newItemIndex;
+    } else {
+        context->measureTextHashMap.internalArray[hashBucket] = newItemIndex;
+    }
+    return measured;
+}
+
+bool Clay__PointIsInsideRect(Clay_Vector2 point, Clay_BoundingBox rect) {
+    return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
+}
+
+Clay_LayoutElementHashMapItem* Clay__AddHashMapItem(Clay_ElementId elementId, Clay_LayoutElement* layoutElement) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    if (context->layoutElementsHashMapInternal.length == context->layoutElementsHashMapInternal.capacity - 1) {
+        if (!context->booleanWarnings.hashMapCapacityExceeded) {
+            context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+                .errorType = CLAY_ERROR_TYPE_HASH_MAP_CAPACITY_EXCEEDED,
+                .errorText = CLAY_STRING("Clay has run out of space in it's internal element ID hashmap.  Try using Clay_SetMaxElementCount() with a higher value."),
+                .userData = context->errorHandler.userData });
+            context->booleanWarnings.hashMapCapacityExceeded = true;
+        }
+        return NULL;
+    }
+    Clay_LayoutElementHashMapItem item = { .elementId = elementId, .layoutElement = layoutElement, .nextIndex = -1, .generation = context->generation + 1, .appearedThisFrame = true };
+    uint32_t hashBucket = elementId.id % context->layoutElementsHashMap.capacity;
+    int32_t hashItemPrevious = -1;
+    int32_t hashItemIndex = context->layoutElementsHashMap.internalArray[hashBucket];
+    while (hashItemIndex != -1) { // Just replace collision, not a big deal - leave it up to the end user
+        Clay_LayoutElementHashMapItem *hashItem = Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, hashItemIndex);
+        if (hashItem->elementId.id == elementId.id) { // Collision - resolve based on generation
+            item.nextIndex = hashItem->nextIndex;
+            if (hashItem->generation <= context->generation) { // First collision - assume this is the "same" element
+                hashItem->appearedThisFrame = hashItem->generation < context->generation;
+                hashItem->elementId = elementId; // Make sure to copy this across. If the stringId reference has changed, we should update the hash item to use the new one.
+                hashItem->generation = context->generation + 1;
+                hashItem->layoutElement = layoutElement;
+                hashItem->debugData.collision = false;
+                hashItem->onHoverFunction = NULL;
+                hashItem->hoverFunctionUserData = 0;
+            } else { // Multiple collisions this frame - two elements have the same ID
+                context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+                    .errorType = CLAY_ERROR_TYPE_DUPLICATE_ID,
+                    .errorText = CLAY_STRING("An element with this ID was already previously declared during this layout."),
+                    .userData = context->errorHandler.userData });
+                if (context->debugModeEnabled) {
+                    hashItem->debugData.collision = true;
+                }
+            }
+            return hashItem;
+        }
+        hashItemPrevious = hashItemIndex;
+        hashItemIndex = hashItem->nextIndex;
+    }
+
+    int32_t indexToUse = 0;
+    if (context->layoutElementsHashMapFreeList.length > 0) {
+        indexToUse = Clay__int32_tArray_GetValue(&context->layoutElementsHashMapFreeList, context->layoutElementsHashMapFreeList.length - 1);
+        context->layoutElementsHashMapFreeList.length--;
+    } else {
+        indexToUse = context->layoutElementsHashMapInternal.length;
+    }
+    Clay_LayoutElementHashMapItem *hashItem = Clay__LayoutElementHashMapItemArray_Set(&context->layoutElementsHashMapInternal, indexToUse, item);
+    if (hashItemPrevious != -1) {
+        Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, hashItemPrevious)->nextIndex = (int32_t)indexToUse;
+    } else {
+        context->layoutElementsHashMap.internalArray[hashBucket] = (int32_t)indexToUse;
+    }
+    return hashItem;
+}
+
+Clay_LayoutElementHashMapItem *Clay__GetHashMapItem(uint32_t id) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    uint32_t hashBucket = id % context->layoutElementsHashMap.capacity;
+    int32_t elementIndex = context->layoutElementsHashMap.internalArray[hashBucket];
+    while (elementIndex != -1) {
+        Clay_LayoutElementHashMapItem *hashEntry = Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, elementIndex);
+        if (hashEntry->elementId.id == id) {
+            return hashEntry;
+        }
+        elementIndex = hashEntry->nextIndex;
+    }
+    return &Clay_LayoutElementHashMapItem_DEFAULT;
+}
+
+void Clay__UpdateAspectRatioBox(Clay_LayoutElement *layoutElement) {
+    if (layoutElement->config.aspectRatio.aspectRatio != 0) {
+        if (layoutElement->dimensions.width == 0 && layoutElement->dimensions.height != 0) {
+            layoutElement->dimensions.width = layoutElement->dimensions.height * layoutElement->config.aspectRatio.aspectRatio;
+        } else if (layoutElement->dimensions.width != 0 && layoutElement->dimensions.height == 0) {
+            layoutElement->dimensions.height = layoutElement->dimensions.width * (1 / layoutElement->config.aspectRatio.aspectRatio);
+        }
+    }
+}
+
+void Clay__CloseElement(void) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    if (context->booleanWarnings.maxElementsExceeded) {
+        return;
+    }
+    Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement();
+    Clay_LayoutConfig *layoutConfig = &openLayoutElement->config.layout;
+    bool elementHasClipHorizontal = openLayoutElement->config.clip.horizontal;
+    bool elementHasClipVertical = openLayoutElement->config.clip.vertical;
+    if (elementHasClipHorizontal || elementHasClipVertical || openLayoutElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) {
+        context->openClipElementStack.length--;
+    }
+
+    float leftRightPadding = (float)(layoutConfig->padding.left + layoutConfig->padding.right);
+    float topBottomPadding = (float)(layoutConfig->padding.top + layoutConfig->padding.bottom);
+
+    // Attach children to the current open element
+    openLayoutElement->children.elements = &context->layoutElementChildren.internalArray[context->layoutElementChildren.length];
+    if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
+        openLayoutElement->dimensions.width = leftRightPadding;
+        openLayoutElement->minDimensions.width = leftRightPadding;
+        for (int32_t i = 0; i < openLayoutElement->children.length; i++) {
+            int32_t childIndex = Clay__int32_tArray_GetValue(&context->layoutElementChildrenBuffer, (int)context->layoutElementChildrenBuffer.length - openLayoutElement->children.length + i);
+            Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, childIndex);
+            openLayoutElement->dimensions.width += child->dimensions.width;
+            openLayoutElement->dimensions.height = CLAY__MAX(openLayoutElement->dimensions.height, child->dimensions.height + topBottomPadding);
+            // Minimum size of child elements doesn't matter to clip containers as they can shrink and hide their contents
+            if (!elementHasClipHorizontal) {
+                openLayoutElement->minDimensions.width += child->minDimensions.width;
+            }
+            if (!elementHasClipVertical) {
+                openLayoutElement->minDimensions.height = CLAY__MAX(openLayoutElement->minDimensions.height, child->minDimensions.height + topBottomPadding);
+            }
+            Clay__int32_tArray_Add(&context->layoutElementChildren, childIndex);
+        }
+        float childGap = (float)(CLAY__MAX(openLayoutElement->children.length - 1, 0) * layoutConfig->childGap);
+        openLayoutElement->dimensions.width += childGap;
+        if (!elementHasClipHorizontal) {
+            openLayoutElement->minDimensions.width += childGap;
+        }
+    }
+    else if (layoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM) {
+        openLayoutElement->dimensions.height = topBottomPadding;
+        openLayoutElement->minDimensions.height = topBottomPadding;
+        for (int32_t i = 0; i < openLayoutElement->children.length; i++) {
+            int32_t childIndex = Clay__int32_tArray_GetValue(&context->layoutElementChildrenBuffer, (int)context->layoutElementChildrenBuffer.length - openLayoutElement->children.length + i);
+            Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, childIndex);
+            openLayoutElement->dimensions.height += child->dimensions.height;
+            openLayoutElement->dimensions.width = CLAY__MAX(openLayoutElement->dimensions.width, child->dimensions.width + leftRightPadding);
+            // Minimum size of child elements doesn't matter to clip containers as they can shrink and hide their contents
+            if (!elementHasClipVertical) {
+                openLayoutElement->minDimensions.height += child->minDimensions.height;
+            }
+            if (!elementHasClipHorizontal) {
+                openLayoutElement->minDimensions.width = CLAY__MAX(openLayoutElement->minDimensions.width, child->minDimensions.width + leftRightPadding);
+            }
+            Clay__int32_tArray_Add(&context->layoutElementChildren, childIndex);
+        }
+        float childGap = (float)(CLAY__MAX(openLayoutElement->children.length - 1, 0) * layoutConfig->childGap);
+        openLayoutElement->dimensions.height += childGap;
+        if (!elementHasClipVertical) {
+            openLayoutElement->minDimensions.height += childGap;
+        }
+    }
+
+    context->layoutElementChildrenBuffer.length -= openLayoutElement->children.length;
+
+    // Clamp element min and max width to the values configured in the layout
+    if (layoutConfig->sizing.width.type != CLAY__SIZING_TYPE_PERCENT) {
+        if (layoutConfig->sizing.width.size.minMax.max <= 0) { // Set the max size if the user didn't specify, makes calculations easier
+            layoutConfig->sizing.width.size.minMax.max = CLAY__MAXFLOAT;
+        }
+        openLayoutElement->dimensions.width = CLAY__MIN(CLAY__MAX(openLayoutElement->dimensions.width, layoutConfig->sizing.width.size.minMax.min), layoutConfig->sizing.width.size.minMax.max);
+        openLayoutElement->minDimensions.width = CLAY__MIN(CLAY__MAX(openLayoutElement->minDimensions.width, layoutConfig->sizing.width.size.minMax.min), layoutConfig->sizing.width.size.minMax.max);
+    } else {
+        openLayoutElement->dimensions.width = 0;
+    }
+
+    // Clamp element min and max height to the values configured in the layout
+    if (layoutConfig->sizing.height.type != CLAY__SIZING_TYPE_PERCENT) {
+        if (layoutConfig->sizing.height.size.minMax.max <= 0) { // Set the max size if the user didn't specify, makes calculations easier
+            layoutConfig->sizing.height.size.minMax.max = CLAY__MAXFLOAT;
+        }
+        openLayoutElement->dimensions.height = CLAY__MIN(CLAY__MAX(openLayoutElement->dimensions.height, layoutConfig->sizing.height.size.minMax.min), layoutConfig->sizing.height.size.minMax.max);
+        openLayoutElement->minDimensions.height = CLAY__MIN(CLAY__MAX(openLayoutElement->minDimensions.height, layoutConfig->sizing.height.size.minMax.min), layoutConfig->sizing.height.size.minMax.max);
+    } else {
+        openLayoutElement->dimensions.height = 0;
+    }
+
+    Clay__UpdateAspectRatioBox(openLayoutElement);
+
+    bool elementIsFloating = openLayoutElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE;
+
+    // Close the currently open element
+    int32_t closingElementIndex = Clay__int32_tArray_RemoveSwapback(&context->openLayoutElementStack, (int)context->openLayoutElementStack.length - 1);
+
+    // Get the currently open parent
+    openLayoutElement = Clay__GetOpenLayoutElement();
+
+    if (context->openLayoutElementStack.length > 1) {
+        if(elementIsFloating) {
+            openLayoutElement->floatingChildrenCount++;
+            return;
+        }
+        openLayoutElement->children.length++;
+        Clay__int32_tArray_Add(&context->layoutElementChildrenBuffer, closingElementIndex);
+    }
+}
+
+bool Clay__MemCmp(const char *s1, const char *s2, int32_t length);
+#if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64))
+    bool Clay__MemCmp(const char *s1, const char *s2, int32_t length) {
+        while (length >= 16) {
+            __m128i v1 = _mm_loadu_si128((const __m128i *)s1);
+            __m128i v2 = _mm_loadu_si128((const __m128i *)s2);
+
+            if (_mm_movemask_epi8(_mm_cmpeq_epi8(v1, v2)) != 0xFFFF) { // If any byte differs
+                return false;
+            }
+
+            s1 += 16;
+            s2 += 16;
+            length -= 16;
+        }
+
+        // Handle remaining bytes
+        while (length--) {
+            if (*s1 != *s2) {
+                return false;
+            }
+            s1++;
+            s2++;
+        }
+
+        return true;
+    }
+#elif !defined(CLAY_DISABLE_SIMD) && defined(__aarch64__)
+    bool Clay__MemCmp(const char *s1, const char *s2, int32_t length) {
+        while (length >= 16) {
+            uint8x16_t v1 = vld1q_u8((const uint8_t *)s1);
+            uint8x16_t v2 = vld1q_u8((const uint8_t *)s2);
+
+            // Compare vectors
+            if (vminvq_u32(vreinterpretq_u32_u8(vceqq_u8(v1, v2))) != 0xFFFFFFFF) { // If there's a difference
+                return false;
+            }
+
+            s1 += 16;
+            s2 += 16;
+            length -= 16;
+        }
+
+        // Handle remaining bytes
+        while (length--) {
+            if (*s1 != *s2) {
+                return false;
+            }
+            s1++;
+            s2++;
+        }
+
+        return true;
+    }
+#else
+    bool Clay__MemCmp(const char *s1, const char *s2, int32_t length) {
+        for (int32_t i = 0; i < length; i++) {
+            if (s1[i] != s2[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+#endif
+
+void Clay__OpenElement(void) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    if (context->layoutElements.length == context->layoutElements.capacity - 1 || context->booleanWarnings.maxElementsExceeded) {
+        context->booleanWarnings.maxElementsExceeded = true;
+        return;
+    }
+    Clay_LayoutElement layoutElement = CLAY__DEFAULT_STRUCT;
+    Clay_LayoutElement* openLayoutElement = Clay_LayoutElementArray_Add(&context->layoutElements, layoutElement);
+    Clay__int32_tArray_Add(&context->openLayoutElementStack, context->layoutElements.length - 1);
+    // Generate an ID
+    Clay_LayoutElement *parentElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 2));
+    uint32_t offset = parentElement->children.length + parentElement->floatingChildrenCount;
+    Clay_ElementId elementId = Clay__HashNumber(offset, parentElement->id);
+    openLayoutElement->id = elementId.id;
+    Clay__AddHashMapItem(elementId, openLayoutElement);
+    Clay__StringArray_Add(&context->layoutElementIdStrings, elementId.stringId);
+    if (context->openClipElementStack.length > 0) {
+        Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, Clay__int32_tArray_GetValue(&context->openClipElementStack, (int)context->openClipElementStack.length - 1));
+    } else {
+        Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, 0);
+    }
+}
+
+void Clay__OpenElementWithId(Clay_ElementId elementId) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    if (context->layoutElements.length == context->layoutElements.capacity - 1 || context->booleanWarnings.maxElementsExceeded) {
+        context->booleanWarnings.maxElementsExceeded = true;
+        return;
+    }
+    Clay_LayoutElement layoutElement = CLAY__DEFAULT_STRUCT;
+    layoutElement.id = elementId.id;
+    Clay_LayoutElement * openLayoutElement = Clay_LayoutElementArray_Add(&context->layoutElements, layoutElement);
+    Clay__int32_tArray_Add(&context->openLayoutElementStack, context->layoutElements.length - 1);
+    Clay__AddHashMapItem(elementId, openLayoutElement);
+    Clay__StringArray_Add(&context->layoutElementIdStrings, elementId.stringId);
+    if (context->openClipElementStack.length > 0) {
+        Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, Clay__int32_tArray_GetValue(&context->openClipElementStack, (int)context->openClipElementStack.length - 1));
+    } else {
+        Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, 0);
+    }
+}
+
+void Clay__OpenTextElement(Clay_String text, Clay_TextElementConfig textConfig) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    if (context->layoutElements.length == context->layoutElements.capacity - 1 || context->booleanWarnings.maxElementsExceeded) {
+        context->booleanWarnings.maxElementsExceeded = true;
+        return;
+    }
+    Clay_LayoutElement *parentElement = Clay__GetOpenLayoutElement();
+
+    Clay_LayoutElement layoutElement = { .textConfig = textConfig, .isTextElement = true };
+    Clay_LayoutElement *textElement = Clay_LayoutElementArray_Add(&context->layoutElements, layoutElement);
+    if (context->openClipElementStack.length > 0) {
+        Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, Clay__int32_tArray_GetValue(&context->openClipElementStack, (int)context->openClipElementStack.length - 1));
+    } else {
+        Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, 0);
+    }
+
+    Clay__int32_tArray_Add(&context->layoutElementChildrenBuffer, context->layoutElements.length - 1);
+    Clay__MeasureTextCacheItem *textMeasured = Clay__MeasureTextCached(&text, &textConfig);
+    Clay_ElementId elementId = Clay__HashNumber(parentElement->children.length + parentElement->floatingChildrenCount, parentElement->id);
+    textElement->id = elementId.id;
+    Clay__AddHashMapItem(elementId, textElement);
+    Clay__StringArray_Add(&context->layoutElementIdStrings, elementId.stringId);
+    Clay_Dimensions textDimensions = { .width = textMeasured->unwrappedDimensions.width, .height = textConfig.lineHeight > 0 ? (float)textConfig.lineHeight : textMeasured->unwrappedDimensions.height };
+    textElement->dimensions = textDimensions;
+    textElement->minDimensions = CLAY__INIT(Clay_Dimensions) { .width = textMeasured->minWidth, .height = textDimensions.height };
+    textElement->textElementData = CLAY__INIT(Clay__TextElementData) { .text = text, .preferredDimensions = textMeasured->unwrappedDimensions };
+    parentElement->children.length++;
+}
+
+void Clay__ConfigureOpenElementPtr(const Clay_ElementDeclaration *declaration) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement();
+    openLayoutElement->config = *declaration;
+    if ((declaration->layout.sizing.width.type == CLAY__SIZING_TYPE_PERCENT && declaration->layout.sizing.width.size.percent > 1) || (declaration->layout.sizing.height.type == CLAY__SIZING_TYPE_PERCENT && declaration->layout.sizing.height.size.percent > 1)) {
+        context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+                .errorType = CLAY_ERROR_TYPE_PERCENTAGE_OVER_1,
+                .errorText = CLAY_STRING("An element was configured with CLAY_SIZING_PERCENT, but the provided percentage value was over 1.0. Clay expects a value between 0 and 1, i.e. 20% is 0.2."),
+                .userData = context->errorHandler.userData });
+    }
+
+    if (declaration->floating.attachTo != CLAY_ATTACH_TO_NONE) {
+        Clay_FloatingElementConfig* floatingConfig = &openLayoutElement->config.floating;
+        // This looks dodgy but because of the auto generated root element the depth of the tree will always be at least 2 here
+        Clay_LayoutElement *hierarchicalParent = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 2));
+        if (hierarchicalParent) {
+            uint32_t clipElementId = 0;
+            if (declaration->floating.attachTo == CLAY_ATTACH_TO_PARENT) {
+                // Attach to the element's direct hierarchical parent
+                floatingConfig->parentId = hierarchicalParent->id;
+                if (context->openClipElementStack.length > 0) {
+                    clipElementId = Clay__int32_tArray_GetValue(&context->openClipElementStack, (int)context->openClipElementStack.length - 1);
+                }
+            } else if (declaration->floating.attachTo == CLAY_ATTACH_TO_ELEMENT_WITH_ID) {
+                Clay_LayoutElementHashMapItem *parentItem = Clay__GetHashMapItem(floatingConfig->parentId);
+                if (parentItem == &Clay_LayoutElementHashMapItem_DEFAULT) {
+                    context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+                            .errorType = CLAY_ERROR_TYPE_FLOATING_CONTAINER_PARENT_NOT_FOUND,
+                            .errorText = CLAY_STRING("A floating element was declared with a parentId, but no element with that ID was found."),
+                            .userData = context->errorHandler.userData });
+                } else {
+                    clipElementId = Clay__int32_tArray_GetValue(&context->layoutElementClipElementIds, (int32_t)(parentItem->layoutElement - context->layoutElements.internalArray));
+                }
+            } else if (declaration->floating.attachTo == CLAY_ATTACH_TO_ROOT) {
+                floatingConfig->parentId = Clay__HashString(CLAY_STRING("Clay__RootContainer"), 0).id;
+            }
+            if (declaration->floating.clipTo == CLAY_CLIP_TO_NONE) {
+                clipElementId = 0;
+            }
+            int32_t currentElementIndex = Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 1);
+            Clay__int32_tArray_Set(&context->layoutElementClipElementIds, currentElementIndex, clipElementId);
+            Clay__int32_tArray_Add(&context->openClipElementStack, clipElementId);
+            Clay__LayoutElementTreeRootArray_Add(&context->layoutElementTreeRoots, CLAY__INIT(Clay__LayoutElementTreeRoot) {
+                .layoutElementIndex = Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 1),
+                .parentId = floatingConfig->parentId,
+                .clipElementId = clipElementId,
+                .zIndex = floatingConfig->zIndex,
+            });
+        }
+    }
+
+    if (declaration->clip.horizontal || declaration->clip.vertical) {
+        Clay__int32_tArray_Add(&context->openClipElementStack, (int)openLayoutElement->id);
+        // Retrieve or create cached data to track scroll position across frames
+        Clay__ScrollContainerDataInternal *scrollOffset = CLAY__NULL;
+        for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) {
+            Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i);
+            if (openLayoutElement->id == mapping->elementId) {
+                scrollOffset = mapping;
+                scrollOffset->layoutElement = openLayoutElement;
+                scrollOffset->openThisFrame = true;
+            }
+        }
+        if (!scrollOffset) {
+            scrollOffset = Clay__ScrollContainerDataInternalArray_Add(&context->scrollContainerDatas, CLAY__INIT(Clay__ScrollContainerDataInternal){.layoutElement = openLayoutElement, .scrollOrigin = {-1,-1}, .elementId = openLayoutElement->id, .openThisFrame = true});
+        }
+        if (context->externalScrollHandlingEnabled) {
+            scrollOffset->scrollPosition = Clay__QueryScrollOffset(scrollOffset->elementId, context->queryScrollOffsetUserData);
+        }
+    }
+    // Setup data to track transitions across frames
+    if (declaration->transition.handler) {
+        Clay__TransitionDataInternal *transitionData = CLAY__NULL;
+        Clay_LayoutElement* parentElement = Clay__GetParentElement();
+        for (int32_t i = 0; i < context->transitionDatas.length; i++) {
+            Clay__TransitionDataInternal *existingData = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i);
+            if (openLayoutElement->id == existingData->elementId) {
+                if (existingData->state == CLAY_TRANSITION_STATE_EXITING) {
+                    existingData->state = CLAY_TRANSITION_STATE_IDLE;
+                    Clay_LayoutElementHashMapItem* hashMapItem = Clay__GetHashMapItem(openLayoutElement->id);
+                    hashMapItem->appearedThisFrame = false;
+                }
+                transitionData = existingData;
+                transitionData->elementThisFrame = openLayoutElement;
+                if (transitionData->parentId != parentElement->id) {
+                    transitionData->reparented = true;
+                }
+                transitionData->parentId = parentElement->id;
+                transitionData->siblingIndex = parentElement->children.length;
+                transitionData->transitionOut = !!declaration->transition.exit.setFinalState;
+            }
+        }
+        if (!transitionData) {
+            transitionData = Clay__TransitionDataInternalArray_Add(&context->transitionDatas, CLAY__INIT(Clay__TransitionDataInternal){
+                .elementThisFrame = openLayoutElement,
+                .elementId = openLayoutElement->id,
+                .parentId = parentElement->id,
+                .siblingIndex = parentElement->children.length,
+                .transitionOut = !!declaration->transition.exit.setFinalState
+            });
+        }
+    }
+}
+
+void Clay__ConfigureOpenElement(const Clay_ElementDeclaration declaration) {
+    Clay__ConfigureOpenElementPtr(&declaration);
+}
+
+void Clay__InitializeEphemeralMemory(Clay_Context* context) {
+    int32_t maxElementCount = context->maxElementCount;
+    // Ephemeral Memory - reset every frame
+    Clay_Arena *arena = &context->internalArena;
+    arena->nextAllocation = context->arenaResetOffset;
+
+    context->layoutElementChildrenBuffer = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+    context->layoutElements = Clay_LayoutElementArray_Allocate_Arena(maxElementCount, arena);
+    context->warnings = Clay__WarningArray_Allocate_Arena(100, arena);
+
+    context->layoutElementIdStrings = Clay__StringArray_Allocate_Arena(maxElementCount, arena);
+    context->wrappedTextLines = Clay__WrappedTextLineArray_Allocate_Arena(maxElementCount, arena);
+    context->layoutElementTreeNodeArray1 = Clay__LayoutElementTreeNodeArray_Allocate_Arena(maxElementCount, arena);
+    context->layoutElementTreeRoots = Clay__LayoutElementTreeRootArray_Allocate_Arena(maxElementCount, arena);
+    context->layoutElementChildren = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+    context->openLayoutElementStack = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+    context->renderCommands = Clay_RenderCommandArray_Allocate_Arena(maxElementCount, arena);
+    context->treeNodeVisited = Clay__boolArray_Allocate_Arena(maxElementCount, arena);
+    context->treeNodeVisited.length = context->treeNodeVisited.capacity; // This array is accessed directly rather than behaving as a list
+    context->openClipElementStack = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+    context->reusableElementIndexBuffer = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+    context->layoutElementClipElementIds = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+    context->dynamicStringData = Clay__charArray_Allocate_Arena(maxElementCount, arena);
+}
+
+void Clay__InitializePersistentMemory(Clay_Context* context) {
+    // Persistent memory - initialized once and not reset
+    int32_t maxElementCount = context->maxElementCount;
+    int32_t maxMeasureTextCacheWordCount = context->maxMeasureTextCacheWordCount;
+    Clay_Arena *arena = &context->internalArena;
+
+    context->scrollContainerDatas = Clay__ScrollContainerDataInternalArray_Allocate_Arena(100, arena);
+    context->transitionDatas = Clay__TransitionDataInternalArray_Allocate_Arena(200, arena);
+    context->layoutElementsHashMapInternal = Clay__LayoutElementHashMapItemArray_Allocate_Arena(maxElementCount, arena);
+    context->layoutElementsHashMap = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+    context->layoutElementsHashMapFreeList = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+    context->measureTextHashMapInternal = Clay__MeasureTextCacheItemArray_Allocate_Arena(maxElementCount, arena);
+    context->measureTextHashMapInternalFreeList = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+    context->measuredWordsFreeList = Clay__int32_tArray_Allocate_Arena(maxMeasureTextCacheWordCount, arena);
+    context->measureTextHashMap = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+    context->measuredWords = Clay__MeasuredWordArray_Allocate_Arena(maxMeasureTextCacheWordCount, arena);
+    context->pointerOverIds = Clay_ElementIdArray_Allocate_Arena(maxElementCount, arena);
+    context->arenaResetOffset = arena->nextAllocation;
+}
+
+const float CLAY__EPSILON = 0.01;
+
+bool Clay__FloatEqual(float left, float right) {
+    float subtracted = left - right;
+    return subtracted < CLAY__EPSILON && subtracted > -CLAY__EPSILON;
+}
+
+Clay_SizingAxis Clay__GetElementSizing(Clay_LayoutElement* element, bool xAxis) {
+    if (element->isTextElement) {
+        return CLAY__INIT(Clay_SizingAxis) {};
+    } else {
+        return xAxis ? element->config.layout.sizing.width : element->config.layout.sizing.height;
+    }
+}
+
+// Writes out the location of text elements to layout elements buffer 1
+void Clay__SizeContainersAlongAxis(bool xAxis, float deltaTime, Clay__int32_tArray* textElementsOut, Clay__int32_tArray* aspectRatioElementsOut) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    Clay__int32_tArray bfsBuffer = context->layoutElementChildrenBuffer;
+    Clay__int32_tArray resizableContainerBuffer = context->openLayoutElementStack;
+    for (int32_t rootIndex = 0; rootIndex < context->layoutElementTreeRoots.length; ++rootIndex) {
+        bfsBuffer.length = 0;
+        Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, rootIndex);
+        Clay_LayoutElement *rootElement = Clay_LayoutElementArray_Get(&context->layoutElements, (int)root->layoutElementIndex);
+        Clay__int32_tArray_Add(&bfsBuffer, (int32_t)root->layoutElementIndex);
+
+        // Size floating containers to their parents
+        if (rootElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) {
+            Clay_FloatingElementConfig *floatingElementConfig = &rootElement->config.floating;
+            Clay_LayoutElementHashMapItem *parentItem = Clay__GetHashMapItem(floatingElementConfig->parentId);
+            if (parentItem && parentItem != &Clay_LayoutElementHashMapItem_DEFAULT) {
+                Clay_LayoutElement *parentLayoutElement = parentItem->layoutElement;
+                switch (rootElement->config.layout.sizing.width.type) {
+                    case CLAY__SIZING_TYPE_GROW: {
+                        rootElement->dimensions.width = parentLayoutElement->dimensions.width;
+                        break;
+                    }
+                    case CLAY__SIZING_TYPE_PERCENT: {
+                        rootElement->dimensions.width = parentLayoutElement->dimensions.width * rootElement->config.layout.sizing.width.size.percent;
+                        break;
+                    }
+                    default: break;
+                }
+                switch (rootElement->config.layout.sizing.height.type) {
+                    case CLAY__SIZING_TYPE_GROW: {
+                        rootElement->dimensions.height = parentLayoutElement->dimensions.height;
+                        break;
+                    }
+                    case CLAY__SIZING_TYPE_PERCENT: {
+                        rootElement->dimensions.height = parentLayoutElement->dimensions.height * rootElement->config.layout.sizing.height.size.percent;
+                        break;
+                    }
+                    default: break;
+                }
+            }
+        }
+
+        if (rootElement->config.layout.sizing.width.type != CLAY__SIZING_TYPE_PERCENT) {
+            rootElement->dimensions.width = CLAY__MIN(CLAY__MAX(rootElement->dimensions.width, rootElement->config.layout.sizing.width.size.minMax.min), rootElement->config.layout.sizing.width.size.minMax.max);
+        }
+        if (rootElement->config.layout.sizing.height.type != CLAY__SIZING_TYPE_PERCENT) {
+            rootElement->dimensions.height = CLAY__MIN(CLAY__MAX(rootElement->dimensions.height, rootElement->config.layout.sizing.height.size.minMax.min), rootElement->config.layout.sizing.height.size.minMax.max);
+        }
+
+
+        for (int32_t i = 0; i < bfsBuffer.length; ++i) {
+            int32_t parentIndex = Clay__int32_tArray_GetValue(&bfsBuffer, i);
+            Clay_LayoutElement *parent = Clay_LayoutElementArray_Get(&context->layoutElements, parentIndex);
+            Clay_LayoutConfig *parentLayoutConfig = &parent->config.layout;
+            int32_t growContainerCount = 0;
+            float parentSize = xAxis ? parent->dimensions.width : parent->dimensions.height;
+            float parentPadding = (float)(xAxis ? (parentLayoutConfig->padding.left + parentLayoutConfig->padding.right) : (parentLayoutConfig->padding.top + parentLayoutConfig->padding.bottom));
+            float innerContentSize = 0, totalPaddingAndChildGaps = parentPadding;
+            bool sizingAlongAxis = (xAxis && parentLayoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) || (!xAxis && parentLayoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM);
+            resizableContainerBuffer.length = 0;
+            float parentChildGap = parentLayoutConfig->childGap;
+            bool isFirstChild = true;
+
+            for (int32_t childOffset = 0; childOffset < parent->children.length; childOffset++) {
+                int32_t childElementIndex = parent->children.elements[childOffset];
+                Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, childElementIndex);
+                Clay_SizingAxis childSizing = Clay__GetElementSizing(childElement, xAxis);
+                float childSize = xAxis ? childElement->dimensions.width : childElement->dimensions.height;
+
+                if (textElementsOut && childElement->isTextElement) {
+                    Clay__int32_tArray_Add(textElementsOut, childElementIndex);
+                } else if (childElement->children.length > 0) {
+                    Clay__int32_tArray_Add(&bfsBuffer, childElementIndex);
+                }
+
+                if (!childElement->isTextElement && aspectRatioElementsOut && childElement->config.aspectRatio.aspectRatio != 0) {
+                    Clay__int32_tArray_Add(aspectRatioElementsOut, childElementIndex);
+                }
+
+                // Note: setting isFirstChild = false is skipped here
+                if (childElement->exiting) {
+                    continue;
+                }
+
+                if (childSizing.type != CLAY__SIZING_TYPE_PERCENT
+                    && childSizing.type != CLAY__SIZING_TYPE_FIXED
+                    && (!childElement->isTextElement || childElement->textConfig.wrapMode == CLAY_TEXT_WRAP_WORDS)
+//                    && (xAxis || !Clay__ElementHasConfig(childElement, CLAY__ELEMENT_CONFIG_TYPE_ASPECT))
+                ) {
+                    Clay__int32_tArray_Add(&resizableContainerBuffer, childElementIndex);
+                }
+
+                if (sizingAlongAxis) {
+                    innerContentSize += (childSizing.type == CLAY__SIZING_TYPE_PERCENT ? 0 : childSize);
+                    if (childSizing.type == CLAY__SIZING_TYPE_GROW) {
+                        growContainerCount++;
+                    }
+                    if (!isFirstChild) {
+                        innerContentSize += parentChildGap; // For children after index 0, the childAxisOffset is the gap from the previous child
+                        totalPaddingAndChildGaps += parentChildGap;
+                    }
+                } else {
+                    innerContentSize = CLAY__MAX(childSize, innerContentSize);
+                }
+                isFirstChild = false;
+            }
+
+            // Expand percentage containers to size
+            for (int32_t childOffset = 0; childOffset < parent->children.length; childOffset++) {
+                int32_t childElementIndex = parent->children.elements[childOffset];
+                Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, childElementIndex);
+                Clay_SizingAxis childSizing = Clay__GetElementSizing(childElement, xAxis);
+                float *childSize = xAxis ? &childElement->dimensions.width : &childElement->dimensions.height;
+                if (childSizing.type == CLAY__SIZING_TYPE_PERCENT) {
+                    *childSize = (parentSize - totalPaddingAndChildGaps) * childSizing.size.percent;
+                    if (sizingAlongAxis) {
+                        innerContentSize += *childSize;
+                    }
+                    Clay__UpdateAspectRatioBox(childElement);
+                }
+            }
+
+            if (sizingAlongAxis) {
+                float sizeToDistribute = parentSize - parentPadding - innerContentSize;
+                // The content is too large, compress the children as much as possible
+                if (sizeToDistribute < 0) {
+                    // If the parent clips content in this axis direction, don't compress children, just leave them alone
+                    if (((xAxis && parent->config.clip.horizontal) || (!xAxis && parent->config.clip.vertical))) {
+                        continue;
+                    }
+                    // Scrolling containers preferentially compress before others
+                    while (sizeToDistribute < -CLAY__EPSILON && resizableContainerBuffer.length > 0) {
+                        float largest = 0;
+                        float secondLargest = 0;
+                        float widthToAdd = sizeToDistribute;
+                        for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) {
+                            Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex));
+                            float childSize = xAxis ? child->dimensions.width : child->dimensions.height;
+                            if (Clay__FloatEqual(childSize, largest)) { continue; }
+                            if (childSize > largest) {
+                                secondLargest = largest;
+                                largest = childSize;
+                            }
+                            if (childSize < largest) {
+                                secondLargest = CLAY__MAX(secondLargest, childSize);
+                                widthToAdd = secondLargest - largest;
+                            }
+                        }
+
+                        widthToAdd = CLAY__MAX(widthToAdd, sizeToDistribute / resizableContainerBuffer.length);
+
+                        for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) {
+                            Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex));
+                            float *childSize = xAxis ? &child->dimensions.width : &child->dimensions.height;
+                            float minSize = xAxis ? child->minDimensions.width : child->minDimensions.height;
+                            float previousWidth = *childSize;
+                            if (Clay__FloatEqual(*childSize, largest)) {
+                                *childSize += widthToAdd;
+                                if (*childSize <= minSize) {
+                                    *childSize = minSize;
+                                    Clay__int32_tArray_RemoveSwapback(&resizableContainerBuffer, childIndex--);
+                                }
+                                sizeToDistribute -= (*childSize - previousWidth);
+                            }
+                        }
+                    }
+                // The content is too small, allow SIZING_GROW containers to expand
+                } else if (sizeToDistribute > 0 && growContainerCount > 0) {
+                    for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) {
+                        Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex));
+                        Clay__SizingType childSizing = Clay__GetElementSizing(child, xAxis).type;
+                        if (childSizing != CLAY__SIZING_TYPE_GROW) {
+                            Clay__int32_tArray_RemoveSwapback(&resizableContainerBuffer, childIndex--);
+                        }
+                    }
+                    while (sizeToDistribute > CLAY__EPSILON && resizableContainerBuffer.length > 0) {
+                        float smallest = CLAY__MAXFLOAT;
+                        float secondSmallest = CLAY__MAXFLOAT;
+                        float widthToAdd = sizeToDistribute;
+                        for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) {
+                            Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex));
+                            float childSize = xAxis ? child->dimensions.width : child->dimensions.height;
+                            if (Clay__FloatEqual(childSize, smallest)) { continue; }
+                            if (childSize < smallest) {
+                                secondSmallest = smallest;
+                                smallest = childSize;
+                            }
+                            if (childSize > smallest) {
+                                secondSmallest = CLAY__MIN(secondSmallest, childSize);
+                                widthToAdd = secondSmallest - smallest;
+                            }
+                        }
+
+                        widthToAdd = CLAY__MIN(widthToAdd, sizeToDistribute / resizableContainerBuffer.length);
+
+                        for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) {
+                            Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex));
+                            float *childSize = xAxis ? &child->dimensions.width : &child->dimensions.height;
+                            Clay_SizingAxis childSizing = Clay__GetElementSizing(child, xAxis);
+                            float maxSize = childSizing.size.minMax.max;
+                            float previousWidth = *childSize;
+                            if (Clay__FloatEqual(*childSize, smallest)) {
+                                *childSize += widthToAdd;
+                                if (*childSize >= maxSize) {
+                                    *childSize = maxSize;
+                                    Clay__int32_tArray_RemoveSwapback(&resizableContainerBuffer, childIndex--);
+                                }
+                                sizeToDistribute -= (*childSize - previousWidth);
+                            }
+                        }
+                    }
+                }
+            // Sizing along the non layout axis ("off axis")
+            } else {
+                for (int32_t childOffset = 0; childOffset < resizableContainerBuffer.length; childOffset++) {
+                    Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childOffset));
+                    Clay_SizingAxis childSizing = Clay__GetElementSizing(childElement, xAxis);
+                    float minSize = xAxis ? childElement->minDimensions.width : childElement->minDimensions.height;
+                    float *childSize = xAxis ? &childElement->dimensions.width : &childElement->dimensions.height;
+
+                    float maxSize = parentSize - parentPadding;
+                    // If we're laying out the children of a scroll panel, grow containers expand to the size of the inner content, not the outer container
+                    if (((xAxis && parent->config.clip.horizontal) || (!xAxis && parent->config.clip.vertical))) {
+                        maxSize = CLAY__MAX(maxSize, innerContentSize);
+                    }
+                    if (childSizing.type == CLAY__SIZING_TYPE_GROW) {
+                        *childSize = CLAY__MIN(maxSize, childSizing.size.minMax.max);
+                    }
+                    *childSize = CLAY__MAX(minSize, CLAY__MIN(*childSize, maxSize));
+                }
+            }
+        }
+    }
+}
+
+Clay_String Clay__IntToString(int32_t integer) {
+    if (integer == 0) {
+        return CLAY__INIT(Clay_String) { .length = 1, .chars = "0" };
+    }
+    Clay_Context* context = Clay_GetCurrentContext();
+    char *chars = (char *)(context->dynamicStringData.internalArray + context->dynamicStringData.length);
+    int32_t length = 0;
+    int32_t sign = integer;
+
+    if (integer < 0) {
+        integer = -integer;
+    }
+    while (integer > 0) {
+        chars[length++] = (char)(integer % 10 + '0');
+        integer /= 10;
+    }
+
+    if (sign < 0) {
+        chars[length++] = '-';
+    }
+
+    // Reverse the string to get the correct order
+    for (int32_t j = 0, k = length - 1; j < k; j++, k--) {
+        char temp = chars[j];
+        chars[j] = chars[k];
+        chars[k] = temp;
+    }
+    context->dynamicStringData.length += length;
+    return CLAY__INIT(Clay_String) { .length = length, .chars = chars };
+}
+
+void Clay__AddRenderCommand(Clay_RenderCommand renderCommand) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    if (context->renderCommands.length < context->renderCommands.capacity - 1) {
+        Clay_RenderCommandArray_Add(&context->renderCommands, renderCommand);
+    } else {
+        if (!context->booleanWarnings.maxRenderCommandsExceeded) {
+            context->booleanWarnings.maxRenderCommandsExceeded = true;
+            context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+                .errorType = CLAY_ERROR_TYPE_ELEMENTS_CAPACITY_EXCEEDED,
+                .errorText = CLAY_STRING("Clay ran out of capacity while attempting to create render commands. This is usually caused by a large amount of wrapping text elements while close to the max element capacity. Try using Clay_SetMaxElementCount() with a higher value."),
+                .userData = context->errorHandler.userData });
+        }
+    }
+}
+
+bool Clay__ElementIsOffscreen(Clay_BoundingBox *boundingBox) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    if (context->disableCulling) {
+        return false;
+    }
+
+    return (boundingBox->x > (float)context->layoutDimensions.width) ||
+           (boundingBox->y > (float)context->layoutDimensions.height) ||
+           (boundingBox->x + boundingBox->width < 0) ||
+           (boundingBox->y + boundingBox->height < 0);
+}
+
+void Clay__CalculateFinalLayout(float deltaTime, bool useStoredBoundingBoxes, bool generateRenderCommands) {
+    Clay_Context* context = Clay_GetCurrentContext();
+
+    // Calculate sizing along the X axis
+    Clay__int32_tArray textElements = context->openClipElementStack;
+    textElements.length = 0;
+    Clay__int32_tArray aspectRatioElements = context->reusableElementIndexBuffer;
+    aspectRatioElements.length = 0;
+    Clay__SizeContainersAlongAxis(true, deltaTime, &textElements, &aspectRatioElements);
+
+    // Wrap text
+    for (int32_t textElementIndex = 0; textElementIndex < textElements.length; ++textElementIndex) {
+        Clay_LayoutElement *element = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&textElements, textElementIndex));
+        Clay__TextElementData *textElementData = &element->textElementData;
+        textElementData->wrappedLines = CLAY__INIT(Clay__WrappedTextLineArraySlice) { .length = 0, .internalArray = &context->wrappedTextLines.internalArray[context->wrappedTextLines.length] };
+        Clay_LayoutElement *containerElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&textElements, textElementIndex));
+        Clay__MeasureTextCacheItem *measureTextCacheItem = Clay__MeasureTextCached(&textElementData->text, &containerElement->textConfig);
+        float lineWidth = 0;
+        float lineHeight = containerElement->textConfig.lineHeight > 0 ? (float)containerElement->textConfig.lineHeight : textElementData->preferredDimensions.height;
+        int32_t lineLengthChars = 0;
+        int32_t lineStartOffset = 0;
+        if (!measureTextCacheItem->containsNewlines && textElementData->preferredDimensions.width <= containerElement->dimensions.width) {
+            Clay__WrappedTextLineArray_Add(&context->wrappedTextLines, CLAY__INIT(Clay__WrappedTextLine) { containerElement->dimensions,  textElementData->text });
+            textElementData->wrappedLines.length++;
+            continue;
+        }
+        float spaceWidth = Clay__MeasureText(CLAY__INIT(Clay_StringSlice) { .length = 1, .chars = CLAY__SPACECHAR.chars, .baseChars = CLAY__SPACECHAR.chars }, &containerElement->textConfig, context->measureTextUserData).width;
+        int32_t wordIndex = measureTextCacheItem->measuredWordsStartIndex;
+        while (wordIndex != -1) {
+            if (context->wrappedTextLines.length > context->wrappedTextLines.capacity - 1) {
+                break;
+            }
+            Clay__MeasuredWord *measuredWord = Clay__MeasuredWordArray_Get(&context->measuredWords, wordIndex);
+            // Only word on the line is too large, just render it anyway
+            if (lineLengthChars == 0 && lineWidth + measuredWord->width > containerElement->dimensions.width) {
+                Clay__WrappedTextLineArray_Add(&context->wrappedTextLines, CLAY__INIT(Clay__WrappedTextLine) { { measuredWord->width, lineHeight }, { .length = measuredWord->length, .chars = &textElementData->text.chars[measuredWord->startOffset] } });
+                textElementData->wrappedLines.length++;
+                wordIndex = measuredWord->next;
+                lineStartOffset = measuredWord->startOffset + measuredWord->length;
+            }
+            // measuredWord->length == 0 means a newline character
+            else if (measuredWord->length == 0 || lineWidth + measuredWord->width > containerElement->dimensions.width) {
+                // Wrapped text lines list has overflowed, just render out the line
+                bool finalCharIsSpace = textElementData->text.chars[CLAY__MAX(lineStartOffset + lineLengthChars - 1, 0)] == ' ';
+                Clay__WrappedTextLineArray_Add(&context->wrappedTextLines, CLAY__INIT(Clay__WrappedTextLine) { { lineWidth + (finalCharIsSpace ? -spaceWidth : 0), lineHeight }, { .length = lineLengthChars + (finalCharIsSpace ? -1 : 0), .chars = &textElementData->text.chars[lineStartOffset] } });
+                textElementData->wrappedLines.length++;
+                if (lineLengthChars == 0 || measuredWord->length == 0) {
+                    wordIndex = measuredWord->next;
+                }
+                lineWidth = 0;
+                lineLengthChars = 0;
+                lineStartOffset = measuredWord->startOffset;
+            } else {
+                lineWidth += measuredWord->width + containerElement->textConfig.letterSpacing;
+                lineLengthChars += measuredWord->length;
+                wordIndex = measuredWord->next;
+            }
+        }
+        if (lineLengthChars > 0) {
+            Clay__WrappedTextLineArray_Add(&context->wrappedTextLines, CLAY__INIT(Clay__WrappedTextLine) { { lineWidth - containerElement->textConfig.letterSpacing, lineHeight }, {.length = lineLengthChars, .chars = &textElementData->text.chars[lineStartOffset] } });
+            textElementData->wrappedLines.length++;
+        }
+        containerElement->dimensions.height = lineHeight * (float)textElementData->wrappedLines.length;
+    }
+
+    // Scale vertical heights according to aspect ratio
+    for (int32_t i = 0; i < aspectRatioElements.length; ++i) {
+        Clay_LayoutElement* aspectElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&aspectRatioElements, i));
+        aspectElement->dimensions.height = (1 / aspectElement->config.aspectRatio.aspectRatio) * aspectElement->dimensions.width;
+        aspectElement->config.layout.sizing.height.size.minMax.max = aspectElement->dimensions.height;
+    }
+
+    // Propagate effect of text wrapping, aspect scaling etc. on height of parents
+    Clay__LayoutElementTreeNodeArray dfsBuffer = context->layoutElementTreeNodeArray1;
+    dfsBuffer.length = 0;
+    for (int32_t i = 0; i < context->layoutElementTreeRoots.length; ++i) {
+        Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, i);
+        context->treeNodeVisited.internalArray[dfsBuffer.length] = false;
+        Clay__LayoutElementTreeNodeArray_Add(&dfsBuffer, CLAY__INIT(Clay__LayoutElementTreeNode) { .layoutElement = Clay_LayoutElementArray_Get(&context->layoutElements, (int)root->layoutElementIndex) });
+    }
+    while (dfsBuffer.length > 0) {
+        Clay__LayoutElementTreeNode *currentElementTreeNode = Clay__LayoutElementTreeNodeArray_Get(&dfsBuffer, (int)dfsBuffer.length - 1);
+        Clay_LayoutElement *currentElement = currentElementTreeNode->layoutElement;
+        if (!context->treeNodeVisited.internalArray[dfsBuffer.length - 1]) {
+            context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = true;
+            // If the element has no children or is the container for a text element, don't bother inspecting it
+            if (currentElement->isTextElement || currentElement->children.length == 0) {
+                dfsBuffer.length--;
+                continue;
+            }
+            // Add the children to the DFS buffer (needs to be pushed in reverse so that stack traversal is in correct layout order)
+            for (int32_t i = 0; i < currentElement->children.length; i++) {
+                context->treeNodeVisited.internalArray[dfsBuffer.length] = false;
+                Clay__LayoutElementTreeNodeArray_Add(&dfsBuffer, CLAY__INIT(Clay__LayoutElementTreeNode) { .layoutElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]) });
+            }
+            continue;
+        }
+        dfsBuffer.length--;
+
+        // DFS node has been visited, this is on the way back up to the root
+        Clay_LayoutConfig *layoutConfig = &currentElement->config.layout;
+        if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
+            // Resize any parent containers that have grown in height along their non layout axis
+            for (int32_t j = 0; j < currentElement->children.length; ++j) {
+                Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[j]);
+                float childHeightWithPadding = CLAY__MAX(childElement->dimensions.height + layoutConfig->padding.top + layoutConfig->padding.bottom, currentElement->dimensions.height);
+                currentElement->dimensions.height = CLAY__MIN(CLAY__MAX(childHeightWithPadding, layoutConfig->sizing.height.size.minMax.min), layoutConfig->sizing.height.size.minMax.max);
+            }
+        } else if (layoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM) {
+            // Resizing along the layout axis
+            float contentHeight = (float)(layoutConfig->padding.top + layoutConfig->padding.bottom);
+            for (int32_t j = 0; j < currentElement->children.length; ++j) {
+                Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[j]);
+                contentHeight += childElement->dimensions.height;
+            }
+            contentHeight += (float)(CLAY__MAX(currentElement->children.length - 1, 0) * layoutConfig->childGap);
+            currentElement->dimensions.height = CLAY__MIN(CLAY__MAX(contentHeight, layoutConfig->sizing.height.size.minMax.min), layoutConfig->sizing.height.size.minMax.max);
+        }
+    }
+
+    // Calculate sizing along the Y axis
+    Clay__SizeContainersAlongAxis(false, deltaTime, NULL, NULL);
+
+    // Scale horizontal widths according to aspect ratio
+    for (int32_t i = 0; i < aspectRatioElements.length; ++i) {
+        Clay_LayoutElement* aspectElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&aspectRatioElements, i));
+        aspectElement->dimensions.width = aspectElement->config.aspectRatio.aspectRatio * aspectElement->dimensions.height;
+    }
+
+    // Sort tree roots by z-index
+    int32_t sortMax = context->layoutElementTreeRoots.length - 1;
+    while (sortMax > 0) { // todo dumb bubble sort
+        for (int32_t i = 0; i < sortMax; ++i) {
+            Clay__LayoutElementTreeRoot current = *Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, i);
+            Clay__LayoutElementTreeRoot next = *Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, i + 1);
+            if (next.zIndex < current.zIndex) {
+                Clay__LayoutElementTreeRootArray_Set(&context->layoutElementTreeRoots, i, next);
+                Clay__LayoutElementTreeRootArray_Set(&context->layoutElementTreeRoots, i + 1, current);
+            }
+        }
+        sortMax--;
+    }
+
+    // Calculate final positions and generate render commands
+    context->renderCommands.length = 0;
+    dfsBuffer.length = 0;
+
+    for (int32_t rootIndex = 0; rootIndex < context->layoutElementTreeRoots.length; ++rootIndex) {
+        dfsBuffer.length = 0;
+        Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, rootIndex);
+        Clay_LayoutElement *rootElement = Clay_LayoutElementArray_Get(&context->layoutElements, (int)root->layoutElementIndex);
+        Clay_Vector2 rootPosition = CLAY__DEFAULT_STRUCT;
+        Clay_LayoutElementHashMapItem *parentHashMapItem = Clay__GetHashMapItem(root->parentId);
+        // Position root floating containers
+        if (rootElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE && parentHashMapItem) {
+            Clay_FloatingElementConfig *config = &rootElement->config.floating;
+            Clay_Dimensions rootDimensions = rootElement->dimensions;
+            Clay_BoundingBox parentBoundingBox = parentHashMapItem->boundingBox;
+            // Set X position
+            Clay_Vector2 targetAttachPosition = CLAY__DEFAULT_STRUCT;
+            switch (config->attachPoints.parent) {
+                case CLAY_ATTACH_POINT_LEFT_TOP:
+                case CLAY_ATTACH_POINT_LEFT_CENTER:
+                case CLAY_ATTACH_POINT_LEFT_BOTTOM: targetAttachPosition.x = parentBoundingBox.x; break;
+                case CLAY_ATTACH_POINT_CENTER_TOP:
+                case CLAY_ATTACH_POINT_CENTER_CENTER:
+                case CLAY_ATTACH_POINT_CENTER_BOTTOM: targetAttachPosition.x = parentBoundingBox.x + (parentBoundingBox.width / 2); break;
+                case CLAY_ATTACH_POINT_RIGHT_TOP:
+                case CLAY_ATTACH_POINT_RIGHT_CENTER:
+                case CLAY_ATTACH_POINT_RIGHT_BOTTOM: targetAttachPosition.x = parentBoundingBox.x + parentBoundingBox.width; break;
+            }
+            switch (config->attachPoints.element) {
+                case CLAY_ATTACH_POINT_LEFT_TOP:
+                case CLAY_ATTACH_POINT_LEFT_CENTER:
+                case CLAY_ATTACH_POINT_LEFT_BOTTOM: break;
+                case CLAY_ATTACH_POINT_CENTER_TOP:
+                case CLAY_ATTACH_POINT_CENTER_CENTER:
+                case CLAY_ATTACH_POINT_CENTER_BOTTOM: targetAttachPosition.x -= (rootDimensions.width / 2); break;
+                case CLAY_ATTACH_POINT_RIGHT_TOP:
+                case CLAY_ATTACH_POINT_RIGHT_CENTER:
+                case CLAY_ATTACH_POINT_RIGHT_BOTTOM: targetAttachPosition.x -= rootDimensions.width; break;
+            }
+            switch (config->attachPoints.parent) { // I know I could merge the x and y switch statements, but this is easier to read
+                case CLAY_ATTACH_POINT_LEFT_TOP:
+                case CLAY_ATTACH_POINT_RIGHT_TOP:
+                case CLAY_ATTACH_POINT_CENTER_TOP: targetAttachPosition.y = parentBoundingBox.y; break;
+                case CLAY_ATTACH_POINT_LEFT_CENTER:
+                case CLAY_ATTACH_POINT_CENTER_CENTER:
+                case CLAY_ATTACH_POINT_RIGHT_CENTER: targetAttachPosition.y = parentBoundingBox.y + (parentBoundingBox.height / 2); break;
+                case CLAY_ATTACH_POINT_LEFT_BOTTOM:
+                case CLAY_ATTACH_POINT_CENTER_BOTTOM:
+                case CLAY_ATTACH_POINT_RIGHT_BOTTOM: targetAttachPosition.y = parentBoundingBox.y + parentBoundingBox.height; break;
+            }
+            switch (config->attachPoints.element) {
+                case CLAY_ATTACH_POINT_LEFT_TOP:
+                case CLAY_ATTACH_POINT_RIGHT_TOP:
+                case CLAY_ATTACH_POINT_CENTER_TOP: break;
+                case CLAY_ATTACH_POINT_LEFT_CENTER:
+                case CLAY_ATTACH_POINT_CENTER_CENTER:
+                case CLAY_ATTACH_POINT_RIGHT_CENTER: targetAttachPosition.y -= (rootDimensions.height / 2); break;
+                case CLAY_ATTACH_POINT_LEFT_BOTTOM:
+                case CLAY_ATTACH_POINT_CENTER_BOTTOM:
+                case CLAY_ATTACH_POINT_RIGHT_BOTTOM: targetAttachPosition.y -= rootDimensions.height; break;
+            }
+            targetAttachPosition.x += config->offset.x;
+            targetAttachPosition.y += config->offset.y;
+            rootPosition = targetAttachPosition;
+        }
+        if (root->clipElementId) {
+            Clay_LayoutElementHashMapItem *clipHashMapItem = Clay__GetHashMapItem(root->clipElementId);
+            if (clipHashMapItem && !Clay__ElementIsOffscreen(&clipHashMapItem->boundingBox)) {
+                // Floating elements that are attached to scrolling contents won't be correctly positioned if external scroll handling is enabled, fix here
+                if (context->externalScrollHandlingEnabled) {
+                    if (clipHashMapItem->layoutElement->config.clip.horizontal) {
+                        rootPosition.x += clipHashMapItem->layoutElement->config.clip.childOffset.x;
+                    }
+                    if (clipHashMapItem->layoutElement->config.clip.vertical) {
+                        rootPosition.y += clipHashMapItem->layoutElement->config.clip.childOffset.y;
+                    }
+                }
+                if (generateRenderCommands) {
+                    Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
+                        .boundingBox = clipHashMapItem->boundingBox,
+                        .userData = 0,
+                        .id = Clay__HashNumber(rootElement->id, rootElement->children.length + 10).id, // TODO need a better strategy for managing derived ids
+                        .zIndex = root->zIndex,
+                        .commandType = CLAY_RENDER_COMMAND_TYPE_SCISSOR_START,
+                    });
+                }
+            }
+        }
+        Clay__LayoutElementTreeNodeArray_Add(&dfsBuffer, CLAY__INIT(Clay__LayoutElementTreeNode) { .layoutElement = rootElement, .position = rootPosition, .nextChildOffset = { .x = (float)rootElement->config.layout.padding.left, .y = (float)rootElement->config.layout.padding.top } });
+
+        context->treeNodeVisited.internalArray[0] = false;
+        while (dfsBuffer.length > 0) {
+            Clay__LayoutElementTreeNode *currentElementTreeNode = Clay__LayoutElementTreeNodeArray_Get(&dfsBuffer, (int)dfsBuffer.length - 1);
+            Clay_LayoutElement *currentElement = currentElementTreeNode->layoutElement;
+            Clay_LayoutConfig *layoutConfig = currentElement->isTextElement ? &CLAY_LAYOUT_DEFAULT : &currentElement->config.layout;
+            Clay_Vector2 scrollOffset = CLAY__DEFAULT_STRUCT;
+
+            // DFS is returning back upwards
+            if (context->treeNodeVisited.internalArray[dfsBuffer.length - 1]) {
+                if (currentElement->isTextElement) {
+                    dfsBuffer.length--;
+                    continue;
+                }
+                Clay_LayoutElementHashMapItem *currentElementData = Clay__GetHashMapItem(currentElement->id);
+                if (generateRenderCommands && !Clay__ElementIsOffscreen(&currentElementData->boundingBox)) {
+                    // DFS is returning upwards backwards
+                    bool closeClipElement = false;
+                    if (currentElement->config.clip.horizontal || currentElement->config.clip.vertical) {
+                        closeClipElement = true;
+                        for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) {
+                            Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i);
+                            if (mapping->layoutElement == currentElement) {
+                                scrollOffset = currentElement->config.clip.childOffset;
+                                if (context->externalScrollHandlingEnabled) {
+                                    scrollOffset = CLAY__INIT(Clay_Vector2) CLAY__DEFAULT_STRUCT;
+                                }
+                                break;
+                            }
+                        }
+                    }
+
+                    if (Clay__BorderHasAnyWidth(&currentElement->config.border)) {
+                        Clay_BoundingBox currentElementBoundingBox = currentElementData->boundingBox;
+                        Clay_BorderElementConfig *borderConfig = &currentElement->config.border;
+                        Clay_RenderCommand renderCommand = {
+                            .boundingBox = currentElementBoundingBox,
+                            .renderData = { .border = {
+                                .color = borderConfig->color,
+                                .cornerRadius = currentElement->config.cornerRadius,
+                                .width = borderConfig->width
+                            }},
+                            .userData = currentElement->config.userData,
+                            .id = Clay__HashNumber(currentElement->id, currentElement->children.length).id,
+                            .commandType = CLAY_RENDER_COMMAND_TYPE_BORDER,
+                        };
+                        Clay__AddRenderCommand(renderCommand);
+                        if (borderConfig->width.betweenChildren > 0 && borderConfig->color.a > 0) {
+                            float halfGap = layoutConfig->childGap / 2;
+                            float halfWidth = borderConfig->width.betweenChildren / 2;
+                            Clay_Vector2 borderOffset = { (float)layoutConfig->padding.left - halfGap, (float)layoutConfig->padding.top - halfGap };
+                            if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
+                                for (int32_t i = 0; i < currentElement->children.length; ++i) {
+                                    Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]);
+                                    if (i > 0) {
+                                        Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
+                                                .boundingBox = { currentElementBoundingBox.x + borderOffset.x + scrollOffset.x - halfWidth, currentElementBoundingBox.y + scrollOffset.y, (float)borderConfig->width.betweenChildren, currentElement->dimensions.height },
+                                                .renderData = { .rectangle = {
+                                                        .backgroundColor = borderConfig->color,
+                                                } },
+                                                .userData = currentElement->config.userData,
+                                                .id = Clay__HashNumber(currentElement->id, currentElement->children.length + 1 + i).id,
+                                                .commandType = CLAY_RENDER_COMMAND_TYPE_RECTANGLE,
+                                        });
+                                    }
+                                    borderOffset.x += (childElement->dimensions.width + (float)layoutConfig->childGap);
+                                }
+                            } else {
+                                for (int32_t i = 0; i < currentElement->children.length; ++i) {
+                                    Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]);
+                                    if (i > 0) {
+                                        Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
+                                                .boundingBox = { currentElementBoundingBox.x + scrollOffset.x, currentElementBoundingBox.y + borderOffset.y + scrollOffset.y - halfWidth, currentElement->dimensions.width, (float)borderConfig->width.betweenChildren },
+                                                .renderData = { .rectangle = {
+                                                        .backgroundColor = borderConfig->color,
+                                                } },
+                                                .userData = currentElement->config.userData,
+                                                .id = Clay__HashNumber(currentElement->id, currentElement->children.length + 1 + i).id,
+                                                .commandType = CLAY_RENDER_COMMAND_TYPE_RECTANGLE,
+                                        });
+                                    }
+                                    borderOffset.y += (childElement->dimensions.height + (float)layoutConfig->childGap);
+                                }
+                            }
+                        }
+                    }
+                    if (currentElement->config.overlayColor.a > 0) {
+                        Clay_RenderCommand renderCommand = {
+                                .userData = currentElement->config.userData,
+                                .id = currentElement->id,
+                                .zIndex = root->zIndex,
+                                .commandType = CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_END,
+                        };
+                        Clay__AddRenderCommand(renderCommand);
+                    }
+                    // This exists because the scissor needs to end _after_ borders between elements
+                    if (closeClipElement) {
+                        Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
+                                .id = Clay__HashNumber(currentElement->id, rootElement->children.length + 11).id,
+                                .commandType = CLAY_RENDER_COMMAND_TYPE_SCISSOR_END,
+                        });
+                    }
+                }
+
+                dfsBuffer.length--;
+                continue;
+            }
+
+            // This will only be run a single time for each element in downwards DFS order
+            context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = true;
+            Clay_BoundingBox currentElementBoundingBox = { currentElementTreeNode->position.x, currentElementTreeNode->position.y, currentElement->dimensions.width, currentElement->dimensions.height };
+            Clay__ScrollContainerDataInternal *scrollContainerData = CLAY__NULL;
+            if (!currentElement->isTextElement) {
+                if (useStoredBoundingBoxes && currentElement->config.transition.handler) {
+                    bool found = false;
+                    for (int j = 0; j < context->transitionDatas.length; ++j) {
+                        Clay__TransitionDataInternal* transitionData = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, j);
+                        if (transitionData->elementId == currentElement->id) {
+                            found = true;
+                            if (transitionData->state != CLAY_TRANSITION_STATE_IDLE) {
+                                if ((transitionData->activeProperties & CLAY_TRANSITION_PROPERTY_X) != 0) currentElementBoundingBox.x = transitionData->currentState.boundingBox.x;
+                                if ((transitionData->activeProperties & CLAY_TRANSITION_PROPERTY_Y) != 0) currentElementBoundingBox.y = transitionData->currentState.boundingBox.y;
+                                if ((transitionData->activeProperties & CLAY_TRANSITION_PROPERTY_WIDTH) != 0) currentElementBoundingBox.width = transitionData->currentState.boundingBox.width;
+                                if ((transitionData->activeProperties & CLAY_TRANSITION_PROPERTY_HEIGHT) != 0) currentElementBoundingBox.height = transitionData->currentState.boundingBox.height;
+                            }
+                            break;
+                        }
+                    }
+                    // An exiting element that completed its transition this frame - skip tree
+                    if (!found && currentElement->config.transition.exit.setFinalState) {
+                        dfsBuffer.length--;
+                        continue;
+                    }
+                }
+                if (currentElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) {
+                    Clay_FloatingElementConfig *floatingElementConfig = &currentElement->config.floating;
+                    Clay_Dimensions expand = floatingElementConfig->expand;
+                    currentElementBoundingBox.x -= expand.width;
+                    currentElementBoundingBox.width += expand.width * 2;
+                    currentElementBoundingBox.y -= expand.height;
+                    currentElementBoundingBox.height += expand.height * 2;
+                }
+
+                // Apply scroll offsets to container
+                if (currentElement->config.clip.horizontal || currentElement->config.clip.vertical) {
+                    // This linear scan could theoretically be slow under very strange conditions, but I can't imagine a real UI with more than a few 10's of scroll containers
+                    for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) {
+                        Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i);
+                        if (mapping->layoutElement == currentElement) {
+                            scrollContainerData = mapping;
+                            mapping->boundingBox = currentElementBoundingBox;
+                            scrollOffset = currentElement->config.clip.childOffset;
+                            if (context->externalScrollHandlingEnabled) {
+                                scrollOffset = CLAY__INIT(Clay_Vector2) CLAY__DEFAULT_STRUCT;
+                            }
+                            break;
+                        }
+                    }
+                }
+            }
+
+            bool offscreen = Clay__ElementIsOffscreen(&currentElementBoundingBox);
+
+            // Generate render commands for current element
+            if (generateRenderCommands && !offscreen) {
+                if (currentElement->isTextElement) {
+                    Clay_TextElementConfig *textElementConfig = &currentElement->textConfig;
+                    float naturalLineHeight = currentElement->textElementData.preferredDimensions.height;
+                    float finalLineHeight = textElementConfig->lineHeight > 0 ? (float)textElementConfig->lineHeight : naturalLineHeight;
+                    float lineHeightOffset = (finalLineHeight - naturalLineHeight) / 2;
+                    float yPosition = lineHeightOffset;
+                    for (int32_t lineIndex = 0; lineIndex < currentElement->textElementData.wrappedLines.length; ++lineIndex) {
+                        Clay__WrappedTextLine *wrappedLine = Clay__WrappedTextLineArraySlice_Get(&currentElement->textElementData.wrappedLines, lineIndex);
+                        if (wrappedLine->line.length == 0) {
+                            yPosition += finalLineHeight;
+                            continue;
+                        }
+                        float offset = (currentElementBoundingBox.width - wrappedLine->dimensions.width);
+                        if (textElementConfig->textAlignment == CLAY_TEXT_ALIGN_LEFT) {
+                            offset = 0;
+                        }
+                        if (textElementConfig->textAlignment == CLAY_TEXT_ALIGN_CENTER) {
+                            offset /= 2;
+                        }
+                        Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
+                            .boundingBox = { currentElementBoundingBox.x + offset, currentElementBoundingBox.y + yPosition, wrappedLine->dimensions.width, wrappedLine->dimensions.height },
+                            .renderData = { .text = {
+                                .stringContents = CLAY__INIT(Clay_StringSlice) { .length = wrappedLine->line.length, .chars = wrappedLine->line.chars, .baseChars = currentElement->textElementData.text.chars },
+                                .textColor = textElementConfig->textColor,
+                                .fontId = textElementConfig->fontId,
+                                .fontSize = textElementConfig->fontSize,
+                                .letterSpacing = textElementConfig->letterSpacing,
+                                .lineHeight = textElementConfig->lineHeight,
+                            }},
+                            .userData = textElementConfig->userData,
+                            .id = Clay__HashNumber(lineIndex, currentElement->id).id,
+                            .zIndex = root->zIndex,
+                            .commandType = CLAY_RENDER_COMMAND_TYPE_TEXT,
+                        });
+                        yPosition += finalLineHeight;
+
+                        if (!context->disableCulling && (currentElementBoundingBox.y + yPosition > context->layoutDimensions.height)) {
+                            break;
+                        }
+                    }
+                } else {
+                    if (currentElement->config.overlayColor.a > 0) {
+                        Clay_RenderCommand renderCommand = {
+                            .renderData = {
+                                .overlayColor = { .color = currentElement->config.overlayColor }
+                            },
+                            .userData = currentElement->config.userData,
+                            .id = currentElement->id,
+                            .zIndex = root->zIndex,
+                            .commandType = CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START,
+                        };
+                        Clay__AddRenderCommand(renderCommand);
+                    }
+                    if (currentElement->config.image.imageData) {
+                        Clay_RenderCommand renderCommand = {
+                            .boundingBox = currentElementBoundingBox,
+                            .renderData = {
+                                .image = {
+                                    .backgroundColor = currentElement->config.backgroundColor,
+                                    .cornerRadius = currentElement->config.cornerRadius,
+                                    .imageData = currentElement->config.image.imageData,
+                                }
+                            },
+                            .userData = currentElement->config.userData,
+                            .id = currentElement->id,
+                            .zIndex = root->zIndex,
+                                .commandType = CLAY_RENDER_COMMAND_TYPE_IMAGE,
+                        };
+                        Clay__AddRenderCommand(renderCommand);
+                    }
+                    if (currentElement->config.custom.customData) {
+                        Clay_RenderCommand renderCommand = {
+                            .boundingBox = currentElementBoundingBox,
+                            .renderData = {
+                                .custom = {
+                                    .backgroundColor = currentElement->config.backgroundColor,
+                                    .cornerRadius = currentElement->config.cornerRadius,
+                                    .customData = currentElement->config.custom.customData,
+                                }
+                            },
+                            .userData = currentElement->config.userData,
+                            .id = currentElement->id,
+                            .zIndex = root->zIndex,
+                            .commandType = CLAY_RENDER_COMMAND_TYPE_CUSTOM,
+                        };
+                        Clay__AddRenderCommand(renderCommand);
+                    }
+                    if (currentElement->config.clip.horizontal || currentElement->config.clip.vertical) {
+                        Clay_RenderCommand renderCommand = {
+                            .boundingBox = currentElementBoundingBox,
+                            .renderData = {
+                                .clip = {
+                                    .horizontal = currentElement->config.clip.horizontal,
+                                    .vertical = currentElement->config.clip.vertical,
+                                }
+                            },
+                            .userData = currentElement->config.userData,
+                            .id = currentElement->id,
+                            .zIndex = root->zIndex,
+                            .commandType = CLAY_RENDER_COMMAND_TYPE_SCISSOR_START,
+                        };
+                        Clay__AddRenderCommand(renderCommand);
+                    }
+                    if (currentElement->config.backgroundColor.a > 0) {
+                        Clay_RenderCommand renderCommand = {
+                            .boundingBox = currentElementBoundingBox,
+                            .renderData = { .rectangle = {
+                                .backgroundColor = currentElement->config.backgroundColor,
+                                .cornerRadius = currentElement->config.cornerRadius,
+                            } },
+                            .userData = currentElement->config.userData,
+                            .id = currentElement->id,
+                            .zIndex = root->zIndex,
+                            .commandType = CLAY_RENDER_COMMAND_TYPE_RECTANGLE,
+                        };
+                        Clay__AddRenderCommand(renderCommand);
+                    }
+                }
+            }
+
+            Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(currentElement->id);
+            hashMapItem->boundingBox = currentElementBoundingBox;
+
+            if (currentElement->isTextElement) continue;
+
+            // Setup positions for child elements and add to DFS buffer ----------
+
+            // On-axis alignment
+            Clay_Dimensions contentSizeCurrent = {};
+            if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
+                for (int32_t i = 0; i < currentElement->children.length; ++i) {
+                    Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]);
+                    if (childElement->exiting) continue;
+                    contentSizeCurrent.width += childElement->dimensions.width;
+                    contentSizeCurrent.height = CLAY__MAX(contentSizeCurrent.height, childElement->dimensions.height);
+                }
+                contentSizeCurrent.width += (float)(CLAY__MAX(currentElement->children.length - 1, 0) * layoutConfig->childGap);
+                float extraSpace = currentElement->dimensions.width - (float)(layoutConfig->padding.left + layoutConfig->padding.right) - contentSizeCurrent.width;
+                switch (layoutConfig->childAlignment.x) {
+                    case CLAY_ALIGN_X_LEFT: extraSpace = 0; break;
+                    case CLAY_ALIGN_X_CENTER: extraSpace /= 2; break;
+                    default: break;
+                }
+                extraSpace = CLAY__MAX(0, extraSpace);
+                currentElementTreeNode->nextChildOffset.x += extraSpace;
+            } else if (layoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM) {
+                for (int32_t i = 0; i < currentElement->children.length; ++i) {
+                    Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]);
+                    if (childElement->exiting) continue;
+                    contentSizeCurrent.width = CLAY__MAX(contentSizeCurrent.width, childElement->dimensions.width);
+                    contentSizeCurrent.height += childElement->dimensions.height;
+                }
+                contentSizeCurrent.height += (float)(CLAY__MAX(currentElement->children.length - 1, 0) * layoutConfig->childGap);
+                float extraSpace = currentElement->dimensions.height - (float)(layoutConfig->padding.top + layoutConfig->padding.bottom) - contentSizeCurrent.height;
+                switch (layoutConfig->childAlignment.y) {
+                    case CLAY_ALIGN_Y_TOP: extraSpace = 0; break;
+                    case CLAY_ALIGN_Y_CENTER: extraSpace /= 2; break;
+                    default: break;
+                }
+                extraSpace = CLAY__MAX(0, extraSpace);
+                currentElementTreeNode->nextChildOffset.y += extraSpace;
+            }
+
+            if (scrollContainerData) {
+                scrollContainerData->contentSize = CLAY__INIT(Clay_Dimensions) {contentSizeCurrent.width + (float)(layoutConfig->padding.left + layoutConfig->padding.right), contentSizeCurrent.height + (float)(layoutConfig->padding.top + layoutConfig->padding.bottom) };
+            }
+
+            // Add children to the DFS buffer
+            dfsBuffer.length += currentElement->children.length;
+            for (int32_t i = 0; i < currentElement->children.length; ++i) {
+                Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]);
+                Clay_LayoutElementHashMapItem* childMapItem = Clay__GetHashMapItem(childElement->id);
+                // Alignment along non layout axis
+                if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
+                    currentElementTreeNode->nextChildOffset.y = currentElement->config.layout.padding.top;
+                    float whiteSpaceAroundChild = currentElement->dimensions.height - (float)(layoutConfig->padding.top + layoutConfig->padding.bottom) - childElement->dimensions.height;
+                    switch (layoutConfig->childAlignment.y) {
+                        case CLAY_ALIGN_Y_TOP: break;
+                        case CLAY_ALIGN_Y_CENTER: currentElementTreeNode->nextChildOffset.y += whiteSpaceAroundChild / 2; break;
+                        case CLAY_ALIGN_Y_BOTTOM: currentElementTreeNode->nextChildOffset.y += whiteSpaceAroundChild; break;
+                    }
+                } else {
+                    currentElementTreeNode->nextChildOffset.x = currentElement->config.layout.padding.left;
+                    float whiteSpaceAroundChild = currentElement->dimensions.width - (float)(layoutConfig->padding.left + layoutConfig->padding.right) - childElement->dimensions.width;
+                    switch (layoutConfig->childAlignment.x) {
+                        case CLAY_ALIGN_X_LEFT: break;
+                        case CLAY_ALIGN_X_CENTER: currentElementTreeNode->nextChildOffset.x += whiteSpaceAroundChild / 2; break;
+                        case CLAY_ALIGN_X_RIGHT: currentElementTreeNode->nextChildOffset.x += whiteSpaceAroundChild; break;
+                    }
+                }
+
+                Clay_Vector2 childPosition = {
+                    currentElementBoundingBox.x + currentElementTreeNode->nextChildOffset.x + scrollOffset.x,
+                    currentElementBoundingBox.y + currentElementTreeNode->nextChildOffset.y + scrollOffset.y,
+                };
+
+                // DFS buffer elements need to be added in reverse because stack traversal happens backwards
+                uint32_t newNodeIndex = dfsBuffer.length - 1 - i;
+                dfsBuffer.internalArray[newNodeIndex] = CLAY__INIT(Clay__LayoutElementTreeNode) {
+                    .layoutElement = childElement,
+                    .position = CLAY__INIT(Clay_Vector2) { childPosition.x, childPosition.y },
+                    .nextChildOffset = { .x = (float)childElement->config.layout.padding.left, .y = (float)childElement->config.layout.padding.top },
+                };
+                context->treeNodeVisited.internalArray[newNodeIndex] = false;
+
+                // Update parent offsets
+                if (!childElement->exiting) {
+                    if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
+                        currentElementTreeNode->nextChildOffset.x += childElement->dimensions.width + (float)layoutConfig->childGap;
+                    } else {
+                        currentElementTreeNode->nextChildOffset.y += childElement->dimensions.height + (float)layoutConfig->childGap;
+                    }
+                }
+            }
+        }
+
+        if (root->clipElementId) {
+            Clay_LayoutElementHashMapItem *clipHashMapItem = Clay__GetHashMapItem(root->clipElementId);
+            if (clipHashMapItem && !Clay__ElementIsOffscreen(&clipHashMapItem->boundingBox)) {
+                Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) { .id = Clay__HashNumber(rootElement->id, rootElement->children.length + 11).id, .commandType = CLAY_RENDER_COMMAND_TYPE_SCISSOR_END });
+            }
+        }
+    }
+}
+
+CLAY_WASM_EXPORT("Clay_GetPointerOverIds")
+CLAY_DLL_EXPORT Clay_ElementIdArray Clay_GetPointerOverIds(void) {
+    return Clay_GetCurrentContext()->pointerOverIds;
+}
+
+#pragma region DebugTools
+Clay_Color CLAY__DEBUGVIEW_COLOR_1 = {58, 56, 52, 255};
+Clay_Color CLAY__DEBUGVIEW_COLOR_2 = {62, 60, 58, 255};
+Clay_Color CLAY__DEBUGVIEW_COLOR_3 = {141, 133, 135, 255};
+Clay_Color CLAY__DEBUGVIEW_COLOR_4 = {238, 226, 231, 255};
+Clay_Color CLAY__DEBUGVIEW_COLOR_SELECTED_ROW = {102, 80, 78, 255};
+const int32_t CLAY__DEBUGVIEW_ROW_HEIGHT = 30;
+const int32_t CLAY__DEBUGVIEW_OUTER_PADDING = 10;
+const int32_t CLAY__DEBUGVIEW_INDENT_WIDTH = 16;
+Clay_TextElementConfig Clay__DebugView_TextNameConfig = {.textColor = {238, 226, 231, 255}, .fontSize = 16, .wrapMode = CLAY_TEXT_WRAP_NONE };
+Clay_LayoutConfig Clay__DebugView_ScrollViewItemLayoutConfig = CLAY__DEFAULT_STRUCT;
+
+typedef struct {
+    Clay_String label;
+    Clay_Color color;
+} Clay__DebugElementConfigTypeLabelConfig;
+
+typedef enum {
+    CLAY__ELEMENT_CONFIG_TYPE_BACKGROUND_COLOR,
+    CLAY__ELEMENT_CONFIG_TYPE_OVERLAY_COLOR,
+    CLAY__ELEMENT_CONFIG_TYPE_CORNER_RADIUS,
+    CLAY__ELEMENT_CONFIG_TYPE_TEXT,
+    CLAY__ELEMENT_CONFIG_TYPE_ASPECT,
+    CLAY__ELEMENT_CONFIG_TYPE_IMAGE,
+    CLAY__ELEMENT_CONFIG_TYPE_FLOATING,
+    CLAY__ELEMENT_CONFIG_TYPE_CLIP,
+    CLAY__ELEMENT_CONFIG_TYPE_BORDER,
+    CLAY__ELEMENT_CONFIG_TYPE_CUSTOM,
+} Clay__DebugElementConfigType;
+
+Clay__DebugElementConfigTypeLabelConfig Clay__DebugGetElementConfigTypeLabel(Clay__DebugElementConfigType type) {
+    switch (type) {
+        case CLAY__ELEMENT_CONFIG_TYPE_BACKGROUND_COLOR: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Background"), {243,134,48,255} };
+        case CLAY__ELEMENT_CONFIG_TYPE_OVERLAY_COLOR: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Overlay"), { 142,129,206, 255} };
+        case CLAY__ELEMENT_CONFIG_TYPE_CORNER_RADIUS: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) {CLAY_STRING("Radius"), {239,148,157, 255 } };
+        case CLAY__ELEMENT_CONFIG_TYPE_TEXT: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Text"), {105,210,231,255} };
+        case CLAY__ELEMENT_CONFIG_TYPE_ASPECT: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Aspect"), {101,149,194,255} };
+        case CLAY__ELEMENT_CONFIG_TYPE_IMAGE: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Image"), {121,189,154,255} };
+        case CLAY__ELEMENT_CONFIG_TYPE_FLOATING: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Floating"), {250,105,0,255} };
+        case CLAY__ELEMENT_CONFIG_TYPE_CLIP: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) {CLAY_STRING("Scroll"), {242, 196, 90, 255} };
+        case CLAY__ELEMENT_CONFIG_TYPE_BORDER: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) {CLAY_STRING("Border"), {108, 91, 123, 255} };
+        case CLAY__ELEMENT_CONFIG_TYPE_CUSTOM: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Custom"), {11,72,107,255} };
+        default: break;
+    }
+    return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Error"), {0,0,0,255} };
+}
+
+void Clay__RenderElementConfigTypeLabel(Clay_String label, Clay_Color color, bool offscreen) {
+    Clay_Color backgroundColor = color;
+    backgroundColor.a = 90;
+    CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .backgroundColor = backgroundColor, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = color, .width = { 1, 1, 1, 1, 0 } } }) {
+        CLAY_TEXT(label, CLAY_TEXT_CONFIG({ .textColor = offscreen ? CLAY__DEBUGVIEW_COLOR_3 : CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 }));
+    }
+}
+
+typedef struct {
+    int32_t rowCount;
+    int32_t selectedElementRowIndex;
+} Clay__RenderDebugLayoutData;
+
+// Returns row count
+Clay__RenderDebugLayoutData Clay__RenderDebugLayoutElementsList(int32_t initialRootsLength, int32_t highlightedRowIndex) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    Clay__int32_tArray dfsBuffer = context->reusableElementIndexBuffer;
+    Clay__DebugView_ScrollViewItemLayoutConfig = CLAY__INIT(Clay_LayoutConfig) { .sizing = { .height = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT) }, .childGap = 6, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }};
+    Clay__RenderDebugLayoutData layoutData = CLAY__DEFAULT_STRUCT;
+
+    uint32_t highlightedElementId = 0;
+
+    for (int32_t rootIndex = 0; rootIndex < initialRootsLength; ++rootIndex) {
+        dfsBuffer.length = 0;
+        Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, rootIndex);
+        Clay__int32_tArray_Add(&dfsBuffer, (int32_t)root->layoutElementIndex);
+        context->treeNodeVisited.internalArray[0] = false;
+        if (rootIndex > 0) {
+            CLAY(CLAY_IDI("Clay__DebugView_EmptyRowOuter", rootIndex), { .layout = { .sizing = {.width = CLAY_SIZING_GROW(0)}, .padding = {CLAY__DEBUGVIEW_INDENT_WIDTH / 2, 0, 0, 0} } }) {
+                CLAY(CLAY_IDI("Clay__DebugView_EmptyRow", rootIndex), { .layout = { .sizing = { .width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED((float)CLAY__DEBUGVIEW_ROW_HEIGHT) }}, .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = { .top = 1 } } }) {}
+            }
+            layoutData.rowCount++;
+        }
+        while (dfsBuffer.length > 0) {
+            int32_t currentElementIndex = Clay__int32_tArray_GetValue(&dfsBuffer, (int)dfsBuffer.length - 1);
+            Clay_LayoutElement *currentElement = Clay_LayoutElementArray_Get(&context->layoutElements, (int)currentElementIndex);
+            if (context->treeNodeVisited.internalArray[dfsBuffer.length - 1]) {
+                if (!currentElement->isTextElement && currentElement->children.length > 0) {
+                    Clay__CloseElement();
+                    Clay__CloseElement();
+                    Clay__CloseElement();
+                }
+                dfsBuffer.length--;
+                continue;
+            }
+
+            if (currentElement->exiting) { // TODO there is a duplicate ID problem with exiting elements
+                dfsBuffer.length--;
+                continue;
+            }
+
+            if (highlightedRowIndex == layoutData.rowCount) {
+                if (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
+                    context->debugSelectedElementId = currentElement->id;
+                }
+                highlightedElementId = currentElement->id;
+            }
+
+            context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = true;
+            Clay_LayoutElementHashMapItem *currentElementData = Clay__GetHashMapItem(currentElement->id);
+            bool offscreen = Clay__ElementIsOffscreen(&currentElementData->boundingBox);
+            if (context->debugSelectedElementId == currentElement->id) {
+                layoutData.selectedElementRowIndex = layoutData.rowCount;
+            }
+            CLAY(CLAY_IDI("Clay__DebugView_ElementOuter", currentElement->id), { .layout = Clay__DebugView_ScrollViewItemLayoutConfig }) {
+                // Collapse icon / button
+                if (!(currentElement->isTextElement || currentElement->children.length == 0)) {
+                    CLAY(CLAY_IDI("Clay__DebugView_CollapseElement", currentElement->id), {
+                        .layout = { .sizing = {CLAY_SIZING_FIXED(16), CLAY_SIZING_FIXED(16)}, .childAlignment = { CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER} },
+                        .cornerRadius = CLAY_CORNER_RADIUS(4),
+                        .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = {1, 1, 1, 1, 0} },
+                    }) {
+                        CLAY_TEXT((currentElementData && currentElementData->debugData.collapsed) ? CLAY_STRING("+") : CLAY_STRING("-"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 }));
+                    }
+                } else { // Square dot for empty containers
+                    CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_FIXED(16), CLAY_SIZING_FIXED(16)}, .childAlignment = { CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER } } }) {
+                        CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_FIXED(8), CLAY_SIZING_FIXED(8)} }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_3, .cornerRadius = CLAY_CORNER_RADIUS(2) }) {}
+                    }
+                }
+                // Collisions and offscreen info
+                if (currentElementData) {
+                    if (currentElementData->debugData.collision) {
+                        CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 }}, .border = { .color = {177, 147, 8, 255}, .width = {1, 1, 1, 1, 0} } }) {
+                            CLAY_TEXT(CLAY_STRING("Duplicate ID"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16 }));
+                        }
+                    }
+                    if (offscreen) {
+                        CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .border = {  .color = CLAY__DEBUGVIEW_COLOR_3, .width = { 1, 1, 1, 1, 0} } }) {
+                            CLAY_TEXT(CLAY_STRING("Offscreen"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16 }));
+                        }
+                    }
+                }
+                if (currentElementData->elementId.stringId.length > 0) {
+                    CLAY_AUTO_ID() {
+                        Clay_TextElementConfig textConfig = offscreen ? CLAY__INIT(Clay_TextElementConfig) { .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16 } : Clay__DebugView_TextNameConfig;
+                        CLAY_TEXT(currentElementData->elementId.stringId, textConfig);
+                        if (currentElementData->elementId.offset != 0) {
+                            CLAY_TEXT(CLAY_STRING(" ("), textConfig);
+                            CLAY_TEXT(Clay__IntToString(currentElementData->elementId.offset), textConfig);
+                            CLAY_TEXT(CLAY_STRING(")"), textConfig);
+                        }
+                    }
+                }
+                if (currentElement->isTextElement) {
+                    Clay__RenderElementConfigTypeLabel(CLAY_STRING("Text"), CLAY__INIT(Clay_Color) { 105,210,231,255 }, offscreen);
+                } else {
+                    if (currentElement->config.backgroundColor.a > 0) {
+                        Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_BACKGROUND_COLOR);
+                        Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+                    }
+                    if (currentElement->config.overlayColor.a > 0) {
+                        Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_OVERLAY_COLOR);
+                        Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+                    }
+                    if (!Clay__MemCmp((const char*)&currentElement->config.cornerRadius, (const char*)&Clay__CornerRadius_DEFAULT, sizeof(Clay_CornerRadius))) {
+                        Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_CORNER_RADIUS);
+                        Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+                    }
+                    if (currentElement->config.aspectRatio.aspectRatio != 0) {
+                        Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_ASPECT);
+                        Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+                    }
+                    if (currentElement->config.image.imageData) {
+                        Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_IMAGE);
+                        Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+                    }
+                    if (currentElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) {
+                        Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_FLOATING);
+                        Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+                    }
+                    if (currentElement->config.clip.horizontal || currentElement->config.clip.vertical) {
+                        Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_CLIP);
+                        Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+                    }
+                    if (Clay__BorderHasAnyWidth(&currentElement->config.border)) {
+                        Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_BORDER);
+                        Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+                    }
+                    if (currentElement->config.custom.customData) {
+                        Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_CUSTOM);
+                        Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+                    }
+                }
+            }
+
+            // Render the text contents below the element as a non-interactive row
+            if (currentElement->isTextElement) {
+                layoutData.rowCount++;
+                Clay__TextElementData *textElementData = &currentElement->textElementData;
+                Clay_TextElementConfig rawTextConfig = offscreen ? CLAY__INIT(Clay_TextElementConfig) { .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16 } : Clay__DebugView_TextNameConfig;
+                CLAY_AUTO_ID({ .layout = { .sizing = { .height = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER } } }) {
+                    CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_INDENT_WIDTH + 16) } } }) {}
+                    CLAY_TEXT(CLAY_STRING("\""), rawTextConfig);
+                    CLAY_TEXT(textElementData->text.length > 40 ? (CLAY__INIT(Clay_String) { .length = 40, .chars = textElementData->text.chars }) : textElementData->text, rawTextConfig);
+                    if (textElementData->text.length > 40) {
+                        CLAY_TEXT(CLAY_STRING("..."), rawTextConfig);
+                    }
+                    CLAY_TEXT(CLAY_STRING("\""), rawTextConfig);
+                }
+            } else if (currentElement->children.length > 0) {
+                Clay__OpenElement();
+                Clay__ConfigureOpenElement(CLAY__INIT(Clay_ElementDeclaration) { .layout = { .padding = { .left = 8 } } });
+                Clay__OpenElement();
+                Clay__ConfigureOpenElement(CLAY__INIT(Clay_ElementDeclaration) { .layout = { .padding = { .left = CLAY__DEBUGVIEW_INDENT_WIDTH }}, .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = { .left = 1 } }});
+                Clay__OpenElement();
+                Clay__ConfigureOpenElement(CLAY__INIT(Clay_ElementDeclaration) { .layout = { .layoutDirection = CLAY_TOP_TO_BOTTOM } });
+            }
+
+            layoutData.rowCount++;
+            if (!(currentElement->isTextElement || (currentElementData && currentElementData->debugData.collapsed))) {
+                for (int32_t i = currentElement->children.length - 1; i >= 0; --i) {
+                    Clay__int32_tArray_Add(&dfsBuffer, currentElement->children.elements[i]);
+                    context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = false; // TODO needs to be ranged checked
+                }
+            }
+        }
+    }
+
+    if (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
+        Clay_ElementId collapseButtonId = Clay__HashString(CLAY_STRING("Clay__DebugView_CollapseElement"), 0);
+        for (int32_t i = (int)context->pointerOverIds.length - 1; i >= 0; i--) {
+            Clay_ElementId *elementId = Clay_ElementIdArray_Get(&context->pointerOverIds, i);
+            if (elementId->baseId == collapseButtonId.baseId) {
+                Clay_LayoutElementHashMapItem *highlightedItem = Clay__GetHashMapItem(elementId->offset);
+                highlightedItem->debugData.collapsed = !highlightedItem->debugData.collapsed;
+                break;
+            }
+        }
+    }
+
+    if (highlightedElementId) {
+        CLAY(CLAY_ID("Clay__DebugView_ElementHighlight"), { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)} }, .floating = { .parentId = highlightedElementId, .zIndex = 32767, .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH, .attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID } }) {
+            CLAY(CLAY_ID("Clay__DebugView_ElementHighlightRectangle"), { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)} }, .backgroundColor = Clay__debugViewHighlightColor }) {}
+        }
+    }
+    return layoutData;
+}
+
+void Clay__RenderDebugLayoutSizing(Clay_SizingAxis sizing, Clay_TextElementConfig infoTextConfig) {
+    Clay_String sizingLabel = CLAY_STRING("GROW");
+    if (sizing.type == CLAY__SIZING_TYPE_FIT) {
+        sizingLabel = CLAY_STRING("FIT");
+    } else if (sizing.type == CLAY__SIZING_TYPE_PERCENT) {
+        sizingLabel = CLAY_STRING("PERCENT");
+    } else if (sizing.type == CLAY__SIZING_TYPE_FIXED) {
+        sizingLabel = CLAY_STRING("FIXED");
+    }
+    CLAY_TEXT(sizingLabel, infoTextConfig);
+    if (sizing.type == CLAY__SIZING_TYPE_GROW || sizing.type == CLAY__SIZING_TYPE_FIT || sizing.type == CLAY__SIZING_TYPE_FIXED) {
+        CLAY_TEXT(CLAY_STRING("("), infoTextConfig);
+        if (sizing.size.minMax.min != 0) {
+            CLAY_TEXT(CLAY_STRING("min: "), infoTextConfig);
+            CLAY_TEXT(Clay__IntToString(sizing.size.minMax.min), infoTextConfig);
+            if (sizing.size.minMax.max != CLAY__MAXFLOAT) {
+                CLAY_TEXT(CLAY_STRING(", "), infoTextConfig);
+            }
+        }
+        if (sizing.size.minMax.max != CLAY__MAXFLOAT) {
+            CLAY_TEXT(CLAY_STRING("max: "), infoTextConfig);
+            CLAY_TEXT(Clay__IntToString(sizing.size.minMax.max), infoTextConfig);
+        }
+        CLAY_TEXT(CLAY_STRING(")"), infoTextConfig);
+    } else if (sizing.type == CLAY__SIZING_TYPE_PERCENT) {
+        CLAY_TEXT(CLAY_STRING("("), infoTextConfig);
+        CLAY_TEXT(Clay__IntToString(sizing.size.percent * 100), infoTextConfig);
+        CLAY_TEXT(CLAY_STRING("%)"), infoTextConfig);
+    }
+}
+
+void Clay__DebugViewRenderElementConfigHeader(Clay_String elementId, Clay__DebugElementConfigType type) {
+    Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(type);
+    Clay_Color backgroundColor = config.color;
+    backgroundColor.a = 90;
+    CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .backgroundColor = backgroundColor, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = config.color, .width = { 1, 1, 1, 1, 0 } } }) {
+        CLAY_TEXT(config.label, CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 }));
+    }
+}
+
+void Clay__RenderDebugViewColor(Clay_Color color, Clay_TextElementConfig textConfig) {
+    CLAY_AUTO_ID({ .layout = { .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) {
+        CLAY_TEXT(CLAY_STRING("{ r: "), textConfig);
+        CLAY_TEXT(Clay__IntToString(color.r), textConfig);
+        CLAY_TEXT(CLAY_STRING(", g: "), textConfig);
+        CLAY_TEXT(Clay__IntToString(color.g), textConfig);
+        CLAY_TEXT(CLAY_STRING(", b: "), textConfig);
+        CLAY_TEXT(Clay__IntToString(color.b), textConfig);
+        CLAY_TEXT(CLAY_STRING(", a: "), textConfig);
+        CLAY_TEXT(Clay__IntToString(color.a), textConfig);
+        CLAY_TEXT(CLAY_STRING(" }"), textConfig);
+        CLAY_AUTO_ID({ .layout = { .sizing = { .width = CLAY_SIZING_FIXED(10) } } }) {}
+        CLAY_AUTO_ID({ .layout = { .sizing = { CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT - 8), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT - 8)} }, .backgroundColor = color, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = CLAY__DEBUGVIEW_COLOR_4, .width = { 1, 1, 1, 1, 0 } } }) {}
+    }
+}
+
+void Clay__RenderDebugViewCornerRadius(Clay_CornerRadius cornerRadius, Clay_TextElementConfig textConfig) {
+    CLAY_AUTO_ID({ .layout = { .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) {
+        CLAY_TEXT(CLAY_STRING("{ topLeft: "), textConfig);
+        CLAY_TEXT(Clay__IntToString(cornerRadius.topLeft), textConfig);
+        CLAY_TEXT(CLAY_STRING(", topRight: "), textConfig);
+        CLAY_TEXT(Clay__IntToString(cornerRadius.topRight), textConfig);
+        CLAY_TEXT(CLAY_STRING(", bottomLeft: "), textConfig);
+        CLAY_TEXT(Clay__IntToString(cornerRadius.bottomLeft), textConfig);
+        CLAY_TEXT(CLAY_STRING(", bottomRight: "), textConfig);
+        CLAY_TEXT(Clay__IntToString(cornerRadius.bottomRight), textConfig);
+        CLAY_TEXT(CLAY_STRING(" }"), textConfig);
+    }
+}
+
+void HandleDebugViewCloseButtonInteraction(Clay_ElementId elementId, Clay_PointerData pointerInfo, void *userData) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    (void) elementId; (void) pointerInfo; (void) userData;
+    if (pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
+        context->debugModeEnabled = false;
+    }
+}
+
+void Clay__RenderDebugView(void) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    Clay_ElementId closeButtonId = Clay__HashString(CLAY_STRING("Clay__DebugViewTopHeaderCloseButtonOuter"), 0);
+    if (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
+        for (int32_t i = 0; i < context->pointerOverIds.length; ++i) {
+            Clay_ElementId *elementId = Clay_ElementIdArray_Get(&context->pointerOverIds, i);
+            if (elementId->id == closeButtonId.id) {
+                context->debugModeEnabled = false;
+                return;
+            }
+        }
+    }
+
+    uint32_t initialRootsLength = context->layoutElementTreeRoots.length;
+    uint32_t initialElementsLength = context->layoutElements.length;
+    Clay_TextElementConfig infoTextConfig = CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16, .wrapMode = CLAY_TEXT_WRAP_NONE });
+    Clay_TextElementConfig infoTitleConfig = CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16, .wrapMode = CLAY_TEXT_WRAP_NONE });
+    Clay_ElementId scrollId = Clay__HashString(CLAY_STRING("Clay__DebugViewOuterScrollPane"), 0);
+    float scrollYOffset = 0;
+    bool pointerInDebugView = context->pointerInfo.position.y < context->layoutDimensions.height - 300;
+    for (int32_t i = 0; i < context->scrollContainerDatas.length; ++i) {
+        Clay__ScrollContainerDataInternal *scrollContainerData = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i);
+        if (scrollContainerData->elementId == scrollId.id) {
+            if (!context->externalScrollHandlingEnabled) {
+                scrollYOffset = scrollContainerData->scrollPosition.y;
+            } else {
+                pointerInDebugView = context->pointerInfo.position.y + scrollContainerData->scrollPosition.y < context->layoutDimensions.height - 300;
+            }
+            break;
+        }
+    }
+    int32_t highlightedRow = pointerInDebugView
+            ? (int32_t)((context->pointerInfo.position.y - scrollYOffset) / (float)CLAY__DEBUGVIEW_ROW_HEIGHT) - 1
+            : -1;
+    if (context->pointerInfo.position.x < context->layoutDimensions.width - (float)Clay__debugViewWidth) {
+        highlightedRow = -1;
+    }
+    Clay__RenderDebugLayoutData layoutData = CLAY__DEFAULT_STRUCT;
+    CLAY(CLAY_ID("Clay__DebugView"), {
+         .layout = { .sizing = { CLAY_SIZING_FIXED((float)Clay__debugViewWidth) , CLAY_SIZING_FIXED(context->layoutDimensions.height) }, .layoutDirection = CLAY_TOP_TO_BOTTOM },
+        .floating = { .zIndex = 32765, .attachPoints = { .element = CLAY_ATTACH_POINT_LEFT_CENTER, .parent = CLAY_ATTACH_POINT_RIGHT_CENTER }, .attachTo = CLAY_ATTACH_TO_ROOT, .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT },
+        .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = { .bottom = 1 } }
+    }) {
+        CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .padding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_2 }) {
+            CLAY_TEXT(CLAY_STRING("Clay Debug Tools"), infoTextConfig);
+            CLAY_AUTO_ID({ .layout = { .sizing = { .width = CLAY_SIZING_GROW(0) } } }) {}
+            // Close button
+            CLAY_AUTO_ID({
+                .layout = { .sizing = {CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT - 10), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT - 10)}, .childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER} },
+                .backgroundColor = {217,91,67,80},
+                .cornerRadius = CLAY_CORNER_RADIUS(4),
+                .border = { .color = { 217,91,67,255 }, .width = { 1, 1, 1, 1, 0 } },
+            }) {
+                Clay_OnHover(HandleDebugViewCloseButtonInteraction, 0);
+                CLAY_TEXT(CLAY_STRING("x"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 }));
+            }
+        }
+        CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(1)} }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_3 } ) {}
+        CLAY(scrollId, { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)} }, .clip = { .horizontal = true, .vertical = true, .childOffset = Clay_GetScrollOffset() } }) {
+            CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = ((initialElementsLength + initialRootsLength) & 1) == 0 ? CLAY__DEBUGVIEW_COLOR_2 : CLAY__DEBUGVIEW_COLOR_1 }) {
+                Clay_ElementId panelContentsId = Clay__HashString(CLAY_STRING("Clay__DebugViewPaneOuter"), 0);
+                // Element list
+                CLAY(panelContentsId, { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)} }, .floating = { .zIndex = 32766, .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH, .attachTo = CLAY_ATTACH_TO_PARENT, .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT } }) {
+                    CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)}, .padding = { CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+                        layoutData = Clay__RenderDebugLayoutElementsList((int32_t)initialRootsLength, highlightedRow);
+                    }
+                }
+                float contentWidth = Clay__GetHashMapItem(panelContentsId.id)->layoutElement->dimensions.width;
+                CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIXED(contentWidth) }, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {}
+                for (int32_t i = 0; i < layoutData.rowCount; i++) {
+                    Clay_Color rowColor = (i & 1) == 0 ? CLAY__DEBUGVIEW_COLOR_2 : CLAY__DEBUGVIEW_COLOR_1;
+                    if (i == layoutData.selectedElementRowIndex) {
+                        rowColor = CLAY__DEBUGVIEW_COLOR_SELECTED_ROW;
+                    }
+                    if (i == highlightedRow) {
+                        rowColor.r *= 1.25f;
+                        rowColor.g *= 1.25f;
+                        rowColor.b *= 1.25f;
+                    }
+                    CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = rowColor } ) {}
+                }
+            }
+        }
+        CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(1)} }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_3 }) {}
+        Clay_LayoutElementHashMapItem *selectedItem = Clay__GetHashMapItem(context->debugSelectedElementId);
+        if (selectedItem->layoutElement) {
+            CLAY_AUTO_ID({
+                .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(300)}, .layoutDirection = CLAY_TOP_TO_BOTTOM },
+                .backgroundColor = CLAY__DEBUGVIEW_COLOR_2 ,
+                .clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() },
+                .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = { .betweenChildren = 1 } }
+            }) {
+                CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT + 8)}, .padding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) {
+                    CLAY_TEXT(CLAY_STRING("Element Configuration"), infoTextConfig);
+                    CLAY_AUTO_ID({ .layout = { .sizing = { .width = CLAY_SIZING_GROW(0) } } }) {}
+                    if (selectedItem->elementId.stringId.length != 0) {
+                        CLAY_TEXT(selectedItem->elementId.stringId, infoTitleConfig);
+                        if (selectedItem->elementId.offset != 0) {
+                            CLAY_TEXT(CLAY_STRING(" ("), infoTitleConfig);
+                            CLAY_TEXT(Clay__IntToString(selectedItem->elementId.offset), infoTitleConfig);
+                            CLAY_TEXT(CLAY_STRING(")"), infoTitleConfig);
+                        }
+                    }
+                }
+                Clay_Padding attributeConfigPadding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 8, 8};
+                // Clay_LayoutConfig debug info
+                CLAY_AUTO_ID({ .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+                    CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .backgroundColor = { 200, 200, 200, 120 }, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = { 200, 200, 200, 255 }, .width = { 1, 1, 1, 1, 0 } } }) {
+                        CLAY_TEXT(CLAY_STRING("Layout"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 }));
+                    }
+                    // .boundingBox
+                    CLAY_TEXT(CLAY_STRING("Bounding Box"), infoTitleConfig);
+                    CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+                        CLAY_TEXT(CLAY_STRING("{ x: "), infoTextConfig);
+                        CLAY_TEXT(Clay__IntToString(selectedItem->boundingBox.x), infoTextConfig);
+                        CLAY_TEXT(CLAY_STRING(", y: "), infoTextConfig);
+                        CLAY_TEXT(Clay__IntToString(selectedItem->boundingBox.y), infoTextConfig);
+                        CLAY_TEXT(CLAY_STRING(", width: "), infoTextConfig);
+                        CLAY_TEXT(Clay__IntToString(selectedItem->boundingBox.width), infoTextConfig);
+                        CLAY_TEXT(CLAY_STRING(", height: "), infoTextConfig);
+                        CLAY_TEXT(Clay__IntToString(selectedItem->boundingBox.height), infoTextConfig);
+                        CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig);
+                    }
+                    if (!selectedItem->layoutElement->isTextElement) {
+                        // .layoutDirection
+                        CLAY_TEXT(CLAY_STRING("Layout Direction"), infoTitleConfig);
+                        Clay_LayoutConfig *layoutConfig = &selectedItem->layoutElement->config.layout;
+                        CLAY_TEXT(layoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM ? CLAY_STRING("TOP_TO_BOTTOM") : CLAY_STRING("LEFT_TO_RIGHT"), infoTextConfig);
+                        // .sizing
+                        CLAY_TEXT(CLAY_STRING("Sizing"), infoTitleConfig);
+                        CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+                            CLAY_TEXT(CLAY_STRING("width: "), infoTextConfig);
+                            Clay__RenderDebugLayoutSizing(layoutConfig->sizing.width, infoTextConfig);
+                        }
+                        CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+                            CLAY_TEXT(CLAY_STRING("height: "), infoTextConfig);
+                            Clay__RenderDebugLayoutSizing(layoutConfig->sizing.height, infoTextConfig);
+                        }
+                        // .padding
+                        CLAY_TEXT(CLAY_STRING("Padding"), infoTitleConfig);
+                        CLAY(CLAY_ID("Clay__DebugViewElementInfoPadding"), { }) {
+                            CLAY_TEXT(CLAY_STRING("{ left: "), infoTextConfig);
+                            CLAY_TEXT(Clay__IntToString(layoutConfig->padding.left), infoTextConfig);
+                            CLAY_TEXT(CLAY_STRING(", right: "), infoTextConfig);
+                            CLAY_TEXT(Clay__IntToString(layoutConfig->padding.right), infoTextConfig);
+                            CLAY_TEXT(CLAY_STRING(", top: "), infoTextConfig);
+                            CLAY_TEXT(Clay__IntToString(layoutConfig->padding.top), infoTextConfig);
+                            CLAY_TEXT(CLAY_STRING(", bottom: "), infoTextConfig);
+                            CLAY_TEXT(Clay__IntToString(layoutConfig->padding.bottom), infoTextConfig);
+                            CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig);
+                        }
+                        // .childGap
+                        CLAY_TEXT(CLAY_STRING("Child Gap"), infoTitleConfig);
+                        CLAY_TEXT(Clay__IntToString(layoutConfig->childGap), infoTextConfig);
+                        // .childAlignment
+                        CLAY_TEXT(CLAY_STRING("Child Alignment"), infoTitleConfig);
+                        CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+                            CLAY_TEXT(CLAY_STRING("{ x: "), infoTextConfig);
+                            Clay_String alignX = CLAY_STRING("LEFT");
+                            if (layoutConfig->childAlignment.x == CLAY_ALIGN_X_CENTER) {
+                                alignX = CLAY_STRING("CENTER");
+                            } else if (layoutConfig->childAlignment.x == CLAY_ALIGN_X_RIGHT) {
+                                alignX = CLAY_STRING("RIGHT");
+                            }
+                            CLAY_TEXT(alignX, infoTextConfig);
+                            CLAY_TEXT(CLAY_STRING(", y: "), infoTextConfig);
+                            Clay_String alignY = CLAY_STRING("TOP");
+                            if (layoutConfig->childAlignment.y == CLAY_ALIGN_Y_CENTER) {
+                                alignY = CLAY_STRING("CENTER");
+                            } else if (layoutConfig->childAlignment.y == CLAY_ALIGN_Y_BOTTOM) {
+                                alignY = CLAY_STRING("BOTTOM");
+                            }
+                            CLAY_TEXT(alignY, infoTextConfig);
+                            CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig);
+                        }
+                    }
+                }
+                if (selectedItem->layoutElement->isTextElement) {
+                    Clay_TextElementConfig *textConfig = &selectedItem->layoutElement->textConfig;
+                    CLAY_AUTO_ID({ .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+                        Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_TEXT);
+                        // .fontSize
+                        CLAY_TEXT(CLAY_STRING("Font Size"), infoTitleConfig);
+                        CLAY_TEXT(Clay__IntToString(textConfig->fontSize), infoTextConfig);
+                        // .fontId
+                        CLAY_TEXT(CLAY_STRING("Font ID"), infoTitleConfig);
+                        CLAY_TEXT(Clay__IntToString(textConfig->fontId), infoTextConfig);
+                        // .lineHeight
+                        CLAY_TEXT(CLAY_STRING("Line Height"), infoTitleConfig);
+                        CLAY_TEXT(textConfig->lineHeight == 0 ? CLAY_STRING("auto") : Clay__IntToString(textConfig->lineHeight), infoTextConfig);
+                        // .letterSpacing
+                        CLAY_TEXT(CLAY_STRING("Letter Spacing"), infoTitleConfig);
+                        CLAY_TEXT(Clay__IntToString(textConfig->letterSpacing), infoTextConfig);
+                        // .wrapMode
+                        CLAY_TEXT(CLAY_STRING("Wrap Mode"), infoTitleConfig);
+                        Clay_String wrapMode = CLAY_STRING("WORDS");
+                        if (textConfig->wrapMode == CLAY_TEXT_WRAP_NONE) {
+                            wrapMode = CLAY_STRING("NONE");
+                        } else if (textConfig->wrapMode == CLAY_TEXT_WRAP_NEWLINES) {
+                            wrapMode = CLAY_STRING("NEWLINES");
+                        }
+                        CLAY_TEXT(wrapMode, infoTextConfig);
+                        // .textAlignment
+                        CLAY_TEXT(CLAY_STRING("Text Alignment"), infoTitleConfig);
+                        Clay_String textAlignment = CLAY_STRING("LEFT");
+                        if (textConfig->textAlignment == CLAY_TEXT_ALIGN_CENTER) {
+                            textAlignment = CLAY_STRING("CENTER");
+                        } else if (textConfig->textAlignment == CLAY_TEXT_ALIGN_RIGHT) {
+                            textAlignment = CLAY_STRING("RIGHT");
+                        }
+                        CLAY_TEXT(textAlignment, infoTextConfig);
+                        // .textColor
+                        CLAY_TEXT(CLAY_STRING("Text Color"), infoTitleConfig);
+                        Clay__RenderDebugViewColor(textConfig->textColor, infoTextConfig);
+                    }
+                } else {
+                    CLAY(CLAY_ID("Clay__DebugViewElementInfoSharedBody"), { .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+                        Clay__DebugElementConfigTypeLabelConfig labelConfig = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_BACKGROUND_COLOR);
+                        Clay_Color backgroundColor = labelConfig.color;
+                        backgroundColor.a = 90;
+                        CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .backgroundColor = backgroundColor, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = labelConfig.color, .width = { 1, 1, 1, 1, 0 } } }) {
+                            CLAY_TEXT(CLAY_STRING("Color & Radius"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 }));
+                        }
+                        // .backgroundColor
+                        if (selectedItem->layoutElement->config.backgroundColor.a > 0) {
+                            CLAY_TEXT(CLAY_STRING("Background Color"), infoTitleConfig);
+                            Clay__RenderDebugViewColor(selectedItem->layoutElement->config.backgroundColor, infoTextConfig);
+                        }
+                        // .cornerRadius
+                        if (!Clay__MemCmp((const char*)&selectedItem->layoutElement->config.cornerRadius, (const char*)&Clay__CornerRadius_DEFAULT, sizeof(Clay_CornerRadius))) {
+                            CLAY_TEXT(CLAY_STRING("Corner Radius"), infoTitleConfig);
+                            Clay__RenderDebugViewCornerRadius(selectedItem->layoutElement->config.cornerRadius, infoTextConfig);
+                        }
+                        // .overlayColor
+                        if (selectedItem->layoutElement->config.overlayColor.a > 0) {
+                            CLAY_TEXT(CLAY_STRING("Overlay Color"), infoTitleConfig);
+                            Clay__RenderDebugViewColor(selectedItem->layoutElement->config.overlayColor, infoTextConfig);
+                        }
+                    }
+                    if (selectedItem->layoutElement->config.aspectRatio.aspectRatio > 0) {
+                        Clay_AspectRatioElementConfig *aspectRatioConfig = &selectedItem->layoutElement->config.aspectRatio;
+                        CLAY(CLAY_ID("Clay__DebugViewElementInfoAspectRatioBody"), { .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+                            Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_ASPECT);
+                            CLAY_TEXT(CLAY_STRING("Aspect Ratio"), infoTitleConfig);
+                            // Aspect Ratio
+                            CLAY(CLAY_ID("Clay__DebugViewElementInfoAspectRatio"), { }) {
+                                CLAY_TEXT(Clay__IntToString(aspectRatioConfig->aspectRatio), infoTextConfig);
+                                CLAY_TEXT(CLAY_STRING("."), infoTextConfig);
+                                float frac = aspectRatioConfig->aspectRatio - (int)(aspectRatioConfig->aspectRatio);
+                                frac *= 100;
+                                if ((int)frac < 10) {
+                                    CLAY_TEXT(CLAY_STRING("0"), infoTextConfig);
+                                }
+                                CLAY_TEXT(Clay__IntToString(frac), infoTextConfig);
+                            }
+                        }
+                    }
+                    if (selectedItem->layoutElement->config.image.imageData) {
+                        Clay_ImageElementConfig *imageConfig = &selectedItem->layoutElement->config.image;
+                        Clay_AspectRatioElementConfig aspectConfig = { 1 };
+                        if (selectedItem->layoutElement->config.aspectRatio.aspectRatio > 0) {
+                            aspectConfig = selectedItem->layoutElement->config.aspectRatio;
+                        }
+                        CLAY(CLAY_ID("Clay__DebugViewElementInfoImageBody"), { .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+                            Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_IMAGE);
+                            // Image Preview
+                            CLAY_TEXT(CLAY_STRING("Preview"), infoTitleConfig);
+                            CLAY_AUTO_ID({ .layout = { .sizing = { .width = CLAY_SIZING_GROW(64, 128), .height = CLAY_SIZING_GROW(64, 128) }}, .aspectRatio = aspectConfig, .image = *imageConfig }) {}
+                        }
+                    }
+                    if (selectedItem->layoutElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) {
+                        Clay_FloatingElementConfig* floatingConfig = &selectedItem->layoutElement->config.floating;
+                        CLAY_AUTO_ID({ .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+                            Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_FLOATING);
+                            // .offset
+                            CLAY_TEXT(CLAY_STRING("Offset"), infoTitleConfig);
+                            CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+                                CLAY_TEXT(CLAY_STRING("{ x: "), infoTextConfig);
+                                CLAY_TEXT(Clay__IntToString(floatingConfig->offset.x), infoTextConfig);
+                                CLAY_TEXT(CLAY_STRING(", y: "), infoTextConfig);
+                                CLAY_TEXT(Clay__IntToString(floatingConfig->offset.y), infoTextConfig);
+                                CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig);
+                            }
+                            // .expand
+                            CLAY_TEXT(CLAY_STRING("Expand"), infoTitleConfig);
+                            CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+                                CLAY_TEXT(CLAY_STRING("{ width: "), infoTextConfig);
+                                CLAY_TEXT(Clay__IntToString(floatingConfig->expand.width), infoTextConfig);
+                                CLAY_TEXT(CLAY_STRING(", height: "), infoTextConfig);
+                                CLAY_TEXT(Clay__IntToString(floatingConfig->expand.height), infoTextConfig);
+                                CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig);
+                            }
+                            // .zIndex
+                            CLAY_TEXT(CLAY_STRING("z-index"), infoTitleConfig);
+                            CLAY_TEXT(Clay__IntToString(floatingConfig->zIndex), infoTextConfig);
+                            // .parentId
+                            CLAY_TEXT(CLAY_STRING("Parent"), infoTitleConfig);
+                            Clay_LayoutElementHashMapItem *hashItem = Clay__GetHashMapItem(floatingConfig->parentId);
+                            CLAY_TEXT(hashItem->elementId.stringId, infoTextConfig);
+                            // .attachPoints
+                            CLAY_TEXT(CLAY_STRING("Attach Points"), infoTitleConfig);
+                            CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+                                CLAY_TEXT(CLAY_STRING("{ element: "), infoTextConfig);
+                                Clay_String attachPointElement = CLAY_STRING("LEFT_TOP");
+                                if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_LEFT_CENTER) {
+                                    attachPointElement = CLAY_STRING("LEFT_CENTER");
+                                } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_LEFT_BOTTOM) {
+                                    attachPointElement = CLAY_STRING("LEFT_BOTTOM");
+                                } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_CENTER_TOP) {
+                                    attachPointElement = CLAY_STRING("CENTER_TOP");
+                                } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_CENTER_CENTER) {
+                                    attachPointElement = CLAY_STRING("CENTER_CENTER");
+                                } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_CENTER_BOTTOM) {
+                                    attachPointElement = CLAY_STRING("CENTER_BOTTOM");
+                                } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_RIGHT_TOP) {
+                                    attachPointElement = CLAY_STRING("RIGHT_TOP");
+                                } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_RIGHT_CENTER) {
+                                    attachPointElement = CLAY_STRING("RIGHT_CENTER");
+                                } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_RIGHT_BOTTOM) {
+                                    attachPointElement = CLAY_STRING("RIGHT_BOTTOM");
+                                }
+                                CLAY_TEXT(attachPointElement, infoTextConfig);
+                                Clay_String attachPointParent = CLAY_STRING("LEFT_TOP");
+                                if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_LEFT_CENTER) {
+                                    attachPointParent = CLAY_STRING("LEFT_CENTER");
+                                } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_LEFT_BOTTOM) {
+                                    attachPointParent = CLAY_STRING("LEFT_BOTTOM");
+                                } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_CENTER_TOP) {
+                                    attachPointParent = CLAY_STRING("CENTER_TOP");
+                                } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_CENTER_CENTER) {
+                                    attachPointParent = CLAY_STRING("CENTER_CENTER");
+                                } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_CENTER_BOTTOM) {
+                                    attachPointParent = CLAY_STRING("CENTER_BOTTOM");
+                                } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_RIGHT_TOP) {
+                                    attachPointParent = CLAY_STRING("RIGHT_TOP");
+                                } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_RIGHT_CENTER) {
+                                    attachPointParent = CLAY_STRING("RIGHT_CENTER");
+                                } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_RIGHT_BOTTOM) {
+                                    attachPointParent = CLAY_STRING("RIGHT_BOTTOM");
+                                }
+                                CLAY_TEXT(CLAY_STRING(", parent: "), infoTextConfig);
+                                CLAY_TEXT(attachPointParent, infoTextConfig);
+                                CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig);
+                            }
+                            // .pointerCaptureMode
+                            CLAY_TEXT(CLAY_STRING("Pointer Capture Mode"), infoTitleConfig);
+                            Clay_String pointerCaptureMode = CLAY_STRING("NONE");
+                            if (floatingConfig->pointerCaptureMode == CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH) {
+                                pointerCaptureMode = CLAY_STRING("PASSTHROUGH");
+                            }
+                            CLAY_TEXT(pointerCaptureMode, infoTextConfig);
+                            // .attachTo
+                            CLAY_TEXT(CLAY_STRING("Attach To"), infoTitleConfig);
+                            Clay_String attachTo = CLAY_STRING("NONE");
+                            if (floatingConfig->attachTo == CLAY_ATTACH_TO_PARENT) {
+                                attachTo = CLAY_STRING("PARENT");
+                            } else if (floatingConfig->attachTo == CLAY_ATTACH_TO_ELEMENT_WITH_ID) {
+                                attachTo = CLAY_STRING("ELEMENT_WITH_ID");
+                            } else if (floatingConfig->attachTo == CLAY_ATTACH_TO_ROOT) {
+                                attachTo = CLAY_STRING("ROOT");
+                            }
+                            CLAY_TEXT(attachTo, infoTextConfig);
+                            // .clipTo
+                            CLAY_TEXT(CLAY_STRING("Clip To"), infoTitleConfig);
+                            Clay_String clipTo = CLAY_STRING("ATTACHED_PARENT");
+                            if (floatingConfig->clipTo == CLAY_CLIP_TO_NONE) {
+                                clipTo = CLAY_STRING("NONE");
+                            }
+                            CLAY_TEXT(clipTo, infoTextConfig);
+                        }
+                    }
+                    Clay_ClipElementConfig *clipConfig = &selectedItem->layoutElement->config.clip;
+                    if (clipConfig->horizontal || clipConfig->vertical) {
+                        CLAY_AUTO_ID({ .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+                            Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_CLIP);
+                            // .vertical
+                            CLAY_TEXT(CLAY_STRING("Vertical"), infoTitleConfig);
+                            CLAY_TEXT(clipConfig->vertical ? CLAY_STRING("true") : CLAY_STRING("false") , infoTextConfig);
+                            // .horizontal
+                            CLAY_TEXT(CLAY_STRING("Horizontal"), infoTitleConfig);
+                            CLAY_TEXT(clipConfig->horizontal ? CLAY_STRING("true") : CLAY_STRING("false") , infoTextConfig);
+                        }
+                    }
+                    Clay_BorderElementConfig *borderConfig = &selectedItem->layoutElement->config.border;
+                    if (Clay__BorderHasAnyWidth(borderConfig)) {
+                        CLAY(CLAY_ID("Clay__DebugViewElementInfoBorderBody"), { .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+                            Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_BORDER);
+                            CLAY_TEXT(CLAY_STRING("Border Widths"), infoTitleConfig);
+                            CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+                                CLAY_TEXT(CLAY_STRING("{ left: "), infoTextConfig);
+                                CLAY_TEXT(Clay__IntToString(borderConfig->width.left), infoTextConfig);
+                                CLAY_TEXT(CLAY_STRING(", right: "), infoTextConfig);
+                                CLAY_TEXT(Clay__IntToString(borderConfig->width.right), infoTextConfig);
+                                CLAY_TEXT(CLAY_STRING(", top: "), infoTextConfig);
+                                CLAY_TEXT(Clay__IntToString(borderConfig->width.top), infoTextConfig);
+                                CLAY_TEXT(CLAY_STRING(", bottom: "), infoTextConfig);
+                                CLAY_TEXT(Clay__IntToString(borderConfig->width.bottom), infoTextConfig);
+                                CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig);
+                            }
+                            // .textColor
+                            CLAY_TEXT(CLAY_STRING("Border Color"), infoTitleConfig);
+                            Clay__RenderDebugViewColor(borderConfig->color, infoTextConfig);
+                        }
+                    }
+                }
+            }
+        } else {
+            CLAY(CLAY_ID("Clay__DebugViewWarningsScrollPane"), { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(300)}, .childGap = 6, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_2, .clip = { .horizontal = true, .vertical = true, .childOffset = Clay_GetScrollOffset() } }) {
+                Clay_TextElementConfig warningConfig = CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16, .wrapMode = CLAY_TEXT_WRAP_NONE });
+                CLAY(CLAY_ID("Clay__DebugViewWarningItemHeader"), { .layout = { .sizing = {.height = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .padding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .childGap = 8, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) {
+                    CLAY_TEXT(CLAY_STRING("Warnings"), warningConfig);
+                }
+                CLAY(CLAY_ID("Clay__DebugViewWarningsTopBorder"), { .layout = { .sizing = { .width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(1)} }, .backgroundColor = {200, 200, 200, 255} }) {}
+                int32_t previousWarningsLength = context->warnings.length;
+                for (int32_t i = 0; i < previousWarningsLength; i++) {
+                    Clay__Warning warning = context->warnings.internalArray[i];
+                    CLAY(CLAY_IDI("Clay__DebugViewWarningItem", i), { .layout = { .sizing = {.height = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .padding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .childGap = 8, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) {
+                        CLAY_TEXT(warning.baseMessage, warningConfig);
+                        if (warning.dynamicMessage.length > 0) {
+                            CLAY_TEXT(warning.dynamicMessage, warningConfig);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+#pragma endregion
+
+uint32_t Clay__debugViewWidth = 400;
+Clay_Color Clay__debugViewHighlightColor = { 168, 66, 28, 100 };
+
+Clay__WarningArray Clay__WarningArray_Allocate_Arena(int32_t capacity, Clay_Arena *arena) {
+    size_t totalSizeBytes = capacity * sizeof(Clay_String);
+    Clay__WarningArray array = {.capacity = capacity, .length = 0};
+    uintptr_t nextAllocOffset = arena->nextAllocation + (64 - (arena->nextAllocation % 64));
+    if (nextAllocOffset + totalSizeBytes <= arena->capacity) {
+        array.internalArray = (Clay__Warning*)((uintptr_t)arena->memory + (uintptr_t)nextAllocOffset);
+        arena->nextAllocation = nextAllocOffset + totalSizeBytes;
+    }
+    else {
+        Clay__currentContext->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+            .errorType = CLAY_ERROR_TYPE_ARENA_CAPACITY_EXCEEDED,
+            .errorText = CLAY_STRING("Clay attempted to allocate memory in its arena, but ran out of capacity. Try increasing the capacity of the arena passed to Clay_Initialize()"),
+            .userData = Clay__currentContext->errorHandler.userData });
+    }
+    return array;
+}
+
+Clay__Warning *Clay__WarningArray_Add(Clay__WarningArray *array, Clay__Warning item)
+{
+    if (array->length < array->capacity) {
+        array->internalArray[array->length++] = item;
+        return &array->internalArray[array->length - 1];
+    }
+    return &CLAY__WARNING_DEFAULT;
+}
+
+void* Clay__Array_Allocate_Arena(int32_t capacity, uint32_t itemSize, Clay_Arena *arena)
+{
+    size_t totalSizeBytes = capacity * itemSize;
+    uintptr_t nextAllocOffset = arena->nextAllocation + ((64 - (arena->nextAllocation % 64)) & 63);
+    if (nextAllocOffset + totalSizeBytes <= arena->capacity) {
+        arena->nextAllocation = nextAllocOffset + totalSizeBytes;
+        return (void*)((uintptr_t)arena->memory + (uintptr_t)nextAllocOffset);
+    }
+    else {
+        Clay__currentContext->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+                .errorType = CLAY_ERROR_TYPE_ARENA_CAPACITY_EXCEEDED,
+                .errorText = CLAY_STRING("Clay attempted to allocate memory in its arena, but ran out of capacity. Try increasing the capacity of the arena passed to Clay_Initialize()"),
+                .userData = Clay__currentContext->errorHandler.userData });
+    }
+    return CLAY__NULL;
+}
+
+bool Clay__Array_RangeCheck(int32_t index, int32_t length)
+{
+    if (index < length && index >= 0) {
+        return true;
+    }
+    Clay_Context* context = Clay_GetCurrentContext();
+    context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+            .errorType = CLAY_ERROR_TYPE_INTERNAL_ERROR,
+            .errorText = CLAY_STRING("Clay attempted to make an out of bounds array access. This is an internal error and is likely a bug."),
+            .userData = context->errorHandler.userData });
+    return false;
+}
+
+bool Clay__Array_AddCapacityCheck(int32_t length, int32_t capacity)
+{
+    if (length < capacity) {
+        return true;
+    }
+    Clay_Context* context = Clay_GetCurrentContext();
+    context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+        .errorType = CLAY_ERROR_TYPE_INTERNAL_ERROR,
+        .errorText = CLAY_STRING("Clay attempted to make an out of bounds array access. This is an internal error and is likely a bug."),
+        .userData = context->errorHandler.userData });
+    return false;
+}
+
+// PUBLIC API FROM HERE ---------------------------------------
+
+CLAY_WASM_EXPORT("Clay_MinMemorySize")
+uint32_t Clay_MinMemorySize(void) {
+    Clay_Context fakeContext = {
+        .maxElementCount = Clay__defaultMaxElementCount,
+        .maxMeasureTextCacheWordCount = Clay__defaultMaxMeasureTextWordCacheCount,
+        .internalArena = {
+            .capacity = SIZE_MAX,
+            .memory = NULL,
+        }
+    };
+    Clay_Context* currentContext = Clay_GetCurrentContext();
+    if (currentContext) {
+        fakeContext.maxElementCount = currentContext->maxElementCount;
+        fakeContext.maxMeasureTextCacheWordCount = currentContext->maxMeasureTextCacheWordCount;
+    }
+    // Reserve space in the arena for the context, important for calculating min memory size correctly
+    Clay__Context_Allocate_Arena(&fakeContext.internalArena);
+    Clay__InitializePersistentMemory(&fakeContext);
+    Clay__InitializeEphemeralMemory(&fakeContext);
+    return (uint32_t)fakeContext.internalArena.nextAllocation + 128;
+}
+
+CLAY_WASM_EXPORT("Clay_CreateArenaWithCapacityAndMemory")
+Clay_Arena Clay_CreateArenaWithCapacityAndMemory(size_t capacity, void *memory) {
+    Clay_Arena arena = {
+        .capacity = capacity,
+        .memory = (char *)memory
+    };
+    return arena;
+}
+
+#ifndef CLAY_WASM
+void Clay_SetMeasureTextFunction(Clay_Dimensions (*measureTextFunction)(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData), void *userData) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    Clay__MeasureText = measureTextFunction;
+    context->measureTextUserData = userData;
+}
+void Clay_SetQueryScrollOffsetFunction(Clay_Vector2 (*queryScrollOffsetFunction)(uint32_t elementId, void *userData), void *userData) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    Clay__QueryScrollOffset = queryScrollOffsetFunction;
+    context->queryScrollOffsetUserData = userData;
+}
+#endif
+
+CLAY_WASM_EXPORT("Clay_SetLayoutDimensions")
+void Clay_SetLayoutDimensions(Clay_Dimensions dimensions) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    context->rootResizedLastFrame = !Clay__FloatEqual(context->layoutDimensions.width, dimensions.width) || !Clay__FloatEqual(context->layoutDimensions.height, dimensions.height);
+    context->layoutDimensions = dimensions;
+}
+
+CLAY_WASM_EXPORT("Clay_SetLayoutDimensions")
+Clay_Dimensions Clay_GetLayoutDimensions() {
+    Clay_Context* context = Clay_GetCurrentContext();
+    return context->layoutDimensions;
+}
+
+CLAY_WASM_EXPORT("Clay_SetPointerState")
+void Clay_SetPointerState(Clay_Vector2 position, bool isPointerDown) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    if (context->booleanWarnings.maxElementsExceeded) {
+        return;
+    }
+    context->pointerInfo.position = position;
+    context->pointerOverIds.length = 0;
+    Clay__int32_tArray dfsBuffer = context->layoutElementChildrenBuffer;
+    for (int32_t rootIndex = context->layoutElementTreeRoots.length - 1; rootIndex >= 0; --rootIndex) {
+        dfsBuffer.length = 0;
+        Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, rootIndex);
+        Clay__int32_tArray_Add(&dfsBuffer, (int32_t)root->layoutElementIndex);
+        context->treeNodeVisited.internalArray[0] = false;
+        bool found = false;
+        bool skipTree = false;
+        while (dfsBuffer.length > 0) {
+            if (context->treeNodeVisited.internalArray[dfsBuffer.length - 1]) {
+                dfsBuffer.length--;
+                continue;
+            }
+            context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = true;
+            Clay_LayoutElement *currentElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&dfsBuffer, (int)dfsBuffer.length - 1));
+
+            Clay_LayoutElementHashMapItem *mapItem = Clay__GetHashMapItem(currentElement->id); // TODO think of a way around this, maybe the fact that it's essentially a binary tree limits the cost, but the worst case is not great
+            int32_t clipElementId = Clay__int32_tArray_GetValue(&context->layoutElementClipElementIds, (int32_t)(currentElement - context->layoutElements.internalArray));
+            Clay_LayoutElementHashMapItem *clipItem = Clay__GetHashMapItem(clipElementId);
+            // This check skips mouse interactions for elements that are currently "exit transitioning"
+            if (mapItem && mapItem->generation > context->generation) {
+                // Conditionally skip mouse interactions on non-exit transitions, based on user config
+                if (!currentElement->isTextElement && currentElement->config.transition.handler) {
+                    for (int I = 0; I < context->transitionDatas.length; ++I) {
+                        Clay__TransitionDataInternal* data = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, I);
+                        if (data->elementId == currentElement->id) {
+                            if (currentElement->config.transition.interactionHandling == CLAY_TRANSITION_DISABLE_INTERACTIONS_WHILE_TRANSITIONING_POSITION) {
+                                if (data->state == CLAY_TRANSITION_STATE_EXITING || data->state == CLAY_TRANSITION_STATE_ENTERING || ((data->activeProperties & CLAY_TRANSITION_PROPERTY_POSITION) && data->state == CLAY_TRANSITION_STATE_TRANSITIONING)) {
+                                    skipTree = true;
+                                }
+                            } else if (currentElement->config.transition.interactionHandling == CLAY_TRANSITION_ALLOW_INTERACTIONS_WHILE_TRANSITIONING_POSITION) {
+                                if (data->state == CLAY_TRANSITION_STATE_EXITING) {
+                                    skipTree = true;
+                                }
+                            }
+                        }
+                    }
+                }
+
+                if (skipTree) {
+                    dfsBuffer.length--;
+                    continue;
+                }
+
+                Clay_BoundingBox elementBox = mapItem->boundingBox;
+                elementBox.x -= root->pointerOffset.x;
+                elementBox.y -= root->pointerOffset.y;
+                if ((Clay__PointIsInsideRect(position, elementBox)) && (clipElementId == 0 || (Clay__PointIsInsideRect(position, clipItem->boundingBox)) || context->externalScrollHandlingEnabled)) {
+                    if (!skipTree) {
+                        if (mapItem->onHoverFunction) {
+                            mapItem->onHoverFunction(mapItem->elementId, context->pointerInfo, mapItem->hoverFunctionUserData);
+                        }
+                        Clay_ElementIdArray_Add(&context->pointerOverIds, mapItem->elementId);
+                    }
+                    found = true;
+                }
+
+                for (int32_t i = currentElement->children.length - 1; i >= 0; --i) {
+                    Clay__int32_tArray_Add(&dfsBuffer, currentElement->children.elements[i]);
+                    context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = false; // TODO needs to be ranged checked
+                }
+            } else {
+                dfsBuffer.length--;
+            }
+        }
+
+        Clay_LayoutElement *rootElement = Clay_LayoutElementArray_Get(&context->layoutElements, root->layoutElementIndex);
+        if (found && rootElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE && rootElement->config.floating.pointerCaptureMode == CLAY_POINTER_CAPTURE_MODE_CAPTURE) {
+            break;
+        }
+    }
+
+    if (isPointerDown) {
+        if (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
+            context->pointerInfo.state = CLAY_POINTER_DATA_PRESSED;
+        } else if (context->pointerInfo.state != CLAY_POINTER_DATA_PRESSED) {
+            context->pointerInfo.state = CLAY_POINTER_DATA_PRESSED_THIS_FRAME;
+        }
+    } else {
+        if (context->pointerInfo.state == CLAY_POINTER_DATA_RELEASED_THIS_FRAME) {
+            context->pointerInfo.state = CLAY_POINTER_DATA_RELEASED;
+        } else if (context->pointerInfo.state != CLAY_POINTER_DATA_RELEASED)  {
+            context->pointerInfo.state = CLAY_POINTER_DATA_RELEASED_THIS_FRAME;
+        }
+    }
+}
+
+CLAY_WASM_EXPORT("Clay_GetPointerState")
+CLAY_DLL_EXPORT Clay_PointerData Clay_GetPointerState(void) {
+    return Clay_GetCurrentContext()->pointerInfo;
+}
+
+CLAY_WASM_EXPORT("Clay_Initialize")
+Clay_Context* Clay_Initialize(Clay_Arena arena, Clay_Dimensions layoutDimensions, Clay_ErrorHandler errorHandler) {
+    // Cacheline align memory passed in
+    uintptr_t baseOffset = 64 - ((uintptr_t)arena.memory % 64);
+    baseOffset = baseOffset == 64 ? 0 : baseOffset;
+    arena.memory += baseOffset;
+    Clay_Context *context = Clay__Context_Allocate_Arena(&arena);
+    if (context == NULL) return NULL;
+    // DEFAULTS
+    Clay_Context *oldContext = Clay_GetCurrentContext();
+    *context = CLAY__INIT(Clay_Context) {
+        .maxElementCount = oldContext ? oldContext->maxElementCount : Clay__defaultMaxElementCount,
+        .maxMeasureTextCacheWordCount = oldContext ? oldContext->maxMeasureTextCacheWordCount : Clay__defaultMaxMeasureTextWordCacheCount,
+        .errorHandler = errorHandler.errorHandlerFunction ? errorHandler : CLAY__INIT(Clay_ErrorHandler) { Clay__ErrorHandlerFunctionDefault, 0 },
+        .layoutDimensions = layoutDimensions,
+        .internalArena = arena,
+    };
+    Clay_SetCurrentContext(context);
+    Clay__InitializePersistentMemory(context);
+    Clay__InitializeEphemeralMemory(context);
+    for (int32_t i = 0; i < context->layoutElementsHashMap.capacity; ++i) {
+        context->layoutElementsHashMap.internalArray[i] = -1;
+    }
+    for (int32_t i = 0; i < context->measureTextHashMap.capacity; ++i) {
+        context->measureTextHashMap.internalArray[i] = 0;
+    }
+    context->measureTextHashMapInternal.length = 1; // Reserve the 0 value to mean "no next element"
+    context->layoutDimensions = layoutDimensions;
+    return context;
+}
+
+CLAY_WASM_EXPORT("Clay_GetCurrentContext")
+Clay_Context* Clay_GetCurrentContext(void) {
+    return Clay__currentContext;
+}
+
+CLAY_WASM_EXPORT("Clay_SetCurrentContext")
+void Clay_SetCurrentContext(Clay_Context* context) {
+    Clay__currentContext = context;
+}
+
+CLAY_WASM_EXPORT("Clay_GetScrollOffset")
+Clay_Vector2 Clay_GetScrollOffset(void) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    if (context->booleanWarnings.maxElementsExceeded) {
+        return CLAY__INIT(Clay_Vector2) CLAY__DEFAULT_STRUCT;
+    }
+    Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement();
+    for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) {
+        Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i);
+        if (mapping->elementId == openLayoutElement->id) {
+            return mapping->scrollPosition;
+        }
+    }
+    return CLAY__INIT(Clay_Vector2) CLAY__DEFAULT_STRUCT;
+}
+
+CLAY_WASM_EXPORT("Clay_UpdateScrollContainers")
+void Clay_UpdateScrollContainers(bool enableDragScrolling, Clay_Vector2 scrollDelta, float deltaTime) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    bool isPointerActive = enableDragScrolling && (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED || context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME);
+    // Don't apply scroll events to ancestors of the inner element
+    int32_t highestPriorityElementIndex = -1;
+    Clay__ScrollContainerDataInternal *highestPriorityScrollData = CLAY__NULL;
+    for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) {
+        Clay__ScrollContainerDataInternal *scrollData = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i);
+        if (!scrollData->openThisFrame) {
+            Clay__ScrollContainerDataInternalArray_RemoveSwapback(&context->scrollContainerDatas, i);
+            continue;
+        }
+        scrollData->openThisFrame = false;
+        Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(scrollData->elementId);
+        // Element isn't rendered this frame but scroll offset has been retained
+        if (!hashMapItem) {
+            Clay__ScrollContainerDataInternalArray_RemoveSwapback(&context->scrollContainerDatas, i);
+            continue;
+        }
+
+        // Touch / click is released
+        if (!isPointerActive && scrollData->pointerScrollActive) {
+            float xDiff = scrollData->scrollPosition.x - scrollData->scrollOrigin.x;
+            if (xDiff < -10 || xDiff > 10) {
+                scrollData->scrollMomentum.x = (scrollData->scrollPosition.x - scrollData->scrollOrigin.x) / (scrollData->momentumTime * 25);
+            }
+            float yDiff = scrollData->scrollPosition.y - scrollData->scrollOrigin.y;
+            if (yDiff < -10 || yDiff > 10) {
+                scrollData->scrollMomentum.y = (scrollData->scrollPosition.y - scrollData->scrollOrigin.y) / (scrollData->momentumTime * 25);
+            }
+            scrollData->pointerScrollActive = false;
+
+            scrollData->pointerOrigin = CLAY__INIT(Clay_Vector2){0,0};
+            scrollData->scrollOrigin = CLAY__INIT(Clay_Vector2){0,0};
+            scrollData->momentumTime = 0;
+        }
+
+        // Apply existing momentum
+        scrollData->scrollPosition.x += scrollData->scrollMomentum.x;
+        scrollData->scrollMomentum.x *= 0.95f;
+        bool scrollOccurred = scrollDelta.x != 0 || scrollDelta.y != 0;
+        if ((scrollData->scrollMomentum.x > -0.1f && scrollData->scrollMomentum.x < 0.1f) || scrollOccurred) {
+            scrollData->scrollMomentum.x = 0;
+        }
+        scrollData->scrollPosition.x = CLAY__MIN(CLAY__MAX(scrollData->scrollPosition.x, -(CLAY__MAX(scrollData->contentSize.width - scrollData->layoutElement->dimensions.width, 0))), 0);
+
+        scrollData->scrollPosition.y += scrollData->scrollMomentum.y;
+        scrollData->scrollMomentum.y *= 0.95f;
+        if ((scrollData->scrollMomentum.y > -0.1f && scrollData->scrollMomentum.y < 0.1f) || scrollOccurred) {
+            scrollData->scrollMomentum.y = 0;
+        }
+        scrollData->scrollPosition.y = CLAY__MIN(CLAY__MAX(scrollData->scrollPosition.y, -(CLAY__MAX(scrollData->contentSize.height - scrollData->layoutElement->dimensions.height, 0))), 0);
+
+        for (int32_t j = 0; j < context->pointerOverIds.length; ++j) { // TODO n & m are small here but this being n*m gives me the creeps
+            if (scrollData->layoutElement->id == Clay_ElementIdArray_Get(&context->pointerOverIds, j)->id) {
+                highestPriorityElementIndex = j;
+                highestPriorityScrollData = scrollData;
+            }
+        }
+    }
+
+    if (highestPriorityElementIndex > -1 && highestPriorityScrollData) {
+        Clay_LayoutElement *scrollElement = highestPriorityScrollData->layoutElement;
+        Clay_ClipElementConfig *clipConfig = &scrollElement->config.clip;
+        bool canScrollVertically = clipConfig->vertical && highestPriorityScrollData->contentSize.height > scrollElement->dimensions.height;
+        bool canScrollHorizontally = clipConfig->horizontal && highestPriorityScrollData->contentSize.width > scrollElement->dimensions.width;
+        // Handle wheel scroll
+        if (canScrollVertically) {
+            highestPriorityScrollData->scrollPosition.y = highestPriorityScrollData->scrollPosition.y + scrollDelta.y * 10;
+        }
+        if (canScrollHorizontally) {
+            highestPriorityScrollData->scrollPosition.x = highestPriorityScrollData->scrollPosition.x + scrollDelta.x * 10;
+        }
+        // Handle click / touch scroll
+        if (isPointerActive) {
+            highestPriorityScrollData->scrollMomentum = CLAY__INIT(Clay_Vector2)CLAY__DEFAULT_STRUCT;
+            if (!highestPriorityScrollData->pointerScrollActive) {
+                highestPriorityScrollData->pointerOrigin = context->pointerInfo.position;
+                highestPriorityScrollData->scrollOrigin = highestPriorityScrollData->scrollPosition;
+                highestPriorityScrollData->pointerScrollActive = true;
+            } else {
+                float scrollDeltaX = 0, scrollDeltaY = 0;
+                if (canScrollHorizontally) {
+                    float oldXScrollPosition = highestPriorityScrollData->scrollPosition.x;
+                    highestPriorityScrollData->scrollPosition.x = highestPriorityScrollData->scrollOrigin.x + (context->pointerInfo.position.x - highestPriorityScrollData->pointerOrigin.x);
+                    highestPriorityScrollData->scrollPosition.x = CLAY__MAX(CLAY__MIN(highestPriorityScrollData->scrollPosition.x, 0), -(highestPriorityScrollData->contentSize.width - highestPriorityScrollData->boundingBox.width));
+                    scrollDeltaX = highestPriorityScrollData->scrollPosition.x - oldXScrollPosition;
+                }
+                if (canScrollVertically) {
+                    float oldYScrollPosition = highestPriorityScrollData->scrollPosition.y;
+                    highestPriorityScrollData->scrollPosition.y = highestPriorityScrollData->scrollOrigin.y + (context->pointerInfo.position.y - highestPriorityScrollData->pointerOrigin.y);
+                    highestPriorityScrollData->scrollPosition.y = CLAY__MAX(CLAY__MIN(highestPriorityScrollData->scrollPosition.y, 0), -(highestPriorityScrollData->contentSize.height - highestPriorityScrollData->boundingBox.height));
+                    scrollDeltaY = highestPriorityScrollData->scrollPosition.y - oldYScrollPosition;
+                }
+                if (scrollDeltaX > -0.1f && scrollDeltaX < 0.1f && scrollDeltaY > -0.1f && scrollDeltaY < 0.1f && highestPriorityScrollData->momentumTime > 0.15f) {
+                    highestPriorityScrollData->momentumTime = 0;
+                    highestPriorityScrollData->pointerOrigin = context->pointerInfo.position;
+                    highestPriorityScrollData->scrollOrigin = highestPriorityScrollData->scrollPosition;
+                } else {
+                     highestPriorityScrollData->momentumTime += deltaTime;
+                }
+            }
+        }
+        // Clamp any changes to scroll position to the maximum size of the contents
+        if (canScrollVertically) {
+            highestPriorityScrollData->scrollPosition.y = CLAY__MAX(CLAY__MIN(highestPriorityScrollData->scrollPosition.y, 0), -(highestPriorityScrollData->contentSize.height - scrollElement->dimensions.height));
+        }
+        if (canScrollHorizontally) {
+            highestPriorityScrollData->scrollPosition.x = CLAY__MAX(CLAY__MIN(highestPriorityScrollData->scrollPosition.x, 0), -(highestPriorityScrollData->contentSize.width - scrollElement->dimensions.width));
+        }
+    }
+}
+
+CLAY_WASM_EXPORT("Clay_BeginLayout")
+void Clay_BeginLayout(void) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    Clay__InitializeEphemeralMemory(context);
+    context->generation++;
+    context->dynamicElementIndex = 0;
+    // Set up the root container that covers the entire window
+    Clay_Dimensions rootDimensions = {context->layoutDimensions.width, context->layoutDimensions.height};
+    if (context->debugModeEnabled) {
+        rootDimensions.width -= (float)Clay__debugViewWidth;
+    }
+    context->booleanWarnings = CLAY__INIT(Clay_BooleanWarnings) CLAY__DEFAULT_STRUCT;
+    Clay__OpenElementWithId(CLAY_ID("Clay__RootContainer"));
+    Clay__ConfigureOpenElement(CLAY__INIT(Clay_ElementDeclaration) {
+        .layout = { .sizing = {CLAY_SIZING_FIXED((rootDimensions.width)), CLAY_SIZING_FIXED(rootDimensions.height)} }
+    });
+    Clay__int32_tArray_Add(&context->openLayoutElementStack, 0);
+    Clay__LayoutElementTreeRootArray_Add(&context->layoutElementTreeRoots, CLAY__INIT(Clay__LayoutElementTreeRoot) { .layoutElementIndex = 0 });
+}
+
+void Clay__CloneElementsWithExitTransition() {
+    Clay_Context* context = Clay_GetCurrentContext();
+    int32_t nextIndex = context->layoutElements.capacity - 1;
+    int32_t nextChildIndex = context->layoutElementChildren.capacity - 1;
+
+    for (int i = 0; i < context->transitionDatas.length; ++i) {
+        Clay__TransitionDataInternal *data = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i);
+        Clay_TransitionElementConfig* config = &data->elementThisFrame->config.transition;
+        if (data->transitionOut) {
+            Clay__int32_tArray bfsBuffer = context->openLayoutElementStack;
+            bfsBuffer.length = 0;
+            Clay_LayoutElement* newElement = Clay_LayoutElementArray_Set_DontTouchLength(&context->layoutElements, nextIndex, *data->elementThisFrame);
+            Clay__StringArray_Set_DontTouchLength(&context->layoutElementIdStrings, nextIndex, *Clay__StringArray_GetCheckCapacity(&context->layoutElementIdStrings, data->elementThisFrame - context->layoutElements.internalArray));
+            Clay__int32_tArray_Add(&bfsBuffer, nextIndex);
+            data->elementThisFrame = newElement;
+            nextIndex--;
+
+            int32_t bufferIndex = 0;
+            while(bufferIndex < bfsBuffer.length) {
+                Clay_LayoutElement *layoutElement = Clay_LayoutElementArray_GetCheckCapacity(&context->layoutElements, Clay__int32_tArray_GetValue(&bfsBuffer, bufferIndex));
+                bufferIndex++;
+                for (int j = layoutElement->children.length - 1; j >= 0; --j) {
+                    Clay_LayoutElement* childElement = Clay_LayoutElementArray_GetCheckCapacity(&context->layoutElements, layoutElement->children.elements[j]);
+                    Clay__int32_tArray_Add(&bfsBuffer, nextIndex);
+                    Clay_LayoutElement* newChildElement = Clay_LayoutElementArray_Set_DontTouchLength(&context->layoutElements, nextIndex, *childElement);
+                    Clay__StringArray_Set_DontTouchLength(&context->layoutElementIdStrings, nextIndex, *Clay__StringArray_GetCheckCapacity(&context->layoutElementIdStrings, childElement - context->layoutElements.internalArray));
+                    Clay__int32_tArray_Set_DontTouchLength(&context->layoutElementChildren, nextChildIndex, nextIndex);
+                    nextIndex--;
+                    nextChildIndex--;
+                }
+                layoutElement->children.elements = &context->layoutElementChildren.internalArray[nextChildIndex + 1];
+            }
+        }
+    }
+};
+
+void Clay_ApplyTransitionedPropertiesToElement(Clay_LayoutElement* currentElement, Clay_TransitionProperty properties, Clay_TransitionData currentTransitionData, Clay_BoundingBox* boundingBox, bool reparented) {
+    if (properties & CLAY_TRANSITION_PROPERTY_WIDTH) {
+        if (!reparented) {
+            currentElement->dimensions.width = currentTransitionData.boundingBox.width;
+            currentElement->config.layout.sizing.width = CLAY_SIZING_FIXED(currentTransitionData.boundingBox.width);
+        } else {
+            boundingBox->width = currentTransitionData.boundingBox.width;
+        }
+    }
+    if (properties & CLAY_TRANSITION_PROPERTY_HEIGHT) {
+        if (!reparented) {
+            currentElement->dimensions.height = currentTransitionData.boundingBox.height;
+            currentElement->config.layout.sizing.height = CLAY_SIZING_FIXED(currentTransitionData.boundingBox.height);
+        } else {
+            boundingBox->height = currentTransitionData.boundingBox.height;
+        }
+    }
+    if (properties & CLAY_TRANSITION_PROPERTY_X) {
+        boundingBox->x = currentTransitionData.boundingBox.x;
+    }
+    if (properties & CLAY_TRANSITION_PROPERTY_Y) {
+        boundingBox->y = currentTransitionData.boundingBox.y;
+    }
+    if (properties & CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR) {
+        currentElement->config.overlayColor = currentTransitionData.overlayColor;
+    }
+    if (properties & CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR) {
+        currentElement->config.backgroundColor = currentTransitionData.backgroundColor;
+    }
+    if (properties & CLAY_TRANSITION_PROPERTY_BORDER_COLOR) {
+        currentElement->config.border.color = currentTransitionData.borderColor;
+    }
+    if (properties & CLAY_TRANSITION_PROPERTY_BORDER_WIDTH) {
+        currentElement->config.border.width = currentTransitionData.borderWidth;
+    }
+}
+
+CLAY_WASM_EXPORT("Clay_EndLayout")
+Clay_RenderCommandArray Clay_EndLayout(float deltaTime) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    Clay__CloseElement();
+
+    if (context->openLayoutElementStack.length > 1) {
+        context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+            .errorType = CLAY_ERROR_TYPE_UNBALANCED_OPEN_CLOSE,
+            .errorText = CLAY_STRING("There were still open layout elements when EndLayout was called. This results from an unequal number of calls to Clay__OpenElement and Clay__CloseElement."),
+            .userData = context->errorHandler.userData });
+    }
+
+    // Prune non exiting transitions
+    for (int i = 0; i < context->transitionDatas.length; ++i) {
+        Clay__TransitionDataInternal *data = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i);
+        Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(data->elementId);
+        // Transition element exited and doesn't have an exit handler defined
+        // Or, the user deleted the transition handler from one frame to the next
+        if (!data->transitionOut && (hashMapItem->generation <= context->generation || !hashMapItem->layoutElement->config.transition.handler)) {
+            Clay__TransitionDataInternalArray_RemoveSwapback(&context->transitionDatas, i);
+            i--;
+            continue;
+        }
+    }
+
+    Clay__int32_tArray elementIdsToRemoveTransitions = context->reusableElementIndexBuffer;
+    elementIdsToRemoveTransitions.length = 0;
+
+    for (int i = 0; i < context->transitionDatas.length; ++i) {
+        Clay__TransitionDataInternal *data = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i);
+        Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(data->elementId);
+        // This might seems strange - can't we just look up the element itself, and check the config to see whether it has an exit transition defined?
+        // That would work fine if the element actually had an exit transition in the first place. If it doesn't have an exit transition defined, the element
+        // will have simply disappeared completely at this point, and there will be no element through which to access the config.
+        if (data->transitionOut) {
+            Clay_TransitionElementConfig* config = &data->elementThisFrame->config.transition;
+            // Element wasn't found this frame - either delete transition data or transition out
+            if (hashMapItem->generation <= context->generation) {
+                Clay_LayoutElementHashMapItem *parentHashMapItem = Clay__GetHashMapItem(data->parentId);
+                // Don't exit transition if the parent has also exited and SKIP_WHEN_PARENT_EXITS is used
+                if (config->exit.trigger == CLAY_TRANSITION_EXIT_TRIGGER_WHEN_PARENT_EXITS || !parentHashMapItem || parentHashMapItem->generation > context->generation) {
+                    // This if only runs one single time when the element first starts exiting
+                    if (data->state != CLAY_TRANSITION_STATE_EXITING) {
+                        if (parentHashMapItem->generation <= context->generation) {
+                            data->elementThisFrame->config.floating.attachTo = CLAY_ATTACH_TO_ROOT;
+                            data->elementThisFrame->config.floating.offset = CLAY__INIT(Clay_Vector2) { hashMapItem->boundingBox.x, hashMapItem->boundingBox.y };
+                            data->elementThisFrame->config.floating.parentId = Clay__HashString(CLAY_STRING("Clay__RootContainer"), 0).id;
+                        }
+                        hashMapItem->appearedThisFrame = false;
+                        data->elementThisFrame->exiting = true;
+                        data->elementThisFrame->config.layout.sizing.width = CLAY_SIZING_FIXED(data->elementThisFrame->dimensions.width);
+                        data->elementThisFrame->config.layout.sizing.height = CLAY_SIZING_FIXED(data->elementThisFrame->dimensions.height);
+                        data->state = CLAY_TRANSITION_STATE_EXITING;
+                        data->activeProperties = config->properties;
+                        data->elapsedTime = 0;
+                        data->targetState = config->exit.setFinalState(data->targetState, config->properties);
+                    }
+
+                    // Below this line runs every frame while element is exiting -----------
+
+                    // Clone the entire subtree back into the main UI layout tree
+                    Clay__int32_tArray bfsBuffer = context->openLayoutElementStack;
+                    bfsBuffer.length = 0;
+                    data->elementThisFrame = Clay_LayoutElementArray_Add(&context->layoutElements, *data->elementThisFrame);
+                    int32_t exitingElementIndex = data->elementThisFrame - context->layoutElements.internalArray;
+                    Clay__StringArray_Add(&context->layoutElementIdStrings, *Clay__StringArray_GetCheckCapacity(&context->layoutElementIdStrings, exitingElementIndex));
+                    Clay__int32_tArray_Add(&context->layoutElementClipElementIds, *Clay__int32_tArray_GetCheckCapacity(&context->layoutElementClipElementIds, exitingElementIndex));
+                    Clay__int32_tArray_Add(&bfsBuffer, exitingElementIndex);
+                    int32_t bufferIndex = 0;
+                    while (bufferIndex < bfsBuffer.length) {
+                        Clay_LayoutElement *layoutElement = Clay_LayoutElementArray_GetCheckCapacity(&context->layoutElements, Clay__int32_tArray_GetValue(&bfsBuffer, bufferIndex));
+                        Clay_LayoutElementHashMapItem* bfsMapItem = Clay__GetHashMapItem(layoutElement->id);
+                        // Children of exiting elements may have been moved elsewhere in the layout, this prevents a duplicate ID error if they still exist.
+                        if (bfsMapItem->generation <= context->generation) {
+                            Clay__AddHashMapItem(CLAY__INIT(Clay_ElementId){ layoutElement->id }, layoutElement);
+                            int32_t firstChildSlot = context->layoutElementChildren.length;
+                            uint16_t newChildrenLength = layoutElement->children.length;
+                            for (int j = 0; j < layoutElement->children.length; ++j) {
+                                Clay_LayoutElement* childElement = Clay_LayoutElementArray_GetCheckCapacity(&context->layoutElements, layoutElement->children.elements[j]);
+                                Clay_LayoutElementHashMapItem* childMapItem = Clay__GetHashMapItem(childElement->id);
+                                if (childMapItem->generation <= context->generation) {
+                                    // Remove any nested transitions inside exiting trees
+                                    if (!childElement->isTextElement && childElement->config.transition.handler) {
+                                        Clay__int32_tArray_Add(&elementIdsToRemoveTransitions, childElement->id);
+                                    }
+                                    int32_t childElementIndex = childElement - context->layoutElements.internalArray;
+                                    Clay_LayoutElement* newChildElement = Clay_LayoutElementArray_Add(&context->layoutElements, *childElement);
+                                    Clay__StringArray_Add(&context->layoutElementIdStrings, *Clay__StringArray_GetCheckCapacity(&context->layoutElementIdStrings, childElementIndex));
+                                    Clay__int32_tArray_Add(&context->layoutElementClipElementIds, *Clay__int32_tArray_GetCheckCapacity(&context->layoutElementClipElementIds, childElementIndex));
+                                    Clay__int32_tArray_Add(&bfsBuffer, context->layoutElements.length - 1);
+                                    if (newChildElement->isTextElement) {
+                                        newChildElement->textElementData.wrappedLines.length = 0;
+                                    }
+                                    Clay__int32_tArray_Add(&context->layoutElementChildren, context->layoutElements.length - 1);
+                                } else {
+                                    newChildrenLength--;
+                                }
+                            }
+                            layoutElement->children = CLAY__INIT(Clay__LayoutElementChildren) {
+                                .elements = &context->layoutElementChildren.internalArray[firstChildSlot],
+                                .length = newChildrenLength,
+                            };
+                        }
+                        bufferIndex++;
+                    }
+                    hashMapItem->layoutElement = data->elementThisFrame;
+
+                    // Reattach the inserted subtree to its previous parent if it still exists
+                    // and the exiting element is not floating
+                    Clay_FloatingElementConfig* floatingConfig = &hashMapItem->layoutElement->config.floating;
+                    if (parentHashMapItem->generation > context->generation && floatingConfig->attachTo == CLAY_ATTACH_TO_NONE) {
+                        Clay_LayoutElement *parentElement = parentHashMapItem->layoutElement;
+                        int32_t newChildrenStartIndex = context->layoutElementChildren.length;
+                        bool found = false;
+                        if (config->exit.siblingOrdering == CLAY_EXIT_TRANSITION_ORDERING_UNDERNEATH_SIBLINGS) {
+                            Clay__int32_tArray_Add(&context->layoutElementChildren, exitingElementIndex);
+                            found = true;
+                        }
+                        for (int j = 0; j < parentElement->children.length; ++j) {
+                            if (config->exit.siblingOrdering == CLAY_EXIT_TRANSITION_ORDERING_NATURAL_ORDER && j == data->siblingIndex) {
+                                Clay__int32_tArray_Add(&context->layoutElementChildren, exitingElementIndex);
+                                found = true;
+                            }
+                            Clay__int32_tArray_Add(&context->layoutElementChildren, parentElement->children.elements[j]);
+                        }
+                        if (!found) {
+                            Clay__int32_tArray_Add(&context->layoutElementChildren, exitingElementIndex);
+                        }
+                        parentElement->children.length++;
+                        parentElement->children.elements = &context->layoutElementChildren.internalArray[newChildrenStartIndex];
+                    // Otherwise, create the tree root for the floating element (needs to be created every frame)
+                    } else {
+                        Clay__LayoutElementTreeRootArray_Add(&context->layoutElementTreeRoots, CLAY__INIT(Clay__LayoutElementTreeRoot) {
+                            .layoutElementIndex = (int32_t)(data->elementThisFrame - context->layoutElements.internalArray),
+                            .parentId = floatingConfig->parentId,
+                            .zIndex = floatingConfig->zIndex,
+                        });
+                    }
+                // Parent exited, just delete child without exit transition
+                } else {
+                    Clay__TransitionDataInternalArray_RemoveSwapback(&context->transitionDatas, i);
+                    i--;
+                    continue;
+                }
+            }
+        }
+    }
+
+    for (int i = 0; i < elementIdsToRemoveTransitions.length; ++i) {
+        for (int j = 0; j < context->transitionDatas.length; ++j) {
+            if (Clay__TransitionDataInternalArray_Get(&context->transitionDatas, j)->elementId == Clay__int32_tArray_GetValue(&elementIdsToRemoveTransitions, i)) {
+                Clay__TransitionDataInternalArray_RemoveSwapback(&context->transitionDatas, j);
+                break;
+            }
+        }
+    }
+
+    if (context->booleanWarnings.maxElementsExceeded) {
+        Clay_String message;
+        message = CLAY_STRING("Clay Error: Layout elements exceeded Clay__maxElementCount");
+        Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand ) {
+            .boundingBox = { context->layoutDimensions.width / 2 - 59 * 4, context->layoutDimensions.height / 2, 0, 0 },
+            .renderData = { .text = { .stringContents = CLAY__INIT(Clay_StringSlice) { .length = message.length, .chars = message.chars, .baseChars = message.chars }, .textColor = {255, 0, 0, 255}, .fontSize = 16 } },
+            .commandType = CLAY_RENDER_COMMAND_TYPE_TEXT
+        });
+    } else {
+        if (context->transitionDatas.length > 0) {
+            Clay__CalculateFinalLayout(deltaTime, false, false);
+
+            for (int i = 0; i < context->transitionDatas.length; ++i) {
+                Clay__TransitionDataInternal* transitionData = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i);
+                Clay_LayoutElement* currentElement = transitionData->elementThisFrame;
+                Clay_LayoutElementHashMapItem* mapItem = Clay__GetHashMapItem(transitionData->elementId);
+                Clay_LayoutElementHashMapItem* parentMapItem = Clay__GetHashMapItem(transitionData->parentId);
+                Clay_TransitionData targetState = transitionData->targetState;
+                if (transitionData->state != CLAY_TRANSITION_STATE_EXITING) {
+                    targetState = CLAY__INIT(Clay_TransitionData) {
+                            mapItem->boundingBox,
+                            currentElement->config.backgroundColor,
+                            currentElement->config.overlayColor,
+                            currentElement->config.border.color,
+                            currentElement->config.border.width,
+                    };
+                }
+                Clay_TransitionData oldTargetState = transitionData->targetState;
+                transitionData->targetState = targetState;
+                if (mapItem->appearedThisFrame) {
+                    if (currentElement->config.transition.enter.setInitialState && !(parentMapItem->appearedThisFrame && currentElement->config.transition.enter.trigger == CLAY_TRANSITION_ENTER_SKIP_ON_FIRST_PARENT_FRAME)) {
+                        transitionData->state = CLAY_TRANSITION_STATE_ENTERING;
+                        transitionData->initialState = currentElement->config.transition.enter.setInitialState(transitionData->targetState, currentElement->config.transition.properties);
+                        transitionData->currentState = transitionData->initialState;
+                        transitionData->activeProperties = currentElement->config.transition.properties;
+                        Clay_ApplyTransitionedPropertiesToElement(currentElement, currentElement->config.transition.properties, transitionData->initialState, &mapItem->boundingBox, transitionData->reparented);
+                    } else {
+                        transitionData->initialState = targetState;
+                        transitionData->currentState = targetState;
+                        transitionData->activeProperties = CLAY_TRANSITION_PROPERTY_NONE;
+                    }
+                } else {
+                    if (transitionData->state != CLAY_TRANSITION_STATE_EXITING) {
+                        Clay_Vector2 parentScrollOffset = parentMapItem->layoutElement->config.clip.childOffset;
+                        Clay_Vector2 newRelativePosition = {
+                            mapItem->boundingBox.x - parentMapItem->boundingBox.x - parentScrollOffset.x,
+                            mapItem->boundingBox.y - parentMapItem->boundingBox.y - parentScrollOffset.y,
+                        };
+                        Clay_Vector2 oldRelativePosition = transitionData->oldParentRelativePosition;
+                        transitionData->oldParentRelativePosition = newRelativePosition;
+                        Clay_TransitionProperty properties = currentElement->config.transition.properties;
+                        int32_t newActiveProperties = CLAY_TRANSITION_PROPERTY_NONE;
+                        if (properties & CLAY_TRANSITION_PROPERTY_X) {
+                            // Don't trigger a transition if...
+                            if (
+                                // The element's absolute position didn't change
+                                !Clay__FloatEqual(oldTargetState.boundingBox.x, targetState.boundingBox.x)
+                                // The element is still in the same parent container, and it's parent-relative position didn't change (parent moved)
+                                && (!(Clay__FloatEqual(oldRelativePosition.x, newRelativePosition.x)) || transitionData->reparented)
+                                // The position changed was triggered by the outer window resizing
+                                && !context->rootResizedLastFrame
+                            ) {
+                                newActiveProperties |= CLAY_TRANSITION_PROPERTY_X;
+                            }
+                        }
+                        if (properties & CLAY_TRANSITION_PROPERTY_Y) {
+                            // See extended comments above in PROPERTY_X for explanation
+                            if (!Clay__FloatEqual(oldTargetState.boundingBox.y, targetState.boundingBox.y) && (!(Clay__FloatEqual(oldRelativePosition.y, newRelativePosition.y)) || transitionData->reparented) && !context->rootResizedLastFrame) {
+                                newActiveProperties |= CLAY_TRANSITION_PROPERTY_Y;
+                            }
+                        }
+                        if (properties & CLAY_TRANSITION_PROPERTY_WIDTH) {
+                            if (!Clay__FloatEqual(oldTargetState.boundingBox.width, targetState.boundingBox.width) && !context->rootResizedLastFrame) {
+                                newActiveProperties |= CLAY_TRANSITION_PROPERTY_WIDTH;
+                            }
+                        }
+                        if (properties & CLAY_TRANSITION_PROPERTY_HEIGHT) {
+                            if (!Clay__FloatEqual(oldTargetState.boundingBox.height, targetState.boundingBox.height) && !context->rootResizedLastFrame) {
+                                newActiveProperties |= CLAY_TRANSITION_PROPERTY_HEIGHT;
+                            }
+                        }
+                        if (properties & CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR) {
+                            if (!Clay__MemCmp((char *) &oldTargetState.backgroundColor, (char *)&targetState.backgroundColor, sizeof(Clay_Color))) {
+                                newActiveProperties |= CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR;
+                            }
+                        }
+                        if (properties & CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR) {
+                            if (!Clay__MemCmp((char *) &oldTargetState.overlayColor, (char *)&targetState.overlayColor, sizeof(Clay_Color))) {
+                                newActiveProperties |= CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR;
+                            }
+                        }
+                        if (properties & CLAY_TRANSITION_PROPERTY_BORDER_COLOR) {
+                            if (!Clay__MemCmp((char *) &oldTargetState.borderColor, (char *)&targetState.borderColor, sizeof(Clay_Color))) {
+                                newActiveProperties |= CLAY_TRANSITION_PROPERTY_BORDER_COLOR;
+                            }
+                        }
+                        if (properties & CLAY_TRANSITION_PROPERTY_BORDER_WIDTH) {
+                            if (!Clay__MemCmp((char *) &oldTargetState.borderWidth, (char *)&targetState.borderWidth, sizeof(Clay_BorderWidth))) {
+                                newActiveProperties |= CLAY_TRANSITION_PROPERTY_BORDER_WIDTH;
+                            }
+                        }
+
+                        if (newActiveProperties != 0) {
+                            transitionData->elapsedTime = 0;
+                            transitionData->initialState = transitionData->currentState;
+                            transitionData->state = CLAY_TRANSITION_STATE_TRANSITIONING;
+                            transitionData->activeProperties = (Clay_TransitionProperty)(transitionData->activeProperties | newActiveProperties);
+                        }
+                    }
+
+                    if (transitionData->state == CLAY_TRANSITION_STATE_IDLE) {
+                        transitionData->initialState = targetState;
+                        transitionData->currentState = targetState;
+                        transitionData->targetState = targetState;
+                        transitionData->activeProperties = CLAY_TRANSITION_PROPERTY_NONE;
+                    } else {
+                        bool transitionComplete = true;
+                        transitionComplete = currentElement->config.transition.handler(CLAY__INIT(Clay_TransitionCallbackArguments) {
+                            transitionData->state,
+                            transitionData->initialState,
+                            &transitionData->currentState,
+                            targetState,
+                            transitionData->elapsedTime,
+                            currentElement->config.transition.duration,
+                            transitionData->activeProperties
+                        });
+
+                        Clay_ApplyTransitionedPropertiesToElement(currentElement, transitionData->activeProperties, transitionData->currentState, &mapItem->boundingBox, transitionData->reparented);
+                        transitionData->elapsedTime += deltaTime;
+
+                        if (transitionComplete) {
+                            if (transitionData->state == CLAY_TRANSITION_STATE_ENTERING || transitionData->state == CLAY_TRANSITION_STATE_TRANSITIONING) {transitionData->state = CLAY_TRANSITION_STATE_IDLE;
+                                transitionData->elapsedTime = 0;
+                                transitionData->reparented = false;
+                                transitionData->activeProperties = CLAY_TRANSITION_PROPERTY_NONE;
+                            } else if (transitionData->state == CLAY_TRANSITION_STATE_EXITING) {
+                                Clay__TransitionDataInternalArray_RemoveSwapback(&context->transitionDatas, i);
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (context->debugModeEnabled) {
+                context->warningsEnabled = false;
+                Clay__RenderDebugView();
+                context->warningsEnabled = true;
+            }
+
+            if (context->booleanWarnings.maxElementsExceeded) {
+                Clay_String message;
+                message = CLAY_STRING("Clay Error: Debug view caused layout element count to exceed Clay__maxElementCount");
+                Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand ) {
+                        .boundingBox = { context->layoutDimensions.width / 2 - 59 * 4, context->layoutDimensions.height / 2, 0, 0 },
+                        .renderData = { .text = { .stringContents = CLAY__INIT(Clay_StringSlice) { .length = message.length, .chars = message.chars, .baseChars = message.chars }, .textColor = {255, 0, 0, 255}, .fontSize = 16 } },
+                        .commandType = CLAY_RENDER_COMMAND_TYPE_TEXT
+                });
+            } else {
+                Clay__CalculateFinalLayout(deltaTime, true, true);
+                Clay__CloneElementsWithExitTransition();
+            }
+        } else {
+            if (context->debugModeEnabled) {
+                context->warningsEnabled = false;
+                Clay__RenderDebugView();
+                context->warningsEnabled = true;
+            }
+
+            if (context->booleanWarnings.maxElementsExceeded) {
+                Clay_String message;
+                message = CLAY_STRING("Clay Error: Debug view caused layout element count to exceed Clay__maxElementCount");
+                Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand ) {
+                    .boundingBox = { context->layoutDimensions.width / 2 - 59 * 4, context->layoutDimensions.height / 2, 0, 0 },
+                    .renderData = { .text = { .stringContents = CLAY__INIT(Clay_StringSlice) { .length = message.length, .chars = message.chars, .baseChars = message.chars }, .textColor = {255, 0, 0, 255}, .fontSize = 16 } },
+                    .commandType = CLAY_RENDER_COMMAND_TYPE_TEXT
+                });
+            } else {
+                Clay__CalculateFinalLayout(deltaTime, false, true);
+            }
+        }
+    }
+
+    for (int i = 0; i < context->layoutElementsHashMap.capacity; ++i) {
+        int32_t currentElementIndex = context->layoutElementsHashMap.internalArray[i];
+        int32_t previousElementIndex = -1;
+        while (currentElementIndex != -1) {
+            Clay_LayoutElementHashMapItem* currentItem = Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, currentElementIndex);
+            int32_t nextIndex = currentItem->nextIndex;
+            // Needs to be pruned
+            if (currentItem->generation <= context->generation) {
+                // Delete the underlying item and add it to the freelist
+                Clay__LayoutElementHashMapItemArray_Set(&context->layoutElementsHashMapInternal, currentElementIndex, CLAY__INIT(Clay_LayoutElementHashMapItem) { .nextIndex = -1 });
+                Clay__int32_tArray_Add(&context->layoutElementsHashMapFreeList, currentElementIndex);
+                // If it's the very top of the bucket, rewrite the first bucket pointer
+                if (previousElementIndex == -1) {
+                    Clay__int32_tArray_Set(&context->layoutElementsHashMap, i, nextIndex);
+                    currentElementIndex = nextIndex;
+                    previousElementIndex = -1;
+                } else {
+                    // Rewrite previous pointer
+                    Clay_LayoutElementHashMapItem* previousItem = Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, previousElementIndex);
+                    previousItem->nextIndex = nextIndex;
+                    currentElementIndex = nextIndex;
+                }
+            } else {
+                previousElementIndex = currentElementIndex;
+                currentElementIndex = nextIndex;
+            }
+        }
+    }
+
+    return context->renderCommands;
+}
+
+CLAY_WASM_EXPORT("Clay_GetOpenElementId")
+uint32_t Clay_GetOpenElementId(void) {
+    return Clay__GetOpenLayoutElement()->id;
+}
+
+CLAY_WASM_EXPORT("Clay_GetElementId")
+Clay_ElementId Clay_GetElementId(Clay_String idString) {
+    return Clay__HashString(idString, 0);
+}
+
+CLAY_WASM_EXPORT("Clay_GetElementIdWithIndex")
+Clay_ElementId Clay_GetElementIdWithIndex(Clay_String idString, uint32_t index) {
+    return Clay__HashStringWithOffset(idString, index, 0);
+}
+
+bool Clay_Hovered(void) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    if (context->booleanWarnings.maxElementsExceeded) {
+        return false;
+    }
+    Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement();
+    for (int32_t i = 0; i < context->pointerOverIds.length; ++i) {
+        if (Clay_ElementIdArray_Get(&context->pointerOverIds, i)->id == openLayoutElement->id) {
+            return true;
+        }
+    }
+    return false;
+}
+
+void Clay_OnHover(void (*onHoverFunction)(Clay_ElementId elementId, Clay_PointerData pointerInfo, void *userData), void *userData) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    if (context->booleanWarnings.maxElementsExceeded) {
+        return;
+    }
+    Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement();
+    Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(openLayoutElement->id);
+    hashMapItem->onHoverFunction = onHoverFunction;
+    hashMapItem->hoverFunctionUserData = userData;
+}
+
+CLAY_WASM_EXPORT("Clay_PointerOver")
+bool Clay_PointerOver(Clay_ElementId elementId) { // TODO return priority for separating multiple results
+    Clay_Context* context = Clay_GetCurrentContext();
+    for (int32_t i = 0; i < context->pointerOverIds.length; ++i) {
+        if (Clay_ElementIdArray_Get(&context->pointerOverIds, i)->id == elementId.id) {
+            return true;
+        }
+    }
+    return false;
+}
+
+CLAY_WASM_EXPORT("Clay_GetScrollContainerData")
+Clay_ScrollContainerData Clay_GetScrollContainerData(Clay_ElementId id) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    for (int32_t i = 0; i < context->scrollContainerDatas.length; ++i) {
+        Clay__ScrollContainerDataInternal *scrollContainerData = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i);
+        if (scrollContainerData->elementId == id.id) {
+            if (!scrollContainerData->layoutElement) { // This can happen on the first frame before a scroll container is declared
+                return CLAY__INIT(Clay_ScrollContainerData) CLAY__DEFAULT_STRUCT;
+            }
+            return CLAY__INIT(Clay_ScrollContainerData) {
+                .scrollPosition = &scrollContainerData->scrollPosition,
+                .scrollContainerDimensions = { scrollContainerData->boundingBox.width, scrollContainerData->boundingBox.height },
+                .contentDimensions = scrollContainerData->contentSize,
+                .config = scrollContainerData->layoutElement->config.clip,
+                .found = true
+            };
+        }
+    }
+    return CLAY__INIT(Clay_ScrollContainerData) CLAY__DEFAULT_STRUCT;
+}
+
+CLAY_WASM_EXPORT("Clay_GetElementData")
+Clay_ElementData Clay_GetElementData(Clay_ElementId id){
+    Clay_LayoutElementHashMapItem * item = Clay__GetHashMapItem(id.id);
+    if(item == &Clay_LayoutElementHashMapItem_DEFAULT) {
+        return CLAY__INIT(Clay_ElementData) CLAY__DEFAULT_STRUCT;
+    }
+
+    return CLAY__INIT(Clay_ElementData){
+        .boundingBox = item->boundingBox,
+        .found = true
+    };
+}
+
+CLAY_WASM_EXPORT("Clay_SetDebugModeEnabled")
+void Clay_SetDebugModeEnabled(bool enabled) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    context->debugModeEnabled = enabled;
+}
+
+CLAY_WASM_EXPORT("Clay_IsDebugModeEnabled")
+bool Clay_IsDebugModeEnabled(void) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    return context->debugModeEnabled;
+}
+
+CLAY_WASM_EXPORT("Clay_SetCullingEnabled")
+void Clay_SetCullingEnabled(bool enabled) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    context->disableCulling = !enabled;
+}
+
+CLAY_WASM_EXPORT("Clay_SetExternalScrollHandlingEnabled")
+void Clay_SetExternalScrollHandlingEnabled(bool enabled) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    context->externalScrollHandlingEnabled = enabled;
+}
+
+CLAY_WASM_EXPORT("Clay_GetMaxElementCount")
+int32_t Clay_GetMaxElementCount(void) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    return context->maxElementCount;
+}
+
+CLAY_WASM_EXPORT("Clay_SetMaxElementCount")
+void Clay_SetMaxElementCount(int32_t maxElementCount) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    if (context) {
+        context->maxElementCount = maxElementCount;
+    } else {
+        Clay__defaultMaxElementCount = maxElementCount; // TODO: Fix this
+        Clay__defaultMaxMeasureTextWordCacheCount = maxElementCount * 2;
+    }
+}
+
+CLAY_WASM_EXPORT("Clay_GetMaxMeasureTextCacheWordCount")
+int32_t Clay_GetMaxMeasureTextCacheWordCount(void) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    return context->maxMeasureTextCacheWordCount;
+}
+
+CLAY_WASM_EXPORT("Clay_SetMaxMeasureTextCacheWordCount")
+void Clay_SetMaxMeasureTextCacheWordCount(int32_t maxMeasureTextCacheWordCount) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    if (context) {
+        Clay__currentContext->maxMeasureTextCacheWordCount = maxMeasureTextCacheWordCount;
+    } else {
+        Clay__defaultMaxMeasureTextWordCacheCount = maxMeasureTextCacheWordCount; // TODO: Fix this
+    }
+}
+
+CLAY_WASM_EXPORT("Clay_ResetMeasureTextCache")
+void Clay_ResetMeasureTextCache(void) {
+    Clay_Context* context = Clay_GetCurrentContext();
+    context->measureTextHashMapInternal.length = 0;
+    context->measureTextHashMapInternalFreeList.length = 0;
+    context->measureTextHashMap.length = 0;
+    context->measuredWords.length = 0;
+    context->measuredWordsFreeList.length = 0;
+
+    for (int32_t i = 0; i < context->measureTextHashMap.capacity; ++i) {
+        context->measureTextHashMap.internalArray[i] = 0;
+    }
+    context->measureTextHashMapInternal.length = 1; // Reserve the 0 value to mean "no next element"
+}
+
+#define CLAY__LERP(from, to, mix) (from + (to - from) * mix)
+
+CLAY_DLL_EXPORT bool Clay_EaseOut(Clay_TransitionCallbackArguments arguments) {
+    float ratio = 1;
+    if (arguments.duration > 0) {
+        ratio = CLAY__MIN(arguments.elapsedTime / arguments.duration, 1);
+    }
+    float inverse = 1.0f - ratio;
+    float lerpAmount = 1.0f - (inverse * inverse * inverse);
+    if (arguments.properties & CLAY_TRANSITION_PROPERTY_X) {
+        arguments.current->boundingBox.x = CLAY__LERP(arguments.initial.boundingBox.x, arguments.target.boundingBox.x, lerpAmount);
+    }
+    if (arguments.properties & CLAY_TRANSITION_PROPERTY_Y) {
+        arguments.current->boundingBox.y = CLAY__LERP(arguments.initial.boundingBox.y, arguments.target.boundingBox.y, lerpAmount);
+    }
+    if (arguments.properties & CLAY_TRANSITION_PROPERTY_WIDTH) {
+        arguments.current->boundingBox.width = CLAY__LERP(arguments.initial.boundingBox.width, arguments.target.boundingBox.width, lerpAmount);
+    }
+    if (arguments.properties & CLAY_TRANSITION_PROPERTY_HEIGHT) {
+        arguments.current->boundingBox.height = CLAY__LERP(arguments.initial.boundingBox.height, arguments.target.boundingBox.height, lerpAmount);
+    }
+    if (arguments.properties & CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR) {
+        arguments.current->backgroundColor = CLAY__INIT(Clay_Color) {
+            .r = CLAY__LERP(arguments.initial.backgroundColor.r, arguments.target.backgroundColor.r, lerpAmount),
+            .g = CLAY__LERP(arguments.initial.backgroundColor.g, arguments.target.backgroundColor.g, lerpAmount),
+            .b = CLAY__LERP(arguments.initial.backgroundColor.b, arguments.target.backgroundColor.b, lerpAmount),
+            .a = CLAY__LERP(arguments.initial.backgroundColor.a, arguments.target.backgroundColor.a, lerpAmount),
+        };
+    }
+    if (arguments.properties & CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR) {
+        arguments.current->overlayColor = CLAY__INIT(Clay_Color) {
+            .r = CLAY__LERP(arguments.initial.overlayColor.r, arguments.target.overlayColor.r, lerpAmount),
+            .g = CLAY__LERP(arguments.initial.overlayColor.g, arguments.target.overlayColor.g, lerpAmount),
+            .b = CLAY__LERP(arguments.initial.overlayColor.b, arguments.target.overlayColor.b, lerpAmount),
+            .a = CLAY__LERP(arguments.initial.overlayColor.a, arguments.target.overlayColor.a, lerpAmount),
+        };
+    }
+    if (arguments.properties & CLAY_TRANSITION_PROPERTY_BORDER_COLOR) {
+        arguments.current->borderColor = CLAY__INIT(Clay_Color) {
+            .r = CLAY__LERP(arguments.initial.borderColor.r, arguments.target.borderColor.r, lerpAmount),
+            .g = CLAY__LERP(arguments.initial.borderColor.g, arguments.target.borderColor.g, lerpAmount),
+            .b = CLAY__LERP(arguments.initial.borderColor.b, arguments.target.borderColor.b, lerpAmount),
+            .a = CLAY__LERP(arguments.initial.borderColor.a, arguments.target.borderColor.a, lerpAmount),
+        };
+    }
+    if (arguments.properties & CLAY_TRANSITION_PROPERTY_BORDER_WIDTH) {
+        arguments.current->borderWidth = CLAY__INIT(Clay_BorderWidth) {
+            .left = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.left, arguments.target.borderWidth.left, lerpAmount),
+            .right = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.right, arguments.target.borderWidth.right, lerpAmount),
+            .top = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.top, arguments.target.borderWidth.top, lerpAmount),
+            .bottom = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.bottom, arguments.target.borderWidth.bottom, lerpAmount),
+            .betweenChildren = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.betweenChildren, arguments.target.borderWidth.betweenChildren, lerpAmount),
+        };
+    }
+    return ratio >= 1;
+}
+
+#endif // CLAY_IMPLEMENTATION
+
+/*
+LICENSE
+zlib/libpng license
+
+Copyright (c) 2024 Nic Barker
+
+This software is provided 'as-is', without any express or implied warranty.
+In no event will the authors be held liable for any damages arising from the
+use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+    1. The origin of this software must not be misrepresented; you must not
+    claim that you wrote the original software. If you use this software in a
+    product, an acknowledgment in the product documentation would be
+    appreciated but is not required.
+
+    2. Altered source versions must be plainly marked as such, and must not
+    be misrepresented as being the original software.
+
+    3. This notice may not be removed or altered from any source
+    distribution.
+*/
diff --git a/vendor/clay_renderer_raylib.c b/vendor/clay_renderer_raylib.c
new file mode 100644
index 0000000..7fbc2c7
--- /dev/null
+++ b/vendor/clay_renderer_raylib.c
@@ -0,0 +1,321 @@
+#include "raylib.h"
+#include "raymath.h"
+#include "stdint.h"
+#include "string.h"
+#include "stdio.h"
+#include "stdlib.h"
+
+#define CLAY_RECTANGLE_TO_RAYLIB_RECTANGLE(rectangle) (Rectangle) { .x = rectangle.x, .y = rectangle.y, .width = rectangle.width, .height = rectangle.height }
+#define CLAY_COLOR_TO_RAYLIB_COLOR(color) (Color) { .r = (unsigned char)roundf(color.r), .g = (unsigned char)roundf(color.g), .b = (unsigned char)roundf(color.b), .a = (unsigned char)roundf(color.a) }
+
+Camera Raylib_camera;
+
+typedef enum
+{
+    CUSTOM_LAYOUT_ELEMENT_TYPE_3D_MODEL
+} CustomLayoutElementType;
+
+typedef struct
+{
+    Model model;
+    float scale;
+    Vector3 position;
+    Matrix rotation;
+} CustomLayoutElement_3DModel;
+
+typedef struct
+{
+    CustomLayoutElementType type;
+    union {
+        CustomLayoutElement_3DModel model;
+    } customData;
+} CustomLayoutElement;
+
+const char* overlayShaderCode = "#version 330\n"
+                                "\n"
+                                "in vec2 fragTexCoord;\n"
+                                "in vec4 fragColor;\n"
+                                "\n"
+                                "uniform sampler2D texture0;\n"
+                                "uniform vec4 overlayColor;\n"
+                                "\n"
+                                "out vec4 finalColor;\n"
+                                "\n"
+                                "void main()\n"
+                                "{\n"
+                                "    vec4 texelColor = texture(texture0, fragTexCoord) * fragColor;\n"
+                                "\n"
+                                "    vec3 blendedRGB = mix(texelColor.rgb, overlayColor.rgb, overlayColor.a);\n"
+                                "\n"
+                                "    finalColor = vec4(blendedRGB, texelColor.a);\n"
+                                "}";
+
+Shader overlayShader;
+int colorLoc;
+bool overlayEnabled = false;
+
+void InitOverlay() {
+    overlayShader = LoadShaderFromMemory(0, overlayShaderCode);
+    colorLoc = GetShaderLocation(overlayShader, "overlayColor");
+}
+
+void SetColorOverlay(Color color) {
+    overlayEnabled = true;
+    float colorFloat[4] = {
+        (float)color.r/255.0f,
+        (float)color.g/255.0f,
+        (float)color.b/255.0f,
+        (float)color.a/255.0f,
+    };
+
+    SetShaderValue(overlayShader, colorLoc, colorFloat, SHADER_UNIFORM_VEC4);
+    BeginShaderMode(overlayShader);
+}
+
+void DisableColorOverlay() {
+    if (overlayEnabled) {
+        EndShaderMode();
+        overlayEnabled = false;
+    }
+}
+
+// Get a ray trace from the screen position (i.e mouse) within a specific section of the screen
+Ray GetScreenToWorldPointWithZDistance(Vector2 position, Camera camera, int screenWidth, int screenHeight, float zDistance)
+{
+    Ray ray = { 0 };
+
+    // Calculate normalized device coordinates
+    // NOTE: y value is negative
+    float x = (2.0f*position.x)/(float)screenWidth - 1.0f;
+    float y = 1.0f - (2.0f*position.y)/(float)screenHeight;
+    float z = 1.0f;
+
+    // Store values in a vector
+    Vector3 deviceCoords = { x, y, z };
+
+    // Calculate view matrix from camera look at
+    Matrix matView = MatrixLookAt(camera.position, camera.target, camera.up);
+
+    Matrix matProj = MatrixIdentity();
+
+    if (camera.projection == CAMERA_PERSPECTIVE)
+    {
+        // Calculate projection matrix from perspective
+        matProj = MatrixPerspective(camera.fovy*DEG2RAD, ((double)screenWidth/(double)screenHeight), 0.01f, zDistance);
+    }
+    else if (camera.projection == CAMERA_ORTHOGRAPHIC)
+    {
+        double aspect = (double)screenWidth/(double)screenHeight;
+        double top = camera.fovy/2.0;
+        double right = top*aspect;
+
+        // Calculate projection matrix from orthographic
+        matProj = MatrixOrtho(-right, right, -top, top, 0.01, 1000.0);
+    }
+
+    // Unproject far/near points
+    Vector3 nearPoint = Vector3Unproject((Vector3){ deviceCoords.x, deviceCoords.y, 0.0f }, matProj, matView);
+    Vector3 farPoint = Vector3Unproject((Vector3){ deviceCoords.x, deviceCoords.y, 1.0f }, matProj, matView);
+
+    // Calculate normalized direction vector
+    Vector3 direction = Vector3Normalize(Vector3Subtract(farPoint, nearPoint));
+
+    ray.position = farPoint;
+
+    // Apply calculated vectors to ray
+    ray.direction = direction;
+
+    return ray;
+}
+
+
+static inline Clay_Dimensions Raylib_MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) {
+    // Measure string size for Font
+    Clay_Dimensions textSize = { 0 };
+
+    float maxTextWidth = 0.0f;
+    float lineTextWidth = 0;
+    int maxLineCharCount = 0;
+    int lineCharCount = 0;
+
+    float textHeight = config->fontSize;
+    Font* fonts = (Font*)userData;
+    Font fontToUse = fonts[config->fontId];
+    // Font failed to load, likely the fonts are in the wrong place relative to the execution dir.
+    // RayLib ships with a default font, so we can continue with that built in one.
+    if (!fontToUse.glyphs) {
+        fontToUse = GetFontDefault();
+    }
+
+    float scaleFactor = config->fontSize/(float)fontToUse.baseSize;
+
+    for (int i = 0; i < text.length; ++i, lineCharCount++)
+    {
+        if (text.chars[i] == '\n') {
+            maxTextWidth = fmax(maxTextWidth, lineTextWidth);
+            maxLineCharCount = CLAY__MAX(maxLineCharCount, lineCharCount);
+            lineTextWidth = 0;
+            lineCharCount = 0;
+            continue;
+        }
+        int index = text.chars[i] - 32;
+        if (fontToUse.glyphs[index].advanceX != 0) lineTextWidth += fontToUse.glyphs[index].advanceX;
+        else lineTextWidth += (fontToUse.recs[index].width + fontToUse.glyphs[index].offsetX);
+    }
+
+    maxTextWidth = fmax(maxTextWidth, lineTextWidth);
+    maxLineCharCount = CLAY__MAX(maxLineCharCount, lineCharCount);
+
+    textSize.width = maxTextWidth * scaleFactor + (lineCharCount * config->letterSpacing);
+    textSize.height = textHeight;
+
+    return textSize;
+}
+
+void Clay_Raylib_Initialize(int width, int height, const char *title, unsigned int flags) {
+    SetConfigFlags(flags);
+    InitWindow(width, height, title);
+    InitOverlay();
+//    EnableEventWaiting();
+}
+
+// A MALLOC'd buffer, that we keep modifying inorder to save from so many Malloc and Free Calls.
+// Call Clay_Raylib_Close() to free
+static char *temp_render_buffer = NULL;
+static int temp_render_buffer_len = 0;
+
+// Call after closing the window to clean up the render buffer
+void Clay_Raylib_Close()
+{
+    if(temp_render_buffer) free(temp_render_buffer);
+    temp_render_buffer_len = 0;
+
+    CloseWindow();
+}
+
+
+void Clay_Raylib_Render(Clay_RenderCommandArray renderCommands, Font* fonts)
+{
+    for (int j = 0; j < renderCommands.length; j++)
+    {
+        Clay_RenderCommand *renderCommand = Clay_RenderCommandArray_Get(&renderCommands, j);
+        Clay_BoundingBox boundingBox = {renderCommand->boundingBox.x, renderCommand->boundingBox.y, renderCommand->boundingBox.width, renderCommand->boundingBox.height};
+        switch (renderCommand->commandType)
+        {
+            case CLAY_RENDER_COMMAND_TYPE_TEXT: {
+                Clay_TextRenderData *textData = &renderCommand->renderData.text;
+                Font fontToUse = fonts[textData->fontId];
+
+                int strlen = textData->stringContents.length + 1;
+
+                if(strlen > temp_render_buffer_len) {
+                    // Grow the temp buffer if we need a larger string
+                    if(temp_render_buffer) free(temp_render_buffer);
+                    temp_render_buffer = (char *) malloc(strlen);
+                    temp_render_buffer_len = strlen;
+                }
+
+                // Raylib uses standard C strings so isn't compatible with cheap slices, we need to clone the string to append null terminator
+                memcpy(temp_render_buffer, textData->stringContents.chars, textData->stringContents.length);
+                temp_render_buffer[textData->stringContents.length] = '\0';
+                DrawTextEx(fontToUse, temp_render_buffer, (Vector2){boundingBox.x, boundingBox.y}, (float)textData->fontSize, (float)textData->letterSpacing, CLAY_COLOR_TO_RAYLIB_COLOR(textData->textColor));
+
+                break;
+            }
+            case CLAY_RENDER_COMMAND_TYPE_IMAGE: {
+                Texture2D imageTexture = *(Texture2D *)renderCommand->renderData.image.imageData;
+                Clay_Color tintColor = renderCommand->renderData.image.backgroundColor;
+                if (tintColor.r == 0 && tintColor.g == 0 && tintColor.b == 0 && tintColor.a == 0) {
+                    tintColor = (Clay_Color) { 255, 255, 255, 255 };
+                }
+                DrawTexturePro(
+                    imageTexture,
+                    (Rectangle) { 0, 0, imageTexture.width, imageTexture.height },
+                    (Rectangle){boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height},
+                    (Vector2) {},
+                    0,
+                    CLAY_COLOR_TO_RAYLIB_COLOR(tintColor));
+                break;
+            }
+            case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: {
+                BeginScissorMode((int)roundf(boundingBox.x), (int)roundf(boundingBox.y), (int)roundf(boundingBox.width), (int)roundf(boundingBox.height));
+                break;
+            }
+            case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: {
+                EndScissorMode();
+                break;
+            }
+            case CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START: {
+                SetColorOverlay(CLAY_COLOR_TO_RAYLIB_COLOR(renderCommand->renderData.overlayColor.color));
+                break;
+            }
+            case CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_END: {
+                DisableColorOverlay();
+            }
+            case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: {
+                Clay_RectangleRenderData *config = &renderCommand->renderData.rectangle;
+                if (config->cornerRadius.topLeft > 0) {
+                    float radius = (config->cornerRadius.topLeft * 2) / (float)((boundingBox.width > boundingBox.height) ? boundingBox.height : boundingBox.width);
+                    DrawRectangleRounded((Rectangle) { boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height }, radius, 8, CLAY_COLOR_TO_RAYLIB_COLOR(config->backgroundColor));
+                } else {
+                    DrawRectangle(boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height, CLAY_COLOR_TO_RAYLIB_COLOR(config->backgroundColor));
+                }
+                break;
+            }
+            case CLAY_RENDER_COMMAND_TYPE_BORDER: {
+                Clay_BorderRenderData *config = &renderCommand->renderData.border;
+                // Left border
+                if (config->width.left > 0) {
+                    DrawRectangleV((Vector2) { boundingBox.x, boundingBox.y + config->cornerRadius.topLeft }, (Vector2) { config->width.left, boundingBox.height - config->cornerRadius.topLeft - config->cornerRadius.bottomLeft }, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+                }
+                // Right border
+                if (config->width.right > 0) {
+                    DrawRectangleV((Vector2) { boundingBox.x + boundingBox.width - config->width.right, boundingBox.y + config->cornerRadius.topRight }, (Vector2) { config->width.right, boundingBox.height - config->cornerRadius.topRight - config->cornerRadius.bottomRight }, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+                }
+                // Top border
+                if (config->width.top > 0) {
+                    DrawRectangleV((Vector2) { boundingBox.x + config->cornerRadius.topLeft, boundingBox.y }, (Vector2) { boundingBox.width - config->cornerRadius.topLeft - config->cornerRadius.topRight, (int)config->width.top }, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+                }
+                // Bottom border
+                if (config->width.bottom > 0) {
+                    DrawRectangleV((Vector2) { boundingBox.x + config->cornerRadius.bottomLeft, boundingBox.y + boundingBox.height - config->width.bottom }, (Vector2) { boundingBox.width - config->cornerRadius.bottomLeft - config->cornerRadius.bottomRight, (int)config->width.bottom }, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+                }
+                if (config->cornerRadius.topLeft > 0) {
+                    DrawRing((Vector2) { roundf(boundingBox.x + config->cornerRadius.topLeft), roundf(boundingBox.y + config->cornerRadius.topLeft) }, roundf(config->cornerRadius.topLeft - config->width.top), config->cornerRadius.topLeft, 180, 270, 10, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+                }
+                if (config->cornerRadius.topRight > 0) {
+                    DrawRing((Vector2) { roundf(boundingBox.x + boundingBox.width - config->cornerRadius.topRight), roundf(boundingBox.y + config->cornerRadius.topRight) }, roundf(config->cornerRadius.topRight - config->width.top), config->cornerRadius.topRight, 270, 360, 10, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+                }
+                if (config->cornerRadius.bottomLeft > 0) {
+                    DrawRing((Vector2) { roundf(boundingBox.x + config->cornerRadius.bottomLeft), roundf(boundingBox.y + boundingBox.height - config->cornerRadius.bottomLeft) }, roundf(config->cornerRadius.bottomLeft - config->width.bottom), config->cornerRadius.bottomLeft, 90, 180, 10, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+                }
+                if (config->cornerRadius.bottomRight > 0) {
+                    DrawRing((Vector2) { roundf(boundingBox.x + boundingBox.width - config->cornerRadius.bottomRight), roundf(boundingBox.y + boundingBox.height - config->cornerRadius.bottomRight) }, roundf(config->cornerRadius.bottomRight - config->width.bottom), config->cornerRadius.bottomRight, 0.1, 90, 10, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+                }
+                break;
+            }
+            case CLAY_RENDER_COMMAND_TYPE_CUSTOM: {
+                Clay_CustomRenderData *config = &renderCommand->renderData.custom;
+                CustomLayoutElement *customElement = (CustomLayoutElement *)config->customData;
+                if (!customElement) continue;
+                switch (customElement->type) {
+                    case CUSTOM_LAYOUT_ELEMENT_TYPE_3D_MODEL: {
+                        Clay_BoundingBox rootBox = renderCommands.internalArray[0].boundingBox;
+                        float scaleValue = CLAY__MIN(CLAY__MIN(1, 768 / rootBox.height) * CLAY__MAX(1, rootBox.width / 1024), 1.5f);
+                        Ray positionRay = GetScreenToWorldPointWithZDistance((Vector2) { renderCommand->boundingBox.x + renderCommand->boundingBox.width / 2, renderCommand->boundingBox.y + (renderCommand->boundingBox.height / 2) + 20 }, Raylib_camera, (int)roundf(rootBox.width), (int)roundf(rootBox.height), 140);
+                        BeginMode3D(Raylib_camera);
+                            DrawModel(customElement->customData.model.model, positionRay.position, customElement->customData.model.scale * scaleValue, WHITE);        // Draw 3d model with texture
+                        EndMode3D();
+                        break;
+                    }
+                    default: break;
+                }
+                break;
+            }
+            default: {
+                printf("Error: unhandled render command.");
+                exit(1);
+            }
+        }
+    }
+}