diff --git a/services/booth/booth/app.py b/services/booth/booth/app.py
index 85b176f..e6903a8 100644
--- a/services/booth/booth/app.py
+++ b/services/booth/booth/app.py
@@ -269,13 +269,31 @@ def build_gallery(child: Path) -> list[dict]:
if rel in sidecars:
continue
p = by_rel[rel]
+ dkind = doc_kind(p.name)
+ rendered = None
+ rendered_html = False
+ # Pre-render docs so the gallery can show them INLINE (collapsible)
+ # instead of linking out to a separate page. Bounded by DOC_MAX_BYTES:
+ # a giant log stays a download link rather than being inlined into every
+ # index render. Markdown → HTML (marked safe in the template); plain text
+ # is returned RAW and the template escapes it inside
— pre-escaping
+ # here would double-encode under Jinja autoescape.
+ if dkind is not None:
+ try:
+ if p.stat().st_size <= DOC_MAX_BYTES:
+ text = p.read_text(errors="replace")
+ rendered, rendered_html = render_doc(text, dkind)
+ except OSError:
+ rendered = None
items.append(
{
"name": rel,
"kind": classify(p.name),
- "doc": doc_kind(p.name),
+ "doc": dkind,
"url": quote(rel, safe="/"),
"caption": caption.get(rel),
+ "rendered": rendered,
+ "rendered_html": rendered_html,
}
)
return items
diff --git a/services/booth/booth/templates/base.html b/services/booth/booth/templates/base.html
index c2e099a..9824cae 100644
--- a/services/booth/booth/templates/base.html
+++ b/services/booth/booth/templates/base.html
@@ -212,6 +212,50 @@
.item figcaption{padding:.6rem .85rem .75rem;color:var(--fg-2);font-size:.78rem;
font-family:var(--font-mono);letter-spacing:.02em;border-top:1px solid var(--border-subtle);word-break:break-word}
.item-audio figcaption,.item-other figcaption{border-top:none}
+
+ /* Inline doc rendering — a .md/.txt/.log shows in place, collapsible and
+ closable, instead of a link to a separate page. The item spans the full
+ grid width so prose has a readable measure. */
+ .item-doc{grid-column:1 / -1}
+ .item-doc.is-closed{display:none}
+ .doc-inline{display:block}
+ .doc-inline > .doc-bar{list-style:none;cursor:pointer;display:flex;align-items:center;gap:.55rem;
+ padding:.6rem .85rem;font-family:var(--font-mono);font-size:.8rem;color:var(--fg-2);
+ background:var(--rk-well);border-bottom:1px solid var(--border-subtle);user-select:none}
+ .doc-inline > .doc-bar::-webkit-details-marker{display:none}
+ .doc-chevron{color:var(--fg-3);transition:transform .12s ease;font-size:.7rem}
+ .doc-inline[open] > .doc-bar .doc-chevron{transform:rotate(90deg)}
+ .doc-name{color:var(--fg-1);word-break:break-all}
+ .doc-spacer{flex:1}
+ .doc-act{color:var(--fg-3);text-decoration:none;padding:.1rem .35rem;border-radius:5px;
+ font-size:.9rem;line-height:1;background:none;border:0;cursor:pointer;font-family:inherit}
+ .doc-act:hover{color:var(--aus-bright-cyan,#42dcd1);background:var(--rk-deep)}
+ .doc-close:hover{color:var(--aus-red,#ff6b6b)}
+ .doc-body{margin:0;border:0;border-radius:0;max-height:32rem;overflow:auto;padding:1rem 1.15rem}
+ .doc-body.textview{background:var(--rk-panel)}
+
+ /* Shared doc typography — used by the inline body above AND the full-page
+ doc view (doc.html). Kept here so both surfaces render identically. */
+ .textview{white-space:pre-wrap;word-break:break-word;font-family:var(--font-mono);
+ font-size:.86rem;line-height:1.5;color:var(--fg-1);background:var(--rk-well);
+ border:1px solid var(--rk-line,#252a35);border-radius:10px;padding:1rem 1.15rem;overflow-x:auto}
+ .markdown-body{color:var(--fg-1);line-height:1.62;font-size:.98rem;overflow-wrap:break-word}
+ .markdown-body h1,.markdown-body h2,.markdown-body h3{line-height:1.25;margin:1.6em 0 .5em}
+ .markdown-body h1{font-size:1.7em}.markdown-body h2{font-size:1.35em}.markdown-body h3{font-size:1.12em}
+ .markdown-body h1,.markdown-body h2{border-bottom:1px solid var(--rk-line,#252a35);padding-bottom:.3em}
+ .markdown-body :first-child{margin-top:0}
+ .markdown-body p,.markdown-body ul,.markdown-body ol,.markdown-body blockquote{margin:.7em 0}
+ .markdown-body a{color:var(--aus-bright-cyan,#42dcd1)}
+ .markdown-body code{font-family:var(--font-mono);font-size:.86em;background:var(--rk-well);
+ padding:.12em .38em;border-radius:5px}
+ .markdown-body pre{background:var(--rk-well);border:1px solid var(--rk-line,#252a35);
+ border-radius:10px;padding:.9rem 1.05rem;overflow-x:auto}
+ .markdown-body pre code{background:none;padding:0}
+ .markdown-body blockquote{border-left:3px solid var(--aus-bright-cyan,#42dcd1);
+ padding-left:1em;color:var(--fg-2);margin-left:0}
+ .markdown-body table{border-collapse:collapse;display:block;overflow-x:auto}
+ .markdown-body th,.markdown-body td{border:1px solid var(--rk-line,#252a35);padding:.4em .7em}
+ .markdown-body img{max-width:100%}
diff --git a/services/booth/booth/templates/booth.html b/services/booth/booth/templates/booth.html
index 52b8fcf..ebfb242 100644
--- a/services/booth/booth/templates/booth.html
+++ b/services/booth/booth/templates/booth.html
@@ -25,6 +25,29 @@
{% else %}
{% for it in items %}
+ {% if it.doc and it.rendered is not none %}
+ {# Docs render INLINE, collapsible, and closable — not a link to a
+ separate page. is native collapse (works with JS off);
+ the ✕ hides the item for the session (JS, progressive enhancement).
+ The item spans the full grid width so prose has room to read. #}
+
+
+
+ ▸
+ {{ it.name }}
+
+ ⤢
+ ⬇
+
+
+ {% if it.rendered_html %}
+ {{ it.rendered|safe }}
+ {% else %}
+
{{ it.rendered }}
+ {% endif %}
+
+
+ {% else %}
{% if it.kind == 'image' %}
@@ -36,6 +59,7 @@
{% elif it.kind == 'audio' %}
{% elif it.doc %}
+ {# a doc too large to inline (over DOC_MAX_BYTES) still links out #}
📄 {{ it.name }}
{% else %}
⬇ {{ it.name }}
@@ -49,6 +73,7 @@
{% endif %}
+ {% endif %}
{% endfor %}
{% endif %}
@@ -85,5 +110,21 @@
});
});
})();
+
+ /* Inline-doc ✕ closes (hides) a rendered doc for the session. The button sits
+ inside , so without this its click would just toggle the
+ open/closed — stopPropagation + preventDefault make ✕ mean "close", not
+ "collapse". Collapse stays available via the rest of the summary bar. With
+ JS off the button is inert and collapse via still works. */
+ (function () {
+ document.querySelectorAll('.doc-close').forEach(function (btn) {
+ btn.addEventListener('click', function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ var item = btn.closest('.item-doc');
+ if (item) item.classList.add('is-closed');
+ });
+ });
+ })();
{% endblock %}
diff --git a/services/booth/booth/templates/doc.html b/services/booth/booth/templates/doc.html
index 66f19ce..265a0c5 100644
--- a/services/booth/booth/templates/doc.html
+++ b/services/booth/booth/templates/doc.html
@@ -15,26 +15,10 @@
{% endif %}