+#+TITLE: AGENTS — Operating guide for the physical repo
+
+* Purpose
+
+This repository catalogs hobby-scale physical designs (3D-printable
+cases, mounts, organizers, optical adapters, small electronic
+projects). Each design lives in its own subdirectory under one of four
+top-level categories.
+
+The repo is a *publishing artifact*, not a buildable codebase. There
+is no compiler, no test runner, no package manager. The deliverable is
+HTML exported from Emacs / org-mode.
+
+* Layout
+
+| Path | Purpose |
+|------------------+---------------------------------------------------|
+| =index.org= | Landing page; lists every project with thumbnails |
+| =style.css= | Shared stylesheet referenced by every page |
+| =COPYING= | CC0 license text |
+| =Cases/= | 10 projects — cases, covers, holders |
+| =Furniture/= | 6 projects — furniture and mounts |
+| =Miscellaneous/= | 13 projects — small electronics and tooling |
+| =Optics/= | 4 projects — optical instruments and adapters |
+| =Tools/= | Empty placeholder; future home for tool designs |
+| =AGENTS.org= | This file |
+
+* Conventions
+
+** Every project directory starts with an uppercase letter
+
+A directory whose first character is lowercase is a bug. Fix with =git mv= via
+a temporary sibling name on case-sensitive Linux:
+
+#+begin_example
+git mv "old/name" "./.__rename_tmp_XXXX"
+git mv "./.__rename_tmp_XXXX" "New/name"
+#+end_example
+
+** Per-page section template
+
+Every =index.org= in a project directory follows this exact top-level
+structure, in this order:
+
+1. =* Overview= (no =:PROPERTIES:= drawer, no =CUSTOM_ID=)
+ - The disclaimer bullet goes *first*, before any other content.
+ - All project content (images, paragraphs, download links, sub-sections
+ like =* Files=) flows directly under this heading.
+
+2. Optional middle sections such as =* Files=, =* Schematic=, =* Parts=
+ — these are content-only, not metadata.
+
+3. =* Repository= (last)
+ - License bullet (CC0, repo-scoped wording)
+ - Author block (name, homepage, mailto link)
+ - See-also list (other-physical-projects link + svjatoslav.eu/projects link)
+ - =** Git repository= sub-section with snapshot / gitweb / clone URL
+
+When adding a new page, copy an existing one and replace the body content. Do
+not invent a new section layout — uniformity across pages matters for the
+export pipeline.
+
+** Front matter (file header)
+
+Every page starts with this block, unchanged:
+
+#+begin_example
+,#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+,#+TITLE: <Page title>
+,#+LANGUAGE: en
+,#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+,#+LATEX_HEADER: \usepackage{parskip}
+,#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+,#+OPTIONS: H:20 num:20
+,#+OPTIONS: author:nil
+
+,#+HTML_HEAD: <link rel="stylesheet" href="../../style.css"/>
+
+[[file:../../index.html#outline-container-<category>][Back to physical projects]]
+#+end_example
+
+The back-link anchor must match the heading =CUSTOM_ID= of the corresponding
+section in =index.org= (=outline-container-cases=, =outline-container-furniture=,
+=outline-container-miscellaneous=, =outline-container-optics=,
+=outline-container-cable-wire= for items under the "Cable, wire" subsection).
+
+** Bullet syntax
+
+In org-mode plain lists, =+=, =+=, and =*= are all interchangeable list markers
+and render identically. Use =+= for top-level bullets in the =* Overview= and
+=* Repository= sections to match the established style. Do not "fix" =+=
+bullets to =-= or vice versa — they are equivalent.
+
+** Links
+
+- Internal project references in =index.org= use paths with *literal
+ spaces* in directory names. Spaces in URL-bearing link types
+ (https, mailto) must be =%20=-encoded (see "Link patterns" below).
+- Cross-references between project pages use raw =file:= links.
+- The back-link in every page's front matter uses an HTML anchor
+ fragment pointing into =index.html=.
+
+** Link patterns
+
+Five link shapes are used in the project. Stick to these — no new
+variations.
+
+*Spaces in filenames.* Filenames and directory names in this project
+often contain spaces (e.g. =Body and covers/foo.png=,
+=Spray gun holder/index.html=). Use a *literal space* in the link
+target — org and the browser both handle it correctly:
+
+#+begin_example
+,#+attr_html: :class responsive-img
+[[file:Body and covers/body, 1.png]]
+#+end_example
+
+*Exception: =https:= and =mailto:= links must use =%20= for any space in
+the path or query string.* A literal space in a URL is invalid (per
+RFC 3986) and the link will be malformed or rejected by browsers. So:
+
+#+begin_example
+[[https://example.com/path%20with%20space][Product page]]
+#+end_example
+
+But for =file:= links, literal space is correct and =%20= is *not*
+required. (The form =[[file:Body%20and%20covers/x.png]]= also works in
+practice — browsers and org both decode it back to a space — but the
+project convention is literal space.)
+
+1. *Image* — a PNG/JPG/etc. that the renderer shows inline. Always
+ preceded by =#+attr_html: :class responsive-img= so the page CSS
+ sizes it correctly. Spaces in the path are literal:
+
+ #+begin_example
+ ,#+attr_html: :class responsive-img
+ [[file:make.png]]
+ #+end_example
+
+ #+begin_example
+ ,#+attr_html: :class responsive-img
+ [[file:Body and covers/body, 1.png]]
+ #+end_example
+
+2. *Download* — a link to a FreeCAD project or STL file. The link
+ text is the file type, not the filename. Spaces in the path are
+ literal; spaces in the display text ([...]) need no encoding:
+
+ #+begin_example
+ - [[file:body.stl][STL file]]
+ - [[file:body.FCStd][FreeCAD project]]
+ - [[file:Lab heater and cooler.FCStd][FreeCAD design file]]
+ #+end_example
+
+3. *External URL* — auto-labelled (org uses the URL as the link text)
+ or explicitly labelled. Both are common. *Spaces in the URL must
+ be =%20=-encoded* — unlike =file:= links, raw spaces are not valid
+ in a URL:
+
+ #+begin_example
+ https://example.com/page
+ [[https://example.com/path%20with%20space][Product page]]
+ #+end_example
+
+4. *Email* — always use the =mailto://= form (note the double slash).
+ This is what every existing page uses, even though the standard
+ org form is =mailto:= (single slash). Don't "fix" the existing
+ pages. Email addresses don't contain spaces, so no encoding is
+ needed:
+
+ #+begin_example
+ [[mailto://svjatoslav@svjatoslav.eu][svjatoslav@svjatoslav.eu]]
+ #+end_example
+
+5. *Back-link* — top of every page, points to the index section
+ listing the project. The anchor fragment never contains spaces
+ (org's auto-generated IDs are hyphenated), so no encoding is
+ needed here. Anchor values that are actually used in the repo:
+
+ | Anchor | Used for |
+ |-----------------------------------------+-------------------------------------|
+ | =outline-container-optics= | Optics/ projects |
+ | =outline-container-furniture= | Furniture/ projects |
+ | =outline-container-miscellaneous= | Most Miscellaneous/ projects |
+ | =outline-container-cable-wire= | Cable/wire Miscellaneous/ projects |
+ | =outline-container-audio= | Audio Miscellaneous/ projects |
+ | =outline-container-cases-optics= | Some Cases/ + Optics/ cross-section |
+ | =outline-container-measurement-device-cases= | A specific Cases/ subsection |
+
+ To find the correct anchor for a new page, run =Tools/Update web
+ site --force= once, then search the published =index.html= for
+ the section name; the =id="outline-container-..."= attribute
+ on the matching =<h2>= (or =<h3>=) is the anchor.
+
+** Page structure variations
+
+The "Per-page section template" above describes the top-level shape
+(Overview → optional middle sections → Repository). Within those
+sections, three sub-patterns recur and are accepted:
+
+- *Inline part headings* (=*Body*= / =*Cover*=): a project with two
+ physical parts uses single-asterisk emphasis headings inside
+ =* Overview= to split the description, instead of a top-level
+ =* Files= section. Each inline heading is followed by an
+ =#+attr_html= + image paragraph:
+
+ #+begin_example
+ ,*Body*
+ ,#+attr_html: :class responsive-img
+ [[file:body.png]]
+
+ ,*Cover*
+ ,#+attr_html: :class responsive-img
+ [[file:cover.png]]
+ #+end_example
+
+ Real examples: =Cases/Microscope slide box/=,
+ =Miscellaneous/Battery tester/=, =Cases/Macro filter storage
+ compartment/=.
+
+- *Download table* — for projects with multiple parts, a three-column
+ org table makes the FreeCAD/STL downloads scannable:
+
+ #+begin_example
+ | item | FreeCAD file | STL file |
+ |-------+--------------+----------|
+ | body | [[file:body.FCStd][download]] | [[file:body.stl][download]] |
+ | cover | [[file:cover.FCStd][download]] | [[file:cover.stl][download]] |
+ #+end_example
+
+- *Project-specific "See also:"* — a paragraph at the end of
+ =* Overview= that points to a related project, distinct from the
+ generic See-also list in =* Repository=:
+
+ #+begin_example
+ See also:
+ + [[file:../../Optics/Fujifilm X-mount microscope adapter/][Fujifilm microscope adapter]]
+ #+end_example
+
+ Use this when one project logically leads to another (e.g. "Case
+ for microscope slides" → "Microscope adapter").
+
+** Anti-patterns to avoid
+
+These have come up while maintaining the project. Each one was
+either a published-site bug or a verification false positive.
+
+- *Bullet-wrapped images.* A gallery image must be a bare paragraph
+ under an =#+attr_html= directive, not a list item:
+
+ #+begin_example
+ WRONG (bullet + image):
+ + Body:
+ + [[file:body.png]]
+
+ RIGHT (heading + image):
+ ** Body
+ ,#+attr_html: :class responsive-img
+ [[file:body.png]]
+ #+end_example
+
+ The bullet form renders as a bullet point around a single image in
+ the published HTML (looks like junk), and the =responsive-img=
+ class is lost because org doesn't apply attribute lines to
+ bullet-item content.
+
+- *=schematic= is ambiguous.* In some projects (e.g. =Miscellaneous/USB
+ desk fan mod=) =schematic= is a *directory* (containing KiCad
+ files like =DCDC_stepdown.sch=). In other projects, the link is
+ to a *file* called =schematic.png= at the project root. Don't
+ blindly rewrite =schematic= → =Schematic= in a link without
+ checking whether the original was a directory or a filename.
+ When renaming, use the =(^|/)old(/|$)= anchor in your search
+ regex to match directory components only.
+
+- *Lowercase-starting directory names.* Every directory in the
+ project starts with an uppercase letter (or a digit followed by
+ an uppercase letter). The rule applies to *directory* names, not
+ to filenames inside them. Files like =main board, 1.png= or
+ =schematic.png= are content and should not be renamed.
+
+** Workflow footnotes
+
+- =touch= does not invalidate the build cache. The incremental
+ export script (see "Build cache" below) keys on =sha256=, and
+ =touch= only updates mtime. If you want to test the sentinel
+ invalidation path, modify the file's content (e.g. =echo "" >>
+ style.css=), don't =touch= it.
+
+- The build script excludes =AGENTS.org= from exports (see commit
+ =f1187e6= "feat(build): incremental org→html export with
+ style.css sentinel"). The operating guide is for agents, not for
+ the public site, and a published =AGENTS.html= would be noise.
+
+- =./Tools/Update web site= re-opens itself in a =gnome-terminal=
+ the first time it's run (the "T" arg is the internal signal
+ meaning "we're already inside the terminal"). If you're running
+ the script from a script or over SSH without a display, the
+ re-open will fail — invoke the script with a "T" arg (e.g.
+ =./Tools/Update\ web\ site T --force=) to skip the re-launch.
+
+* Adding a new project
+
+1. Pick a category directory (=Cases/=, =Furniture/=, =Miscellaneous/=,
+ =Optics/=, or =Tools/=).
+2. Create a new subdirectory whose name starts with an uppercase letter.
+ Capitalize consistently with the rest of the category
+ (=Cases/Meiqqm portable dosimeter case/= style).
+3. Copy the front matter block from any existing page in the same category,
+ adjusting the back-link anchor if the category outline structure has
+ changed.
+4. Copy the =* Overview= + body + =* Repository= skeleton. Replace the body
+ content with the new project's images, text, and download links.
+5. Add an entry to =index.org= under the correct category heading, using the
+ existing ~{{{imglnk(...)}}}~ macro pattern. Match the URL-encoding of
+ sibling entries (lower or upper first letter, percent-encoded spaces).
+6. =git add= the new directory. Stage the =index.org= edit. Commit.
+7. Regenerate the HTML locally with Emacs to verify the result. The HTML
+ itself stays untracked.
+
+* Workflow preferences
+
+** Build cache (incremental export)
+
+=Tools/Update web site= maintains a small cache at =.org-export-state= (repo
+root, gitignored) that records one sha256 per exported =.org= file plus one
+sha256 for =style.css= (the "shared sentinel"). On each run, files whose hash
+matches the cached value are skipped — re-export only the ones you actually
+changed.
+
+The style.css sentinel is checked on every run: if =style.css= has changed
+since the last publish, every =.org= file is re-exported automatically. This
+catches style changes that you would otherwise have to remember to force.
+
+- To force a full rebuild regardless of the cache, pass =--force= (or
+ =--rebuild=) to the script.
+- If you change the darksun theme file (=~/.emacs.d/org-styles/html/darksun.theme=)
+ or any other Emacs export dependency outside the repo, use =--force=. The
+ cache only tracks files inside the repo.
+- If you change =index.org= and forget =--force=, the per-project HTML files
+ keep their old back-link text. Run =--force= to refresh.
+- To wipe the cache entirely, delete the =.org-export-state= file. The next
+ run will rebuild everything and recreate it.