adding administrator role
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Sat, 23 May 2026 11:54:26 +0000 (11:54 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Sat, 23 May 2026 11:54:26 +0000 (11:54 +0000)
34 files changed:
administrator/authored/.gitkeep [new file with mode: 0644]
administrator/document/how-to_release.html [new file with mode: 0644]
administrator/document/setup.js [new file with mode: 0644]
administrator/tool/archive [new file with mode: 0755]
administrator/tool/new-project [new file with mode: 0755]
administrator/tool/setup [new file with mode: 0644]
developer/document/2025-01-03_notes.txt [new file with mode: 0644]
document/doc_1.html [new file with mode: 0644]
document/doc_2.html [new file with mode: 0644]
document/install.org [new file with mode: 0644]
document/style/RT_TOC.js [new file with mode: 0644]
document/style/RT_code.js [new file with mode: 0644]
document/style/RT_math.js [new file with mode: 0644]
document/style/RT_term.js [new file with mode: 0644]
document/style/RT_title.js [new file with mode: 0644]
document/style/Rubio.js [new file with mode: 0644]
document/style/article_tech_ref.js [new file with mode: 0644]
document/style/body_visibility_hidden.js [new file with mode: 0644]
document/style/body_visibility_visible.js [new file with mode: 0644]
document/style/custom_tag.txt [new file with mode: 0644]
document/style/page_fixed_glow.js [new file with mode: 0644]
document/style/paginate_by_element.js [new file with mode: 0644]
document/style/style_orchestrator.js [new file with mode: 0644]
document/style/theme_dark_gold.js [new file with mode: 0644]
document/style/theme_light.js [new file with mode: 0644]
document/style/theme_light_gold.js [new file with mode: 0644]
document/style/utility.js [new file with mode: 0644]
document/style/version.txt [new file with mode: 0644]
env_developer [deleted file]
env_tester [deleted file]
env_toolsmith [deleted file]
setup [new file with mode: 0644]
tool_shared/bespoke/scratchpad
tool_shared/document/packages.org [new file with mode: 0644]

diff --git a/administrator/authored/.gitkeep b/administrator/authored/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/administrator/document/how-to_release.html b/administrator/document/how-to_release.html
new file mode 100644 (file)
index 0000000..888d414
--- /dev/null
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <title>Release howto</title>
+    <script src="setup.js"></script>
+    <script>
+      window.StyleRT.include('RT/theme');
+      window.StyleRT.include('RT/layout/article_tech_ref');
+    </script>
+  </head>
+  <body>
+    <RT-article>
+      <RT-title 
+        author="Thomas Walker Lynch" 
+        date="2026-03-10" 
+        title="How to release a project">
+      </RT-title>
+
+      <RT-TOC level="1"></RT-TOC>
+
+      <h1>Prerequisites</h1>
+      <p>
+        A release requires a tested promotion candidate. The developer compiles the code and promotes it to the <RT-code>consumer/made</RT-code> directory. The tester validates the candidate. The product manager gives the order to release.
+      </p>
+
+      <h1>Create the release branch</h1>
+      <p>
+        The <RT-term>administrator</RT-term> executes the release. Open a terminal. Enter the project workspace.
+      </p>
+      <RT-code>
+        > . setup administrator
+      </RT-code>
+
+      <p>
+        Switch to the core development branch. Ensure the local repository is current.
+      </p>
+      <RT-code>
+        > git checkout core_developer_branch
+        > git pull
+      </RT-code>
+
+      <p>
+        Create a new branch for the release. Use the major version number.
+      </p>
+      <RT-code>
+        > git checkout -b release_v&lt;major&gt;
+      </RT-code>
+
+      <h1>Issue management and patching</h1>
+      <p>
+        Only critical issues receive patches on a release branch. The product manager defines what is critical. If approved, the release team applies the fix directly to the existing <RT-code>release_v&lt;major&gt;</RT-code> branch.
+      </p>
+      <p>
+        The branch name <RT-code>release_v&lt;major&gt;</RT-code> remains static. The code on the branch is updated. The administrator advances the minor release number in the <RT-code>shared/tool/version</RT-code> file.
+      </p>
+      <p>
+        The administrator tags the new commit with the full version number, such as <RT-code>v&lt;major&gt;.&lt;minor&gt;</RT-code>. Minor versions are edits on the major release branch. They are visible to a person by running the version command.
+      </p>
+      <p>
+        The developer must ensure the fix is also ported to the <RT-code>core_developer_branch</RT-code>. This prevents the defect from reappearing in future major releases.
+      </p>
+
+      <h1>Tag and push</h1>
+      <p>
+        Tag the release with the exact version number.
+      </p>
+      <RT-code>
+        > git tag -a v&lt;major&gt;.&lt;minor&gt; -m "Release v&lt;major&gt;.&lt;minor&gt;"
+      </RT-code>
+
+      <p>
+        Push the branch to the remote repository. Push the tags.
+      </p>
+      <RT-code>
+        > git push origin release_v&lt;major&gt;
+        > git push origin --tags
+      </RT-code>
+
+      <h1>Repository default branch</h1>
+      <p>
+        Set the repository default branch to the new <RT-code>release_v&lt;major&gt;</RT-code> branch on the hosting platform. This ensures a person cloning or pulling the repository receives the most recent major release code by default.
+      </p>
+
+      <h1>Verification</h1>
+      <p>
+        Check the remote repository. Confirm the branch exists. Confirm the tag is visible. Confirm the default branch is updated.
+      </p>
+
+      <h1>Role responsibilities</h1>
+      <ul>
+        <li><strong>Administrator:</strong> Executes the release. Manages branches, tags, and remote pushes. Updates the version file. Sets the repository default branch.</li>
+        <li><strong>Developer:</strong> Compiles and promotes release candidates. Ports release branch fixes back to the core developer branch.</li>
+        <li><strong>Tester:</strong> Validates candidates. Writes a test for every issue in the core developer queue.</li>
+        <li><strong>Product Manager:</strong> Defines critical issues. Orders the release. Participates in release issue triage.</li>
+        <li><strong>Project Manager:</strong> Participates in release issue triage. Assesses impact to the overall project schedule.</li>
+      </ul>
+
+    </RT-article>
+  </body>
+</html>
diff --git a/administrator/document/setup.js b/administrator/document/setup.js
new file mode 100644 (file)
index 0000000..de1173d
--- /dev/null
@@ -0,0 +1,4 @@
+window.RT_REPO_ROOT = "../../";
+document.write('<script src="' + window.RT_REPO_ROOT + 'shared/dictionary_style-directory.js"></script>');
+document.write('<script src="' + window.RT_REPO_ROOT + 'shared/linked-project/RT-style-JS_public/consumer/release/RT/core/loader.js"></script>');
+document.write('<script src="' + window.RT_REPO_ROOT + 'shared/linked-project/RT-style-JS_public/consumer/release/RT/core/body_visibility_hidden.js"></script>');
diff --git a/administrator/tool/archive b/administrator/tool/archive
new file mode 100755 (executable)
index 0000000..a60a0a0
--- /dev/null
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+"""
+archive - Create an archive of the current Git repo's ref into ./scratchpad
+
+Commands (order-insensitive):
+  archive                 # default: zip (HEAD, ./scratchpad, Z-stamp if importable)
+  archive help            # show help
+  archive version         # show version
+  archive ref-<REF>       # choose ref (tag/branch/commit), default HEAD
+  archive out-<OUTDIR>    # choose output directory (default: <repo>/scratchpad)
+  archive no-stamp        # force omit timestamp even if Z is importable
+  archive z-format-<FMT>  # override timestamp format used with Z (optional)
+  archive zip             # write .zip
+  archive tgz             # write .tgz (compressed tar)
+  archive tar             # write .tar (uncompressed tar)
+
+Output names:
+  <repo>__<ref>[__<Z>].zip
+  <repo>__<ref>[__<Z>].tgz
+  <repo>__<ref>[__<Z>].tar
+"""
+
+from __future__ import annotations
+import gzip ,os ,pathlib ,subprocess ,sys
+from typing import Optional
+import importlib ,importlib.util
+from importlib.machinery import SourceFileLoader
+
+VERSION = "2.0"
+
+# ----------------------------------------------------------------------
+# Editable timestamp format (used when calling Z)
+# ----------------------------------------------------------------------
+Z_FORMAT = "%year-%month-%day_%hour%minute%secondZ"
+
+USAGE = f"""archive {VERSION}
+
+Usage:
+  archive [commands...]
+
+Commands (order-insensitive):
+  help
+  version
+  ref-<REF>
+  out-<OUTDIR>
+  no-stamp
+  z-format-<FMT>
+  zip
+  tgz
+  tar
+
+Examples:
+  archive
+  archive tgz
+  archive ref-main out-/tmp
+  archive z-format-%year-%month-%dayT%hour:%minute:%second.%scintillaZ
+""".rstrip()
+
+# ----------------------------------------------------------------------
+# git helpers
+# ----------------------------------------------------------------------
+def _run(*args: str ,check: bool = True ,cwd: Optional[pathlib.Path] = None) -> subprocess.CompletedProcess[str]:
+  return subprocess.run(
+    args
+    ,check=check
+    ,cwd=(str(cwd) if cwd else None)
+    ,text=True
+    ,stdout=subprocess.PIPE
+    ,stderr=subprocess.PIPE
+  )
+
+def _in_git_repo() -> bool:
+  try:
+    return _run("git" ,"rev-parse" ,"--is-inside-work-tree").stdout.strip().lower() == "true"
+  except subprocess.CalledProcessError:
+    return False
+
+def _git_top() -> pathlib.Path:
+  return pathlib.Path(_run("git" ,"rev-parse" ,"--show-toplevel").stdout.strip())
+
+def _git_ref_label(repo_top: pathlib.Path ,ref: str) -> str:
+  try:
+    return _run("git" ,"-C" ,str(repo_top) ,"describe" ,"--tags" ,"--always" ,"--dirty" ,ref).stdout.strip()
+  except subprocess.CalledProcessError:
+    return _run("git" ,"-C" ,str(repo_top) ,"rev-parse" ,"--short" ,ref).stdout.strip()
+
+# ----------------------------------------------------------------------
+# Z module discovery (supports extension-less file named "Z")
+# ----------------------------------------------------------------------
+def _import_Z_module(repo_top: pathlib.Path) -> Optional[object]:
+  try:
+    return importlib.import_module("Z")
+  except Exception:
+    pass
+
+  candidates: list[pathlib.Path] = []
+  here = pathlib.Path(__file__).resolve().parent
+  candidates += [here / "Z" ,here / "Z.py"]
+  candidates += [
+    repo_top / "shared" / "third_party" / "RT-project-share" / "release" / "python" / "Z"
+    ,repo_top / "shared" / "third_party" / "RT-project-share" / "release" / "python" / "Z.py"
+    ,repo_top / "shared" / "third_party" / "RT-project-share" / "release" / "bash" / "Z"
+  ]
+  for d in (pathlib.Path(p) for p in (os.getenv("PATH") or "").split(os.pathsep) if p):
+    p = d / "Z"
+    if p.exists() and p.is_file():
+      candidates.append(p)
+
+  for path in candidates:
+    try:
+      if not path.exists() or not path.is_file(): continue
+      spec = importlib.util.spec_from_loader("Z" ,SourceFileLoader("Z" ,str(path)))
+      if not spec or not spec.loader: continue
+      mod = importlib.util.module_from_spec(spec)
+      spec.loader.exec_module(mod)
+      if hasattr(mod ,"make_timestamp") or (hasattr(mod ,"get_utc_dict") and hasattr(mod ,"format_timestamp")):
+        return mod
+    except Exception:
+      continue
+  return None
+
+# ----------------------------------------------------------------------
+# Z stamp helper
+# ----------------------------------------------------------------------
+def make_z_stamp(zmod: object ,z_format: str) -> Optional[str]:
+  try:
+    if hasattr(zmod ,"make_timestamp"):
+      s = zmod.make_timestamp(fmt=z_format)
+      return (str(s).strip().replace("\n" ,"") or None)
+    if hasattr(zmod ,"get_utc_dict") and hasattr(zmod ,"format_timestamp"):
+      td = zmod.get_utc_dict()
+      s = zmod.format_timestamp(td ,z_format)
+      return (str(s).strip().replace("\n" ,"") or None)
+  except Exception:
+    return None
+  return None
+
+# ----------------------------------------------------------------------
+# archiving
+# ----------------------------------------------------------------------
+def _stream_git_archive_tar(repo_top: pathlib.Path ,prefix: str ,ref: str ,out_path: pathlib.Path ,compress: bool) -> None:
+  proc = subprocess.Popen(
+    ["git" ,"-C" ,str(repo_top) ,"archive" ,"--format=tar" ,f"--prefix={prefix}/" ,ref]
+    ,stdout=subprocess.PIPE
+  )
+  try:
+    if compress:
+      with gzip.open(out_path ,"wb") as f:
+        while True:
+          chunk = proc.stdout.read(1024 * 1024)
+          if not chunk: break
+          f.write(chunk)
+    else:
+      with open(out_path ,"wb") as f:
+        while True:
+          chunk = proc.stdout.read(1024 * 1024)
+          if not chunk: break
+          f.write(chunk)
+  finally:
+    if proc.stdout: proc.stdout.close()
+    rc = proc.wait()
+    if rc != 0:
+      try:
+        out_path.unlink(missing_ok=True)
+      finally:
+        raise subprocess.CalledProcessError(rc ,proc.args)
+
+def _stream_git_archive_zip(repo_top: pathlib.Path ,prefix: str ,ref: str ,out_zip_path: pathlib.Path) -> None:
+  proc = subprocess.Popen(
+    ["git" ,"-C" ,str(repo_top) ,"archive" ,"--format=zip" ,f"--prefix={prefix}/" ,ref]
+    ,stdout=subprocess.PIPE
+  )
+  try:
+    with open(out_zip_path ,"wb") as f:
+      while True:
+        chunk = proc.stdout.read(1024 * 1024)
+        if not chunk: break
+        f.write(chunk)
+  finally:
+    if proc.stdout: proc.stdout.close()
+    rc = proc.wait()
+    if rc != 0:
+      try:
+        out_zip_path.unlink(missing_ok=True)
+      finally:
+        raise subprocess.CalledProcessError(rc ,proc.args)
+
+# ----------------------------------------------------------------------
+# work function
+# ----------------------------------------------------------------------
+def work(
+  ref: str = "HEAD"
+  ,outdir: Optional[pathlib.Path] = None
+  ,force_no_stamp: bool = False
+  ,z_format: Optional[str] = None
+  ,archive_kind: str = "zip"
+) -> pathlib.Path:
+
+  if archive_kind not in ("zip" ,"tgz" ,"tar"):
+    raise RuntimeError("archive_kind must be 'zip', 'tgz', or 'tar'")
+
+  if not _in_git_repo():
+    raise RuntimeError("not inside a git repository")
+
+  repo_top = _git_top()
+  repo_name = repo_top.name
+  ref_label = _git_ref_label(repo_top ,ref)
+
+  stamp: Optional[str] = None
+  if not force_no_stamp:
+    zmod = _import_Z_module(repo_top)
+    if zmod is not None:
+      stamp = make_z_stamp(zmod ,z_format or Z_FORMAT)
+
+  target_dir = (outdir or (repo_top / "scratchpad"))
+  target_dir.mkdir(parents=True ,exist_ok=True)
+
+  if archive_kind == "zip":
+    suffix = ".zip"
+  elif archive_kind == "tgz":
+    suffix = ".tgz"
+  else:
+    suffix = ".tar"
+
+  out_name = f"{repo_name}__{ref_label}{('__' + stamp) if stamp else ''}{suffix}"
+  out_path = target_dir / out_name
+
+  if archive_kind == "zip":
+    _stream_git_archive_zip(repo_top ,repo_name ,ref ,out_path)
+  else:
+    compress = (archive_kind == "tgz")
+    _stream_git_archive_tar(repo_top ,repo_name ,ref ,out_path ,compress)
+
+  return out_path
+
+# ----------------------------------------------------------------------
+# CLI with command tokens
+# ----------------------------------------------------------------------
+def CLI(argv: Optional[list[str]] = None) -> int:
+  if argv is None: argv = sys.argv[1:]
+
+  ref = "HEAD"
+  outdir: Optional[pathlib.Path] = None
+  force_no_stamp = False
+  z_format: Optional[str] = None
+  archive_kind = "zip"
+
+  if not argv:
+    try:
+      print(f"Wrote {work(ref=ref ,outdir=outdir ,force_no_stamp=force_no_stamp ,z_format=z_format ,archive_kind=archive_kind)}")
+      return 0
+    except Exception as e:
+      print(f"archive: {e}" ,file=sys.stderr); return 1
+
+  for arg in argv:
+    if arg in ("help" ,"-h" ,"--help"): print(USAGE); return 0
+    if arg == "version": print(f"archive {VERSION}"); return 0
+    if arg == "no-stamp": force_no_stamp = True; continue
+    if arg == "zip": archive_kind = "zip"; continue
+    if arg == "tgz": archive_kind = "tgz"; continue
+    if arg == "tar": archive_kind = "tar"; continue
+    if arg.startswith("ref-"): ref = arg[4:] or ref; continue
+    if arg.startswith("out-"):
+      od = arg[4:]
+      outdir = pathlib.Path(od).resolve() if od else None
+      continue
+    if arg.startswith("z-format-"):
+      z_format = arg[len("z-format-"):] or None
+      continue
+    print(f"archive: unknown command '{arg}'" ,file=sys.stderr); return 1
+
+  try:
+    out_path = work(ref=ref ,outdir=outdir ,force_no_stamp=force_no_stamp ,z_format=z_format ,archive_kind=archive_kind)
+  except Exception as e:
+    print(f"archive: {e}" ,file=sys.stderr); return 1
+
+  print(f"Wrote {out_path}")
+  return 0
+
+# ----------------------------------------------------------------------
+if __name__ == "__main__":
+  raise SystemExit(CLI())
diff --git a/administrator/tool/new-project b/administrator/tool/new-project
new file mode 100755 (executable)
index 0000000..07582e2
--- /dev/null
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+import sys ,os ,subprocess
+from pathlib import Path
+
+def get_Z_date() -> str:
+  try:
+    res = subprocess.run(["Z" ,"format-%year-%month-%day"] ,capture_output=True ,text=True ,check=True)
+    return res.stdout.strip()
+  except Exception:
+    return "1970-01-01"
+
+def work(project_name: str ,remotes: list[str]) -> int:
+  repo_home = os.environ.get("REPO_HOME")
+  if not repo_home:
+    print("Error: REPO_HOME is not set. A person must run this from within a sourced role environment." ,file=sys.stderr)
+    return 1
+
+  repo_path = Path(repo_home).resolve()
+  
+  dest_path = Path(project_name)
+  if not dest_path.is_absolute():
+    dest_path = (repo_path.parent / project_name).resolve()
+
+  if dest_path.exists():
+    print(f"Error: Destination {dest_path} already exists." ,file=sys.stderr)
+    return 1
+
+  print(f"Creating new project at: {dest_path}")
+  dest_path.mkdir(parents=True)
+  
+  archive_cmd = "git archive HEAD | tar -x -C " + str(dest_path)
+  res = subprocess.run(archive_cmd ,shell=True ,cwd=str(repo_path))
+  if res.returncode != 0:
+    print("Error extracting archive." ,file=sys.stderr)
+    return 1
+
+  opus_src = dest_path / "0pus_Harmony"
+  opus_dst = dest_path / f"0pus_{dest_path.name}"
+  if opus_src.exists():
+    opus_src.rename(opus_dst)
+
+  version_file = dest_path / "shared" / "tool" / "version"
+  date_str = get_Z_date()
+  version_line = f"echo \"{dest_path.name} v0.1 {date_str}\"\n"
+  
+  if version_file.exists():
+    with open(version_file ,"a" ,encoding="utf-8") as vf:
+      vf.write(version_line)
+  else:
+    with open(version_file ,"w" ,encoding="utf-8") as vf:
+      vf.write(version_line)
+
+  print("Initializing git repository...")
+  subprocess.run(["git" ,"init" ,"-b" ,"core_developer_branch"] ,cwd=str(dest_path) ,check=True)
+  subprocess.run(["git" ,"add" ,"."] ,cwd=str(dest_path) ,check=True)
+  subprocess.run(["git" ,"commit" ,"-m" ,f"Initial commit of {dest_path.name} from Harmony skeleton"] ,cwd=str(dest_path) ,check=True)
+
+  for idx ,remote_url in enumerate(remotes):
+    remote_name = f"remote_{idx}"
+    if "github.com" in remote_url: 
+      remote_name = "github"
+    elif "RT" in remote_url: 
+      remote_name = "RT"
+    elif idx == 0:
+      remote_name = "origin"
+    
+    print(f"Adding remote '{remote_name}': {remote_url}")
+    subprocess.run(["git" ,"remote" ,"add" ,remote_name ,remote_url] ,cwd=str(dest_path) ,check=True)
+
+  print(f"Project '{dest_path.name}' successfully created.")
+  return 0
+
+def CLI(argv=None) -> int:
+  if argv is None:
+    argv = sys.argv[1:]
+
+  if not argv or "-h" in argv or "--help" in argv:
+    print("Usage: new-project <name> [-remote <url>]...")
+    return 0
+
+  project_name = None
+  remotes = []
+  
+  idx = 0
+  while idx < len(argv):
+    arg = argv[idx]
+    if arg == "-remote":
+      if idx + 1 < len(argv):
+        remotes.append(argv[idx+1])
+        idx += 1
+      else:
+        print("Error: -remote requires a URL." ,file=sys.stderr)
+        return 1
+    elif project_name is None:
+      project_name = arg
+    else:
+      print(f"Error: unexpected argument '{arg}'" ,file=sys.stderr)
+      return 1
+    idx += 1
+
+  if not project_name:
+    print("Error: project name is required." ,file=sys.stderr)
+    return 1
+
+  return work(project_name ,remotes)
+
+if __name__ == "__main__":
+  sys.exit(CLI())
diff --git a/administrator/tool/setup b/administrator/tool/setup
new file mode 100644 (file)
index 0000000..0b993ad
--- /dev/null
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+script_afp=$(realpath "${BASH_SOURCE[0]}")
+
diff --git a/developer/document/2025-01-03_notes.txt b/developer/document/2025-01-03_notes.txt
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/document/doc_1.html b/document/doc_1.html
new file mode 100644 (file)
index 0000000..813f900
--- /dev/null
@@ -0,0 +1,173 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <title>Subu User Containers</title>
+    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap" rel="stylesheet">
+    
+    <script src="style/body_visibility_hidden.js"></script>
+    <script>
+      window.StyleRT.body_visibility_hidden();
+    </script>
+  </head>
+  <body>
+    <RT-article>
+      <RT-title 
+        author="Thomas Walker Lynch" 
+        date="2026-03-02 20:38" 
+        title="Subu User Containers">
+      </RT-title>
+
+      <RT-TOC level="1"></RT-TOC>
+
+      <h1>Introduction</h1>
+
+      <p>
+        A <RT-neologism>Subu</RT-neologism> is an isolated user environment operating within a broader parent system. It functions as a strict <RT-neologism>User Container</RT-neologism>. Each subu possesses its own namespace, isolated processes, and dedicated I/O resources.
+      </p>
+
+      <p>
+        The core architectural goal of the subu system is administration without elevation. A <RT-neologism>Master User</RT-neologism> has the ability to completely administer a subordinate user environment without requiring <RT-code>sudo</RT-code> privileges or system-wide root access. The master user accomplishes this through a dedicated suite of tools and a specialized file system architecture.
+      </p>
+
+      <h1>Design Decisions and Architecture</h1>
+
+      <h2>Namespacing</h2>
+
+      <p>
+        Subu accounts require a naming convention that links them logically to their master user while remaining fully compliant with standard Linux utilities. The system utilizes the middle dot (<RT-code>·</RT-code>) as the namespace separator.
+      </p>
+
+      <p>
+        An example account name is <RT-code>Thomas·developer</RT-code>.
+      </p>
+
+      <p>
+        The middle dot offers several distinct advantages over traditional separators. The colon (<RT-code>:</RT-code>) is strictly forbidden, as it acts as the field delimiter in <RT-code>/etc/passwd</RT-code> and will corrupt the system configuration. The hyphen (<RT-code>-</RT-code>) and underscore (<RT-code>_</RT-code>) are heavily utilized in standard user naming conventions, increasing the probability of accidental collisions. The middle dot is visually clean, requires a specific <RT-code>--badname</RT-code> flag during account creation, and presents a typing barrier for standard users, practically eliminating accidental naming conflicts.
+      </p>
+
+      <h2>The File System Dilemma</h2>
+
+      <p>
+        Attempting to nest subu home directories within the master user's home directory (e.g., <RT-code>/home/Thomas/subu/developer</RT-code>) creates an intractable conflict with <RT-term>POSIX</RT-term> permissions and <RT-term>Access Control Lists</RT-term> (ACLs). 
+      </p>
+
+      <p>
+        If a subu is nested, the operating system requires group execute permissions on the master's home directory to allow traversal. If a person attempts to restrict this using ACL masks, the subu retains sovereign control over its own files. A subu executing a standard <RT-code>chmod 600</RT-code> on a file recalculates the ACL mask to zero, instantly severing the master user's access to that file and causing subsequent system backups to fail with permission errors.
+      </p>
+
+      <h2>The Dual Layer Architecture</h2>
+
+      <p>
+        To resolve the file system dilemma, the subu architecture utilizes a dual layer approach, splitting the physical data location from the administrative view.
+      </p>
+
+      <h3>The Physical Layer</h3>
+
+      <p>
+        Every subu home directory resides directly under <RT-code>/home</RT-code>, entirely flat and parallel to the master user account. 
+      </p>
+
+      <RT-code>
+        /home/Thomas
+        /home/Thomas·developer
+      </RT-code>
+
+      <p>
+        Placing the directories side by side eliminates traversal conflicts. The subu relies purely on standard POSIX ownership of its dedicated path, ensuring absolute sovereign control over its workspace and preventing permission changes from breaking the directory structure.
+      </p>
+
+      <h3>The Management Layer</h3>
+
+      <p>
+        To grant the master user administrative access without violating the physical layer permissions, the system utilizes <RT-term>ID-mapped mounts</RT-term>.       </p>
+
+      <p>
+        A system service establishes a mount point within the master user's workspace, pointing to the subu's physical directory:
+      </p>
+
+      <RT-code>
+        /home/Thomas/subu/Thomas·developer  ->  /home/Thomas·developer
+      </RT-code>
+
+      <p>
+        The kernel translates the subu's user ID to the master user's user ID in real-time. Through this mount, the master user perceives absolute ownership of all subu files. If the subu locks down a file in the physical layer, the master user retains the ability to read, modify, or repair it through the management layer.
+      </p>
+
+      <h2>Hardware Access and Audio Seating</h2>
+
+      <p>
+        Because subu accounts utilize <RT-code>logind</RT-code> to acquire their own session seats, they inherit strict hardware isolation rules. Modern Linux dynamically assigns hardware ACLs (such as audio and graphics nodes) to the active seated user. The first user to claim the audio socket locks out all other background sessions.
+      </p>
+
+      <p>
+        To allow multiple subu containers to output sound or utilize graphics hardware simultaneously, the system abandons per-user daemons. The architecture relies on system-wide mixing daemons, forcing subu applications to route their streams through local UNIX sockets to a central service, bypassing the restrictive physical hardware locks entirely.
+      </p>
+
+      <h1>System Setup and Initialization</h1>
+
+      <p>
+        The architecture depends on a dedicated setup account to bootstrap the environment during system boot. 
+      </p>
+
+      <p>
+        If a subu or master user creates an ID-mapped mount directly during their session, the Linux login daemon (<RT-code>systemd-logind</RT-code>) will aggressively terminate the background FUSE processes upon logout, destroying the management layer. 
+      </p>
+
+      <p>
+        The system setup process executes independently of user sessions. It queries a central SQLite database to determine the registered subu relationships, constructs the <RT-code>/home/master/subu</RT-code> directory structures, and establishes the ID-mapped mounts. Crucially, the setup process assigns ownership of these mounts to the master account. Because the mounts are created by a boot-level service, they persist indefinitely, surviving all individual user logouts.
+      </p>
+
+      <h1>User Manual</h1>
+
+      <h2>Locating Files and Directories</h2>
+
+      <p>
+        Data locations depend on the perspective of the user interacting with the system.
+      </p>
+
+      <h3>For the Subu</h3>
+      <ul>
+        <li><strong>Home Directory:</strong> <RT-code>/home/Thomas·developer</RT-code></li>
+        <li><strong>Perspective:</strong> The subu operates in a standard Linux environment. Files are owned by the subu. All paths behave normally.</li>
+      </ul>
+
+      <h3>For the Master User</h3>
+      <ul>
+        <li><strong>Primary Workspace:</strong> <RT-code>/home/Thomas</RT-code></li>
+        <li><strong>Subu Management Directory:</strong> <RT-code>/home/Thomas/subu/Thomas·developer</RT-code></li>
+        <li><strong>Perspective:</strong> The master user navigates into the <RT-code>subu</RT-code> directory to administer subordinate accounts. All files within this directory appear to be owned by the master user. Modifying a file here instantly modifies the physical file for the subu.</li>
+      </ul>
+
+      <h2>Backups</h2>
+
+      <p>
+        When a master user initiates a backup of their home directory, they must instruct the archiving tool (such as <RT-code>tar</RT-code> or <RT-code>rsync</RT-code>) to respect filesystem boundaries. Passing the <RT-code>--one-file-system</RT-code> flag prevents the backup utility from traversing into the <RT-code>/home/Thomas/subu</RT-code> mounts, avoiding infinite recursion loops and duplicate data capture. The physical subu directories (<RT-code>/home/Thomas·developer</RT-code>) are backed up via separate, independent archive routines.
+      </p>
+
+      <script src="style/style_orchestrator.js"></script>
+      <script>
+        window.StyleRT.style_orchestrator();
+      </script>
+
+<h2>Hardware Access and the Virtualization Horizon</h2>
+
+      <p>
+        Currently, because subu accounts utilize <RT-code>logind</RT-code> to acquire their own session seats, they inherit strict hardware isolation rules. Modern Linux dynamically assigns hardware ACLs (such as audio and primary graphics nodes) to the active seated user. 
+      </p>
+
+      <p>
+        The system currently accepts a "first to grab" behavior for these resources. The first subu session to initialize and claim the audio socket or the primary display node holds the exclusive lock for the duration of its session.
+      </p>
+
+      <p>
+        The long term architectural goal is to eliminate this bottleneck by virtualizing the graphics card and distributing it among multiple users, thereby maintaining strict isolation without requiring exclusive locks.
+      </p>
+
+      <p>
+        Fundamentally, hardware virtualization and user process or memory virtualization are identical solutions. The historical separation between a virtual machine and a user namespace represents an incomplete modernization of the operating system user model. The subu architecture views these concepts as a unified system. Future iterations will seek to integrate an existing Linux GPU virtualization layer directly into the user container, granting each subu parallel, isolated access to the underlying hardware substrate.
+      </p>
+
+    </RT-article>
+  </body>
+</html>
diff --git a/document/doc_2.html b/document/doc_2.html
new file mode 100644 (file)
index 0000000..0c310c3
--- /dev/null
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <title>Subu User Containers</title>
+    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap" rel="stylesheet">
+    
+    <script src="style/body_visibility_hidden.js"></script>
+    <script>
+      window.StyleRT.body_visibility_hidden();
+    </script>
+  </head>
+  <body>
+    <RT-article>
+      <RT-title 
+        author="Thomas Walker Lynch" 
+        date="2026-03-03 06:08" 
+        title="Subu User Containers">
+      </RT-title>
+
+      <RT-TOC level="1"></RT-TOC>
+
+      <h1>Introduction</h1>
+
+      <p>
+        A <RT-neologism>Subu</RT-neologism> is an isolated user environment operating within a broader parent system. It functions as a strict <RT-neologism>User Container</RT-neologism>. Each subu possesses its own namespace, isolated processes, and dedicated I/O resources.
+      </p>
+
+      <p>
+        The core architectural goal of the subu system is administration without elevation. A <RT-neologism>Master User</RT-neologism> has the ability to completely administer a subordinate user environment without requiring <RT-code>sudo</RT-code> privileges or system-wide root access. The master user accomplishes this through a dedicated suite of tools and a specialized file system architecture.
+      </p>
+
+      <h1>Design Decisions and Architecture</h1>
+
+      <h2>Namespacing</h2>
+
+      <p>
+        Subu accounts require a naming convention that links them logically to their master user while remaining fully compliant with legacy Linux utilities. The system utilizes the underscore (<RT-code>_</RT-code>) as the namespace separator, or appends a sequential integer or hash.
+      </p>
+
+      <p>
+        Example account names are <RT-code>Thomas_developer</RT-code> and <RT-code>Thomas_incommon</RT-code>.
+      </p>
+
+      <p>
+        Historically, the architecture explored using the middle dot as a separator. However, strict POSIX compliance and legacy toolchains, such as Emacs TRAMP regular expressions, fundamentally reject non-ASCII characters in hostnames and usernames. Adopting the underscore ensures absolute compatibility across the entire Linux ecosystem without requiring complex workarounds. To manage path length and organizational clarity, the system relies on an SQLite database to map these system-level names to human-readable subu aliases.
+      </p>
+
+      <h2>The File System Dilemma</h2>
+
+      <p>
+        Attempting to nest subu home directories within the master user's home directory (e.g., <RT-code>/home/Thomas/subu/developer</RT-code>) creates an intractable conflict with <RT-term>POSIX</RT-term> permissions and <RT-term>Access Control Lists</RT-term> (ACLs). 
+      </p>
+
+      <p>
+        If a subu is nested, the operating system requires group execute permissions on the master's home directory to allow traversal. If a person attempts to restrict this using ACL masks, the subu retains sovereign control over its own files. A subu executing a standard <RT-code>chmod 600</RT-code> on a file recalculates the ACL mask to zero, instantly severing the master user's access to that file and causing subsequent system backups to fail with permission errors.
+      </p>
+
+      <h2>The Dual Layer Architecture</h2>
+
+      <p>
+        To resolve the file system dilemma, the subu architecture utilizes a dual layer approach, splitting the physical data location from the administrative view.
+      </p>
+
+      <h3>The Physical Layer</h3>
+
+      <p>
+        Every subu home directory resides directly under <RT-code>/home</RT-code>, entirely flat and parallel to the master user account. 
+      </p>
+
+      <RT-code>
+        /home/Thomas
+        /home/Thomas_developer
+      </RT-code>
+
+      <p>
+        Placing the directories side by side eliminates traversal conflicts. The subu relies purely on standard POSIX ownership of its dedicated path, ensuring absolute sovereign control over its workspace and preventing permission changes from breaking the directory structure.
+      </p>
+
+      <h3>The Management Layer</h3>
+
+      <p>
+        To grant the master user administrative access without violating the physical layer permissions, the system utilizes <RT-term>ID-mapped mounts</RT-term>.
+      </p>
+
+      <p>
+        A system service establishes a mount point within the master user's workspace, pointing to the subu's physical directory:
+      </p>
+
+      <RT-code>
+        /home/Thomas/subu/developer  ->  /home/Thomas_developer
+      </RT-code>
+
+      <p>
+        The kernel translates the subu's user ID to the master user's user ID in real-time. Through this mount, the master user perceives absolute ownership of all subu files. If the subu locks down a file in the physical layer, the master user retains the ability to read, modify, or repair it through the management layer.
+      </p>
+
+      <h2>Hardware Access and the Virtualization Horizon</h2>
+
+      <p>
+        Currently, because subu accounts utilize <RT-code>logind</RT-code> to acquire their own session seats, they inherit strict hardware isolation rules. Modern Linux dynamically assigns hardware ACLs (such as audio and primary graphics nodes) to the active seated user. 
+      </p>
+
+      <p>
+        The system currently accepts a "first to grab" behavior for these resources. The first subu session to initialize and claim the audio socket or the primary display node holds the exclusive lock for the duration of its session.
+      </p>
+
+      <p>
+        The long term architectural goal is to eliminate this bottleneck by virtualizing the graphics card and distributing it among multiple users, thereby maintaining strict isolation without requiring exclusive locks.
+      </p>
+
+      <p>
+        Fundamentally, hardware virtualization and user process or memory virtualization are identical solutions. The historical separation between a virtual machine and a user namespace represents an incomplete modernization of the operating system user model. The subu architecture views these concepts as a unified system. Future iterations will seek to integrate an existing Linux GPU virtualization layer directly into the user container, granting each subu parallel, isolated access to the underlying hardware substrate.
+      </p>
+
+      <h1>System Setup and Initialization</h1>
+
+      <p>
+        The architecture depends on a dedicated setup account to bootstrap the environment during system boot. 
+      </p>
+
+      <p>
+        If a subu or master user creates an ID-mapped mount directly during their session, the Linux login daemon (<RT-code>systemd-logind</RT-code>) will aggressively terminate the background FUSE processes upon logout, destroying the management layer. 
+      </p>
+
+      <p>
+        The system setup process executes independently of user sessions. It queries a central SQLite database to determine the registered subu relationships, constructs the <RT-code>/home/master/subu</RT-code> directory structures, and establishes the ID-mapped mounts. Crucially, the setup process assigns ownership of these mounts to the master account. Because the mounts are created by a boot-level service, they persist indefinitely, surviving all individual user logouts.
+      </p>
+
+      <h1>User Manual</h1>
+
+      <h2>Locating Files and Directories</h2>
+
+      <p>
+        Data locations depend on the perspective of the user interacting with the system.
+      </p>
+
+      <h3>For the Subu</h3>
+      <ul>
+        <li><strong>Home Directory:</strong> <RT-code>/home/Thomas_developer</RT-code></li>
+        <li><strong>Perspective:</strong> The subu operates in a standard Linux environment. Files are owned by the subu. All paths behave normally.</li>
+      </ul>
+
+      <h3>For the Master User</h3>
+      <ul>
+        <li><strong>Primary Workspace:</strong> <RT-code>/home/Thomas</RT-code></li>
+        <li><strong>Subu Management Directory:</strong> <RT-code>/home/Thomas/subu/developer</RT-code></li>
+        <li><strong>Perspective:</strong> The master user navigates into the <RT-code>subu</RT-code> directory to administer subordinate accounts. All files within this directory appear to be owned by the master user. Modifying a file here instantly modifies the physical file for the subu.</li>
+      </ul>
+
+      <h2>Backups</h2>
+
+      <p>
+        When a person initiates a backup of their home directory, they must instruct the archiving tool (such as <RT-code>tar</RT-code> or <RT-code>rsync</RT-code>) to respect filesystem boundaries. Passing the <RT-code>--one-file-system</RT-code> flag prevents the backup utility from traversing into the <RT-code>/home/Thomas/subu</RT-code> mounts, avoiding infinite recursion loops and duplicate data capture. The physical subu directories are backed up via separate, independent archive routines.
+      </p>
+
+      <script src="style/style_orchestrator.js"></script>
+      <script>
+        window.StyleRT.style_orchestrator();
+      </script>
+
+    </RT-article>
+  </body>
+</html>
diff --git a/document/install.org b/document/install.org
new file mode 100644 (file)
index 0000000..4c18a72
--- /dev/null
@@ -0,0 +1,13 @@
+
+1. edit these files:
+
+  /etc/crypttab
+    <mapname>-crypt  UUID=<UUID-of-partition>  none  luks
+
+
+  /etc/fstab
+    /dev/mapper/<mapname>-crypt  /mnt/<mapname>  ext4  noatime,nofail,x-systemd.device-timeout=30  0 2
+
+
+2. Unlock + mount the device at boot
+  
diff --git a/document/style/RT_TOC.js b/document/style/RT_TOC.js
new file mode 100644 (file)
index 0000000..86ee51a
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+  Processes <RT-TOC> tags.
+  Populates each with headings found below it.
+  
+  Attributes:
+    level="N" : Explicitly sets the target heading level (1-6).
+                e.g., level="1" collects H1s. level="2" collects H2s.
+                Stops collecting if it hits a heading of (level - 1) or higher.
+  
+  Default (No attribute):
+    Context-Aware. Looks backwards for the nearest heading H(N).
+    Targets H(N+1). Stops at the next H(N).
+*/
+window.StyleRT = window.StyleRT || {};
+
+window.StyleRT.RT_TOC = function() {
+  const debug = window.StyleRT.debug || { log: function(){} };
+  const toc_tags = document.querySelectorAll('rt-toc');
+
+  toc_tags.forEach((container, toc_index) => {
+    container.style.display = 'block';
+    
+    // 1. Determine Target Level
+    const attr_level = parseInt(container.getAttribute('level'));
+    let target_level;
+    
+    if (!isNaN(attr_level)) {
+       // EXPLICIT MODE
+       target_level = attr_level;
+       if (debug.log) debug.log('RT_TOC', `TOC #${toc_index} explicit target: H${target_level}`);
+    } else {
+       // IMPLICIT / CONTEXT MODE
+       let context_level = 0; // Default 0 (Root)
+       let prev = container.previousElementSibling;
+       while (prev) {
+         const match = prev.tagName.match(/^H([1-6])$/);
+         if (match) {
+           context_level = parseInt(match[1]);
+           break;
+         }
+         prev = prev.previousElementSibling;
+       }
+       target_level = context_level + 1;
+       if (debug.log) debug.log('RT_TOC', `TOC #${toc_index} context implied target: H${target_level}`);
+    }
+
+    // Stop condition: Stop if we hit a heading that is a "parent" or "sibling" of the context.
+    // Mathematically: Stop if found_level < target_level.
+    const stop_threshold = target_level; 
+
+    // 2. Setup Container
+    container.innerHTML = ''; 
+    const title = document.createElement('h1');
+    // Title logic: If targeting H1, it's a Main TOC. Otherwise it's a Section TOC.
+    title.textContent = target_level === 1 ? 'Table of Contents' : 'Section Contents';
+    title.style.textAlign = 'center';
+    container.appendChild(title);
+
+    const list = document.createElement('ul');
+    list.style.listStyle = 'none';
+    list.style.paddingLeft = '0';
+    container.appendChild(list);
+
+    // 3. Scan Forward
+    let next_el = container.nextElementSibling;
+    while (next_el) {
+      const match = next_el.tagName.match(/^H([1-6])$/);
+      if (match) {
+        const found_level = parseInt(match[1]);
+
+        // STOP Logic:
+        // If we are looking for H2s, we stop if we hit an H1 (level 1).
+        // If we are looking for H1s, we stop if we hit... nothing (level 0).
+        if (found_level < target_level) {
+          break;
+        }
+
+        // COLLECT Logic:
+        if (found_level === target_level) {
+          if (!next_el.id) next_el.id = `toc-ref-${toc_index}-${found_level}-${list.children.length}`;
+
+          const li = document.createElement('li');
+          li.style.marginBottom = '0.5rem';
+
+          const a = document.createElement('a');
+          a.href = `#${next_el.id}`;
+          a.textContent = next_el.textContent;
+          a.style.textDecoration = 'none';
+          a.style.color = 'inherit';
+          a.style.display = 'block';
+
+          a.onmouseover = () => a.style.color = 'var(--rt-brand-primary)'; 
+          a.onmouseout = () => a.style.color = 'inherit';
+
+          li.appendChild(a);
+          list.appendChild(li);
+        }
+      }
+      next_el = next_el.nextElementSibling;
+    }
+  });
+};
diff --git a/document/style/RT_code.js b/document/style/RT_code.js
new file mode 100644 (file)
index 0000000..bc3418c
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+  Processes <RT-CODE> tags.
+  Uses the central config or CSS variables from the theme.
+
+  Removes common indent from lines of code.
+*/
+function RT_code() {
+  const RT = window.StyleRT;
+  const U = RT.utility;
+  const debug = RT.debug;
+
+  debug.log('RT_code', 'Starting render cycle.');
+
+  const metrics = U.measure_ink_ratio('monospace');
+  
+  document.querySelectorAll('rt-code').forEach((el) => {
+    el.style.fontFamily = 'monospace';
+    
+    const computed = window.getComputedStyle(el);
+    const accent = computed.getPropertyValue('--rt-accent').trim() || 'gold';
+    
+    const is_block = U.is_block_content(el);
+    const parentColor = computed.color;
+    const is_text_light = U.is_color_light(parentColor);
+    
+    const alpha = is_block ? 0.08 : 0.15;
+    const overlay = is_text_light ? `rgba(255,255,255,${alpha})` : `rgba(0,0,0,${alpha})`;
+    const text_color = is_text_light ? '#ffffff' : '#000000';
+
+    el.style.backgroundColor = overlay;
+
+    if (is_block) {
+      el.style.display = 'block';
+
+      // --- Tag-Relative Auto-Dedent Logic ---
+      
+      // 1. Get Tag Indentation (The Anchor)
+      let tagIndent = '';
+      const prevNode = el.previousSibling;
+      if (prevNode && prevNode.nodeType === 3) {
+        const prevText = prevNode.nodeValue;
+        const lastNewLineIndex = prevText.lastIndexOf('\n');
+        if (lastNewLineIndex !== -1) {
+          tagIndent = prevText.substring(lastNewLineIndex + 1);
+        } else if (/^\s*$/.test(prevText)) {
+          tagIndent = prevText;
+        }
+      }
+
+      // 2. Calculate Common Leading Whitespace from Content
+      const rawLines = el.textContent.split('\n');
+      
+      // Filter out empty lines for calculation purposes so they don't break the logic
+      const contentLines = rawLines.filter(line => line.trim().length > 0);
+
+      let commonIndent = null;
+
+      if (contentLines.length > 0) {
+        // Assume the first line sets the standard
+        const firstMatch = contentLines[0].match(/^\s*/);
+        commonIndent = firstMatch ? firstMatch[0] : '';
+
+        // Reduce the commonIndent if subsequent lines have LESS indentation
+        for (let i = 1; i < contentLines.length; i++) {
+          const line = contentLines[i];
+          // Determine how much of commonIndent this line shares
+          let j = 0;
+          while (j < commonIndent.length && j < line.length && commonIndent[j] === line[j]) {
+            j++;
+          }
+          commonIndent = commonIndent.substring(0, j);
+          if (commonIndent.length === 0) break; // Optimization
+        }
+      } else {
+        commonIndent = '';
+      }
+
+      // 3. Process Content
+      // Rule: Only strip if the Common Indent contains the Tag Indent (Safety Check)
+      // This handles the Emacs case: Tag is "  ", Common is "    ". "    " starts with "  ".
+      // We strip "    ", leaving the code flush left.
+      let finalString = '';
+
+      if (commonIndent.length > 0 && commonIndent.startsWith(tagIndent)) {
+         const cleanedLines = rawLines.map(line => {
+            // Strip the common indent from valid lines
+            return line.startsWith(commonIndent) ? line.replace(commonIndent, '') : line;
+         });
+
+         // Remove artifact lines (first/last empty lines)
+         if (cleanedLines.length > 0 && cleanedLines[0].length === 0) {
+           cleanedLines.shift();
+         }
+         if (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1].trim().length === 0) {
+            cleanedLines.pop();
+         }
+         finalString = cleanedLines.join('\n');
+      } else {
+         // Fallback: Code is to the left of the tag or weirdly formatted. 
+         // Just trim the wrapper newlines.
+         finalString = el.textContent.trim();
+      }
+
+      el.textContent = finalString;
+      // --- End Indentation Logic ---
+
+      el.style.whiteSpace = 'pre';
+      el.style.fontSize = (parseFloat(computed.fontSize) * metrics.ratio * 0.95) + 'px'; 
+      el.style.padding = '1.2rem';
+      el.style.margin = '1.5rem 0';
+      el.style.borderLeft = `4px solid ${accent}`;
+      el.style.color = 'inherit'; 
+    } else {
+      el.style.display = 'inline';
+      const exactPx = parseFloat(computed.fontSize) * metrics.ratio * 1.0; 
+      el.style.fontSize = exactPx + 'px';
+      el.style.padding = '0.1rem 0.35rem';
+      el.style.borderRadius = '3px';
+      const offsetPx = metrics.baseline_diff * (exactPx / 100);
+      el.style.verticalAlign = offsetPx + 'px';
+      el.style.color = text_color; 
+    }
+  });
+  
+  debug.log('RT_code', 'Render cycle complete.');
+}
+
+window.StyleRT = window.StyleRT || {};
+window.StyleRT.RT_code = RT_code;
diff --git a/document/style/RT_math.js b/document/style/RT_math.js
new file mode 100644 (file)
index 0000000..2d07cfa
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+  Processes <RT-MATH> tags.
+  JavaScript: RT_math() 
+  HTML Tag: <RT-MATH> (parsed as rt-math)
+*/
+function RT_math(){
+  // querySelector treats 'rt-math' as case-insensitive for the tag
+  document.querySelectorAll('rt-math').forEach(el => {
+    if (el.textContent.startsWith('$')) return;
+
+    const is_block = el.parentElement.tagName === 'DIV' || 
+                     el.textContent.includes('\n') ||
+                     el.parentElement.childNodes.length === 1;
+
+    const delimiter = is_block ? '$$' : '$';
+    el.style.display = is_block ? 'block' : 'inline';
+    el.textContent = `${delimiter}${el.textContent.trim()}${delimiter}`;
+  });
+
+  // MathJax must find its config at window.MathJax
+  window.MathJax = {
+    tex: { 
+      inlineMath: [['$', '$']], 
+      displayMath: [['$$', '$$']] 
+    }
+  };
+
+  const script = document.createElement('script');
+  script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
+  script.async = true;
+  document.head.appendChild(script);
+}
+
+window.StyleRT = window.StyleRT || {};
+window.StyleRT.RT_math = RT_math;
diff --git a/document/style/RT_term.js b/document/style/RT_term.js
new file mode 100644 (file)
index 0000000..1dec8fb
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+  Processes <RT-TERM> and <RT-NEOLOGISM> tags.
+  - Styles only the first occurrence of a unique term/neologism.
+  - The "-em" variants (e.g., <RT-term-em>) are always styled.
+  - Automatically generates IDs for first occurrences for future indexing.
+*/
+
+window.StyleRT = window.StyleRT || {};
+
+window.StyleRT.RT_term = function() {
+  const RT = window.StyleRT;
+
+  const debug = RT.debug || {
+    log: function() {}
+    ,warn: function() {}
+    ,error: function() {}
+  };
+
+  const DEBUG_TOKEN_S = 'term';
+
+  try {
+    // Track seen terms so only the first occurrence is decorated
+    const seen_terms_dpa = new Set();
+
+    const apply_style = (el, is_neologism_b) => {
+      el.style.fontStyle = 'italic';
+      el.style.fontWeight = is_neologism_b ? '600' : '500';
+      el.style.color = is_neologism_b
+        ? 'var(--rt-brand-secondary)'
+        : 'var(--rt-brand-primary)';
+      el.style.paddingRight = '0.1em'; // Compensation for italic slant
+      el.style.display = 'inline';
+    };
+
+    const clear_style = (el) => {
+      el.style.fontStyle = 'normal';
+      el.style.color = 'inherit';
+      el.style.fontWeight = 'inherit';
+      el.style.paddingRight = '';
+      el.style.display = '';
+    };
+
+    const selector_s = [
+      'rt-term'
+      ,'rt-term-em'
+      ,'rt-neologism'
+      ,'rt-neologism-em'
+    ].join(',');
+
+    const tags_dpa = document.querySelectorAll(selector_s);
+
+    debug.log(DEBUG_TOKEN_S, `Scanning ${tags_dpa.length} term tags`);
+
+    tags_dpa.forEach(el => {
+      const tag_name_s = el.tagName.toLowerCase();
+      const is_neologism_b = tag_name_s.includes('neologism');
+      const is_explicit_em_b = tag_name_s.endsWith('-em');
+
+      const term_text_raw_s = (el.textContent || '').trim();
+      if (!term_text_raw_s.length) {
+        debug.warn(DEBUG_TOKEN_S, `Empty term tag encountered: <${tag_name_s}>`);
+        return;
+      }
+
+      // Normalize text for uniqueness tracking
+      const term_norm_s = term_text_raw_s.toLowerCase();
+
+      // Slug for ID generation (simple + stable)
+      const slug_s = term_norm_s.replace(/\s+/g, '-');
+
+      const is_first_occurrence_b = !seen_terms_dpa.has(term_norm_s);
+
+      if (is_explicit_em_b || is_first_occurrence_b) {
+        apply_style(el, is_neologism_b);
+
+        if (!is_explicit_em_b && is_first_occurrence_b) {
+          seen_terms_dpa.add(term_norm_s);
+
+          if (!el.id) {
+            el.id = `def-${is_neologism_b ? 'neo-' : ''}${slug_s}`;
+            debug.log(
+              DEBUG_TOKEN_S
+              ,`First occurrence: "${term_norm_s}" -> id="${el.id}"`
+            );
+          } else {
+            debug.log(
+              DEBUG_TOKEN_S
+              ,`First occurrence: "${term_norm_s}" (existing id="${el.id}")`
+            );
+          }
+        } else if (is_explicit_em_b) {
+          debug.log(
+            DEBUG_TOKEN_S
+            ,`Emphasized occurrence: "${term_norm_s}" (<${tag_name_s}>)`
+          );
+        }
+      } else {
+        // Subsequent mentions render as normal prose
+        clear_style(el);
+      }
+    });
+
+    debug.log(DEBUG_TOKEN_S, `Unique terms defined: ${seen_terms_dpa.size}`);
+  } catch (e) {
+    debug.error('error', `RT_term failed: ${e && e.message ? e.message : String(e)}`);
+  }
+};
diff --git a/document/style/RT_title.js b/document/style/RT_title.js
new file mode 100644 (file)
index 0000000..93757f8
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+  Processes <RT-TITLE> tags.
+  Generates a standard document header block.
+  
+  Usage: 
+  <RT-title title="..." author="..." date="..."></RT-title>
+*/
+window.StyleRT = window.StyleRT || {};
+
+window.StyleRT.RT_title = function() {
+  const debug = window.StyleRT.debug || { log: function(){} };
+  
+  document.querySelectorAll('rt-title').forEach(el => {
+    const title = el.getAttribute('title') || 'Untitled Document';
+    const author = el.getAttribute('author');
+    const date = el.getAttribute('date');
+
+    if (debug.log) debug.log('RT_title', `Generating title block: ${title}`);
+
+    // Container
+    const container = document.createElement('div');
+    container.style.textAlign = 'center';
+    container.style.marginBottom = '3rem';
+    container.style.marginTop = '2rem';
+    container.style.borderBottom = '1px solid var(--rt-border-default)';
+    container.style.paddingBottom = '1.5rem';
+
+    // Main Title (H1)
+    const h1 = document.createElement('h1');
+    h1.textContent = title;
+    h1.style.margin = '0 0 0.8rem 0';
+    h1.style.border = 'none'; // Override standard H1 border
+    h1.style.padding = '0';
+    h1.style.color = 'var(--rt-brand-primary)';
+    h1.style.fontSize = '2.5em';
+    h1.style.lineHeight = '1.1';
+    h1.style.letterSpacing = '-0.03em';
+
+    container.appendChild(h1);
+
+    // Metadata Row (Author | Date)
+    if (author || date) {
+      const meta = document.createElement('div');
+      meta.style.color = 'var(--rt-content-muted)';
+      meta.style.fontStyle = 'italic';
+      meta.style.fontSize = '1.1em';
+      meta.style.fontFamily = '"Georgia", "Times New Roman", serif'; // Classy serif
+
+      const parts = [];
+      if (author) parts.push(`<span style="font-weight:600; color:var(--rt-brand-secondary)">${author}</span>`);
+      if (date) parts.push(date);
+
+      meta.innerHTML = parts.join(' &nbsp;&mdash;&nbsp; ');
+      container.appendChild(meta);
+    }
+
+    // Replace the raw tag with the generated block
+    el.replaceWith(container);
+  });
+};
diff --git a/document/style/Rubio.js b/document/style/Rubio.js
new file mode 100644 (file)
index 0000000..cd21a5a
--- /dev/null
@@ -0,0 +1,15 @@
+/* Style: The "State Department" Override
+  Description: Restores decorum. 
+*/
+(function(){
+   const RT = window.StyleRT || {};
+   
+   // Force the font regardless of other settings
+   RT.rubio = function() {
+      const articles = document.querySelectorAll("rt-article");
+      articles.forEach(el => {
+          el.style.fontFamily = '"Times New Roman", "Times", serif';
+          el.style.letterSpacing = "0px"; // No modern spacing allowed
+      });
+   };
+})();
diff --git a/document/style/article_tech_ref.js b/document/style/article_tech_ref.js
new file mode 100644 (file)
index 0000000..8ad9dbb
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+  Article Layout: Technical Reference
+  Standard: Theme 1.0
+  Description: High-readability layout for technical documentation on screens.
+  Features: Sans-serif, justified text, distinct headers, boxed code.
+*/
+(function(){
+  const RT = window.StyleRT = window.StyleRT || {};
+
+  RT.article = function() {
+    const debug = RT.debug || { log: function(){} };
+    debug.log('layout', 'RT.article starting...');
+
+    RT.config = RT.config || {};
+    
+    // Default Configuration
+    RT.config.article = {
+       font_family: '"Noto Sans", "Segoe UI", "Helvetica Neue", sans-serif'
+      ,line_height: "1.8"       
+      ,font_size: "16px"        
+      ,font_weight: "400"       // Default (String)
+      ,max_width: "820px" 
+      ,margin: "0 auto"
+    };
+
+    // SAFE THEME DETECTION
+    // If the theme is loaded and explicitly Light, bump the weight.
+    try {
+      if (RT.config.theme && RT.config.theme.meta_is_dark === false) {
+         RT.config.article.font_weight = "600";
+         debug.log('layout', 'Light theme detected: adjusting font weight to 600.');
+      }
+    } catch(e) {
+      console.warn("StyleRT: Auto-weight adjustment failed, using default.", e);
+    }
+
+    const conf = RT.config.article;
+    const article_seq = document.querySelectorAll("RT-article");
+
+    if(article_seq.length === 0) {
+      debug.log('layout', 'No <RT-article> elements found. Exiting.');
+      return;
+    }
+
+    // 1. Apply Container Styles
+    article_seq.forEach( (article) =>{
+      const style = article.style;
+      style.display = "block";
+      style.fontFamily = conf.font_family;
+      style.fontSize = conf.font_size;
+      style.lineHeight = conf.line_height;
+      style.fontWeight = conf.font_weight;
+      style.maxWidth = conf.max_width;
+      style.margin = conf.margin;
+      style.padding = "0 20px";
+      style.color = "var(--rt-content-main)";
+    });
+
+    // 2. Inject Child Typography
+    const style_id = 'rt-article-typography';
+    if (!document.getElementById(style_id)) {
+      debug.log('layout', 'Injecting CSS typography rules.');
+      const style_el = document.createElement('style');
+      style_el.id = style_id;
+      
+      style_el.textContent = `
+        /* --- HEADERS --- */
+        rt-article h1 { 
+          color: var(--rt-brand-primary);
+          font-size: 2.0em; 
+          font-weight: 500; 
+          text-align: center;
+          margin-top: 1.2em; 
+          margin-bottom: 0.6em; 
+          border-bottom: 2px solid var(--rt-border-default);
+          padding-bottom: 0.3em;
+          line-height: 1.2;
+          letter-spacing: -0.02em;
+        }
+
+        rt-article h2 { 
+          color: var(--rt-brand-secondary);
+          font-size: 1.5em; 
+          font-weight: 400; 
+          text-align: center;
+          margin-top: 1.0em; 
+          margin-bottom: 0.5em; 
+        }
+
+        rt-article h2 + h3 {
+           margin-top: -0.3em; 
+           padding-top: 0;
+        }
+
+        rt-article h3 { 
+          color: var(--rt-brand-tertiary);
+          font-size: 1.4em; 
+          font-weight: 400;
+          margin-top: 1.0em; 
+          margin-bottom: 0.5em;
+        }
+        
+        /* --- DEEP LEVELS (H4-H6) --- */
+        rt-article h4, rt-article h5, rt-article h6 {
+           color: var(--rt-brand-tertiary);
+           font-weight: bold;
+           margin-top: 1.2em;
+           font-style: italic;
+        }
+        rt-article h4 { margin-left: 2em; }
+        rt-article h5 { margin-left: 4em; }
+        rt-article h6 { margin-left: 6em; }
+
+        /* --- BODY TEXT --- */
+        rt-article p { 
+          margin-bottom: 1.4em; 
+          text-align: justify; 
+          hyphens: auto;
+          color: var(--rt-content-main);
+        }
+
+        /* --- RICH ELEMENTS --- */
+        rt-article blockquote { 
+          border-left: 4px solid var(--rt-brand-secondary); 
+          margin: 1.5em 0; 
+          padding: 0.5em 1em; 
+          font-style: italic; 
+          color: var(--rt-content-muted);
+          background: var(--rt-surface-1);
+          border-radius: 0 4px 4px 0;
+        }
+
+        rt-article ul, rt-article ol {
+          margin-bottom: 1.4em;
+          padding-left: 2em;
+        }
+        rt-article li {
+           margin-bottom: 0.4em;
+        }
+        rt-article li::marker {
+          color: var(--rt-brand-secondary);
+          font-weight: bold;
+        }
+        
+        /* Links */
+        rt-article a {
+          color: var(--rt-brand-link);
+          text-decoration: none;
+          border-bottom: 1px dotted var(--rt-border-default);
+          transition: all 0.2s;
+        }
+        rt-article a:hover {
+          color: var(--rt-brand-primary);
+          border-bottom: 1px solid var(--rt-brand-primary);
+          background: var(--rt-surface-1);
+        }
+        
+        /* --- TECHNICAL --- */
+        rt-article pre {
+           background: var(--rt-surface-code);
+           padding: 1em;
+           border-radius: 4px;
+           overflow-x: auto;
+           border: 1px solid var(--rt-border-default);
+        }
+      `;
+      document.head.appendChild(style_el);
+    }
+  };
+})();
diff --git a/document/style/body_visibility_hidden.js b/document/style/body_visibility_hidden.js
new file mode 100644 (file)
index 0000000..d6a178a
--- /dev/null
@@ -0,0 +1,12 @@
+/*
+  Targets the root element to ensure total blackout during load.
+*/
+function body_visibility_hidden(){
+  const gate = document.createElement('style');
+  gate.id = 'rt-visibility-gate';
+  gate.textContent = 'html{visibility:hidden !important; background:black !important;}';
+  document.head.appendChild(gate);
+}
+
+window.StyleRT = window.StyleRT || {};
+window.StyleRT.body_visibility_hidden = body_visibility_hidden;
diff --git a/document/style/body_visibility_visible.js b/document/style/body_visibility_visible.js
new file mode 100644 (file)
index 0000000..ff1c4b6
--- /dev/null
@@ -0,0 +1,13 @@
+/*
+  Restores visibility by removing the visibility gate.
+*/
+function body_visibility_visible(){
+  const gate = document.getElementById('rt-visibility-gate');
+  if (gate){
+    gate.remove();
+  }
+  document.body.style.visibility = 'visible';
+}
+
+window.StyleRT = window.StyleRT || {};
+window.StyleRT.body_visibility_visible = body_visibility_visible;
diff --git a/document/style/custom_tag.txt b/document/style/custom_tag.txt
new file mode 100644 (file)
index 0000000..146d3ad
--- /dev/null
@@ -0,0 +1,8 @@
+  <RT-article>: The root container.
+     <RT-page>: The page wrapper.
+
+<RT-math>
+<RT-code>
+
+     <RT-term>: For Conventional terms (standard definitions)
+<RT-neologism>: Terms introduced in this article
diff --git a/document/style/page_fixed_glow.js b/document/style/page_fixed_glow.js
new file mode 100644 (file)
index 0000000..91bd648
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+  Page Layout: Fixed Glow
+  Standard: Theme 1.0
+  Description: A fixed-height container with a glowing border effect that matches the active theme.
+*/
+(function(){
+  const RT = window.StyleRT = window.StyleRT || {};
+
+  // Function name stays generic so the orchestrator can call RT.page() regardless of file choice
+  RT.page = function() {
+    RT.config = RT.config || {};
+    
+    // Default Configuration
+    // We use CSS Variables here so the Theme controls the actual colors.
+    const defaults = {
+       width: "100%"
+      ,height: "1056px"
+      ,padding: "3rem"
+      ,margin: "4rem auto"
+      
+      // Dynamic Theme Bindings
+      ,bg_color:     "var(--rt-surface-0)"         // Black (Dark) or Cream (Light)
+      ,border_color: "var(--rt-brand-primary)"     // The Main Accent Color
+      ,text_color:   "var(--rt-brand-primary)"     // Page Number Color
+      
+      // The Glow: Uses the primary brand color for the shadow
+      ,shadow: "drop-shadow(0px 0px 15px var(--rt-brand-primary))" 
+    };
+
+    // Merge defaults
+    RT.config.page = Object.assign({}, defaults, RT.config.page || {});
+
+    const conf = RT.config.page;
+    const style_id = 'rt-page-fixed-glow';
+    
+    if (!document.getElementById(style_id)) {
+      const style_el = document.createElement('style');
+      style_el.id = style_id;
+      
+      style_el.textContent = `
+        /* Reset page counter on the article container */
+        rt-article {
+          counter-reset: rt-page-counter;
+        }
+
+        rt-page {
+          display: block;
+          position: relative;
+          box-sizing: border-box;
+          overflow: hidden; 
+
+          /* Dimensions */
+          width: ${conf.width};
+          height: ${conf.height};
+          margin: ${conf.margin};
+          padding: ${conf.padding};
+          
+          /* Theming */
+          background-color: ${conf.bg_color}; 
+          border: 1px solid ${conf.border_color};
+          
+          /* The "Glow" Effect */
+          filter: ${conf.shadow};
+          
+          /* Counter Increment */
+          counter-increment: rt-page-counter;
+        }
+
+        /* Page Numbering */
+        rt-page::after {
+          content: "Page " counter(rt-page-counter);
+          position: absolute;
+          bottom: 1.5rem;
+          right: 3rem;
+          
+          font-family: "Noto Sans", sans-serif;
+          font-size: 0.9rem;
+          font-weight: bold;
+          
+          color: ${conf.text_color}; 
+          opacity: 0.8;
+          pointer-events: none; /* Prevent interference with clicks */
+        }
+      `;
+      document.head.appendChild(style_el);
+    }
+  };
+})();
diff --git a/document/style/paginate_by_element.js b/document/style/paginate_by_element.js
new file mode 100644 (file)
index 0000000..e085148
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+  Layout Paginator: paginate_by_element
+*/
+window.StyleRT = window.StyleRT || {};
+
+window.StyleRT.paginate_by_element = function() {
+  const RT = window.StyleRT;
+  
+  // Fix: Read safely without overwriting the config namespace
+  const page_conf = (RT.config && RT.config.page) ? RT.config.page : {};
+  const page_height_limit = page_conf.height_limit || 1000; 
+
+  const article_seq = document.querySelectorAll("RT-article");
+  
+  // HURDLE: Error if no articles found to paginate
+  if(article_seq.length === 0) {
+    RT.debug.error('pagination', 'No <RT-article> elements found. Pagination aborted.');
+    return;
+  }
+
+  article_seq.forEach( (article) => {
+    const raw_elements = Array.from(article.children).filter(el => 
+      !['SCRIPT', 'STYLE', 'RT-PAGE'].includes(el.tagName)
+    );
+
+    if(raw_elements.length === 0) return;
+
+    const pages = [];
+    let current_batch = [];
+    let current_h = 0;
+
+    const get_el_height = (el) => {
+      const rect = el.getBoundingClientRect();
+      const style = window.getComputedStyle(el);
+      const margin = parseFloat(style.marginTop) + parseFloat(style.marginBottom);
+      return (rect.height || 0) + (margin || 0);
+    };
+
+    for (let i = 0; i < raw_elements.length; i++) {
+      const el = raw_elements[i];
+      const h = get_el_height(el);
+      const is_heading = /^H[1-6]/.test(el.tagName);
+
+      let total_required_h = h;
+      if (is_heading && i + 1 < raw_elements.length) {
+        total_required_h += get_el_height(raw_elements[i + 1]);
+      }
+
+      if (current_h + total_required_h > page_height_limit && current_batch.length > 0) {
+        pages.push(current_batch);
+        current_batch = [];
+        current_h = 0;
+      }
+
+      current_batch.push(el);
+      current_h += h;
+    }
+
+    if (current_batch.length > 0) pages.push(current_batch);
+
+    article.innerHTML = ''; 
+    
+    pages.forEach( (list, index) => {
+      const page_el = document.createElement('rt-page');
+      page_el.id = `page-${index+1}`;
+      list.forEach(item => page_el.appendChild(item));
+      article.appendChild(page_el);
+    });
+
+    if (RT.debug) RT.debug.log('pagination', `Article paginated into ${pages.length} pages.`);
+  });
+};
diff --git a/document/style/style_orchestrator.js b/document/style/style_orchestrator.js
new file mode 100644 (file)
index 0000000..755c1c8
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+  Master Loader & Orchestrator for StyleRT.
+*/
+
+window.StyleRT = window.StyleRT || {};
+
+window.StyleRT.style_orchestrator = function() {
+  const RT = window.StyleRT;
+  
+  const modules = [
+    // Theme & Semantics
+    'style/RT_title.js',       
+    'style/theme_dark_gold.js',        
+    'style/RT_term.js',          
+    'style/RT_math.js',          
+    'style/RT_code.js',          
+    'style/article_tech_ref.js',
+    'style/RT_TOC.js',            
+
+    // Layout & Pagination
+    'style/paginate_by_element.js', 
+    'style/page_fixed_glow.js',             
+
+    // Visibility
+    'style/body_visibility_visible.js' 
+  ];
+
+  // 1. Bootloader
+  const utility = document.createElement('script');
+  utility.src = 'style/utility.js';
+  
+  utility.onload = () => { load_next(0); };
+  utility.onerror = () => { console.error("StyleRT: Critical failure - utility.js missing."); };
+  document.head.appendChild(utility);
+
+  // 2. The Chain Loader
+  const load_next = (index) => {
+    if (index >= modules.length) {
+      run_style();
+      return;
+    }
+    const src = modules[index];
+    if (RT.debug) RT.debug.log('style', `Loading: ${src}`);
+
+    const script = document.createElement('script');
+    script.src = src;
+    script.onload = () => load_next(index + 1);
+    script.onerror = () => { 
+      console.error(`StyleRT: Failed load on ${src}`); 
+      load_next(index + 1); 
+    };
+    document.head.appendChild(script);
+  };
+
+  // 3. Phase 1: Semantics
+  const run_style = () => {
+    RT.debug.log('style', 'Starting Phase 1: Setup & Semantics');
+
+    // Naming Convention: RT.<filename_without_js>
+    if(RT.theme) RT.theme();     
+    if(RT.article) RT.article(); 
+    
+    // NEW: Trigger the Title Generator
+    if(RT.RT_title) RT.RT_title(); 
+
+    if(RT.RT_term) RT.RT_term();
+    if(RT.RT_math) RT.RT_math();
+    if(RT.RT_code) RT.RT_code();
+
+    if (window.MathJax && MathJax.Hub && MathJax.Hub.Queue) {
+      RT.debug.log('style', 'MathJax detected. Queueing layout tasks...');
+      MathJax.Hub.Queue(["Typeset", MathJax.Hub], continue_style);
+    } else {
+      continue_style();
+    }
+  };
+
+  // 4. Phase 2: Layout
+  const continue_style = () => {
+    RT.debug.log('style', 'Starting Phase 2: Layout & Reveal');
+    
+    // Debug: Dump the config to see what values we are using
+    if(RT.debug) RT.debug.log('config', JSON.stringify(RT.config || {}, null, 2));
+    
+    if(RT.RT_TOC) RT.RT_TOC();
+    if(RT.paginate_by_element) RT.paginate_by_element();
+    if(RT.page) RT.page(); // Defined in page_css_pn.js
+    if(RT.body_visibility_visible) RT.body_visibility_visible();
+    
+    RT.debug.log('style', 'Style execution complete.');
+  };
+};
diff --git a/document/style/theme_dark_gold.js b/document/style/theme_dark_gold.js
new file mode 100644 (file)
index 0000000..f576147
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+  Theme: Inverse Wheat (Dark)
+  Standard: Theme 1.0
+  Description: High contrast Amber on Deep Charcoal.
+*/
+( function(){
+  const RT = window.StyleRT = window.StyleRT || {};
+  
+  RT.theme = function(){
+    RT.config = RT.config || {};
+    
+    // THEME 1.0 DATA CONTRACT
+    RT.config.theme = {
+       meta_is_dark: true
+      ,meta_name:    "Inverse Wheat"
+
+      // --- SURFACES (Depth & Container Hierarchy) ---
+      ,surface_0:       "hsl(0, 0%, 5%)"      // App Background (Deepest)
+      ,surface_1:       "hsl(0, 0%, 10%)"     // Sidebar / Nav / Panels
+      ,surface_2:       "hsl(0, 0%, 14%)"     // Cards / Floating Elements
+      ,surface_3:       "hsl(0, 0%, 18%)"     // Modals / Dropdowns / Popovers
+      ,surface_input:   "hsl(0, 0%, 12%)"     // Form Inputs
+      ,surface_code:    "hsl(0, 0%, 11%)"     // Code Block Background
+      ,surface_select:  "hsl(45, 100%, 15%)"  // Text Selection Highlight
+
+      // --- CONTENT (Text & Icons) ---
+      ,content_main:    "hsl(50, 60%, 85%)"   // Primary Reading Text
+      ,content_muted:   "hsl(36, 15%, 60%)"   // Metadata, subtitles
+      ,content_subtle:  "hsl(36, 10%, 40%)"   // Placeholders, disabled states
+      ,content_inverse: "hsl(0, 0%, 5%)"      // Text on high-contrast buttons
+
+      // --- BRAND & ACTION (The "Wheat" Identity) ---
+      ,brand_primary:   "hsl(45, 100%, 50%)"  // Main Action / H1 / Focus Ring
+      ,brand_secondary: "hsl(38, 90%, 65%)"   // Secondary Buttons / H2
+      ,brand_tertiary:  "hsl(30, 60%, 70%)"   // Accents / H3
+      ,brand_link:      "hsl(48, 100%, 50%)"  // Hyperlinks (High Visibility)
+
+      // --- BORDERS & DIVIDERS ---
+      ,border_faint:    "hsl(36, 20%, 15%)"   // Subtle separation
+      ,border_default:  "hsl(36, 20%, 25%)"   // Standard Card Borders
+      ,border_strong:   "hsl(36, 20%, 40%)"   // Active states / Inputs
+
+      // --- STATE & FEEDBACK (Earth Tones) ---
+      ,state_success:   "hsl(100, 50%, 45%)"  // Olive Green
+      ,state_warning:   "hsl(35, 90%, 55%)"   // Burnt Orange
+      ,state_error:     "hsl(0, 60%, 55%)"    // Brick Red
+      ,state_info:      "hsl(200, 40%, 55%)"  // Slate Blue
+
+      // --- SYNTAX HIGHLIGHTING (For Code) ---
+      ,syntax_keyword:  "hsl(35, 100%, 65%)"  // Orange
+      ,syntax_string:   "hsl(75, 50%, 60%)"   // Sage Green
+      ,syntax_func:     "hsl(45, 90%, 70%)"   // Light Gold
+      ,syntax_comment:  "hsl(36, 15%, 45%)"   // Brown/Gray
+    };
+
+    // --- APPLY THEME ---
+    const palette = RT.config.theme;
+    const body = document.body;
+    const html = document.documentElement;
+
+    // 1. Paint Base
+    html.style.backgroundColor = palette.surface_0;
+    body.style.backgroundColor = palette.surface_0;
+    body.style.color = palette.content_main;
+
+    // 2. Export Variables (Standardization)
+    const s = body.style;
+    for (const [key, value] of Object.entries(palette)) {
+      s.setProperty(`--rt-${key.replace(/_/g, '-')}`, value);
+    }
+    
+    // 3. Global Overrides
+    const style_id = 'rt-global-overrides';
+    if (!document.getElementById(style_id)) {
+      const style = document.createElement('style');
+      style.id = style_id;
+      style.textContent = `
+        ::selection { background: var(--rt-surface-select); color: var(--rt-brand-primary); }
+        ::-moz-selection { background: var(--rt-surface-select); color: var(--rt-brand-primary); }
+        
+        ::-webkit-scrollbar { width: 12px; }
+        ::-webkit-scrollbar-track { background: var(--rt-surface-0); }
+        ::-webkit-scrollbar-thumb { 
+           background: var(--rt-border-default); 
+           border: 2px solid var(--rt-surface-0);
+           border-radius: 8px; 
+        }
+        ::-webkit-scrollbar-thumb:hover { background: var(--rt-brand-secondary); }
+      `;
+      document.head.appendChild(style);
+    }
+  };
+  
+} )();
diff --git a/document/style/theme_light.js b/document/style/theme_light.js
new file mode 100644 (file)
index 0000000..4b5eea4
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+  Theme: Classic Wheat (Light)
+  Standard: Theme 1.0
+  Description: Warm paper tones with Burnt Orange accents.
+*/
+( function(){
+  const RT = window.StyleRT = window.StyleRT || {};
+  
+  RT.theme_light = function(){
+    RT.config = RT.config || {};
+    
+    // THEME 1.0 DATA CONTRACT
+    RT.config.theme = {
+       meta_is_dark: false
+      ,meta_name:    "Classic Wheat"
+
+      // --- SURFACES ---
+      ,surface_0:       "hsl(40, 30%, 94%)"   // App Background (Cream/Linen)
+      ,surface_1:       "hsl(40, 25%, 90%)"   // Sidebar (Slightly darker beige)
+      ,surface_2:       "hsl(40, 20%, 98%)"   // Cards (Lighter, almost white)
+      ,surface_3:       "hsl(0, 0%, 100%)"    // Modals (Pure White)
+      ,surface_input:   "hsl(40, 20%, 98%)"   // Form Inputs
+      ,surface_code:    "hsl(40, 15%, 90%)"   // Code Block Background
+      ,surface_select:  "hsl(45, 100%, 85%)"  // Text Selection Highlight
+
+      // --- CONTENT ---
+      ,content_main:    "hsl(30, 20%, 20%)"   // Deep Umber (Not Black)
+      ,content_muted:   "hsl(30, 15%, 45%)"   // Medium Brown
+      ,content_subtle:  "hsl(30, 10%, 65%)"   // Light Brown/Gray
+      ,content_inverse: "hsl(40, 30%, 94%)"   // Text on dark buttons
+
+      // --- BRAND & ACTION ---
+      ,brand_primary:   "hsl(30, 90%, 35%)"   // Burnt Orange (Action)
+      ,brand_secondary: "hsl(35, 70%, 45%)"   // Rust / Gold
+      ,brand_tertiary:  "hsl(25, 60%, 55%)"   // Copper
+      ,brand_link:      "hsl(30, 100%, 35%)"  // Link Color
+
+      // --- BORDERS ---
+      ,border_faint:    "hsl(35, 20%, 85%)"
+      ,border_default:  "hsl(35, 20%, 75%)"
+      ,border_strong:   "hsl(35, 20%, 55%)"
+
+      // --- STATE & FEEDBACK ---
+      ,state_success:   "hsl(100, 40%, 40%)"  // Forest Green
+      ,state_warning:   "hsl(30, 90%, 50%)"   // Persimmon
+      ,state_error:     "hsl(0, 60%, 45%)"    // Crimson
+      ,state_info:      "hsl(200, 50%, 45%)"  // Navy Blue
+
+      // --- SYNTAX ---
+      ,syntax_keyword:  "hsl(20, 90%, 45%)"   // Rust
+      ,syntax_string:   "hsl(100, 35%, 35%)"  // Ivy Green
+      ,syntax_func:     "hsl(300, 30%, 40%)"  // Muted Purple
+      ,syntax_comment:  "hsl(35, 10%, 60%)"   // Light Brown
+    };
+
+    // --- APPLY THEME ---
+    const palette = RT.config.theme;
+    const body = document.body;
+    const html = document.documentElement;
+
+    html.style.backgroundColor = palette.surface_0;
+    body.style.backgroundColor = palette.surface_0;
+    body.style.color = palette.content_main;
+
+    const s = body.style;
+    for (const [key, value] of Object.entries(palette)) {
+      s.setProperty(`--rt-${key.replace(/_/g, '-')}`, value);
+    }
+  };
+} )();
diff --git a/document/style/theme_light_gold.js b/document/style/theme_light_gold.js
new file mode 100644 (file)
index 0000000..206f3da
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+  Theme: Golden Wheat (Light) - "Spanish Gold Edition"
+  File: style/theme-light-gold.js
+  Standard: Theme 1.0
+  Description: Light Parchment background with Oxblood Red ink.
+*/
+( function(){
+  const RT = window.StyleRT = window.StyleRT || {};
+  
+  RT.theme = function(){
+    RT.config = RT.config || {};
+    
+    RT.config.theme = {
+       meta_is_dark: false
+      ,meta_name:    "Golden Wheat (Yellow)"
+
+      // --- SURFACES (Light Parchment) ---
+      // Shifted lightness up to 94% for a "whiter" feel that still holds the yellow tint.
+      ,surface_0:       "hsl(48, 50%, 94%)"   // Main Page: Fine Parchment
+      ,surface_1:       "hsl(48, 40%, 90%)"   // Panels: Slightly darker
+      ,surface_2:       "hsl(48, 30%, 97%)"   // Cards: Very light
+      ,surface_3:       "hsl(0, 0%, 100%)"    // Popups
+      ,surface_input:   "hsl(48, 20%, 96%)"   
+      ,surface_code:    "hsl(48, 25%, 88%)"   // Distinct Code BG
+      ,surface_select:  "hsl(10, 70%, 85%)"   // Red Highlight
+
+      // --- CONTENT (Deep Ink) ---
+      ,content_main:    "hsl(10, 25%, 7%)"    // Deep Warm Black (Ink)
+      ,content_muted:   "hsl(10, 15%, 35%)"   // Dark Grey-Red
+      ,content_subtle:  "hsl(10, 10%, 55%)"   
+      ,content_inverse: "hsl(48, 50%, 90%)"   
+
+      // --- BRAND & ACTION (The Red Spectrum) ---
+      ,brand_primary:   "hsl(12, 85%, 30%)"   // H1 (Deep Oxblood)
+      ,brand_secondary: "hsl(10, 80%, 35%)"   // H2 (Garnet)
+      ,brand_tertiary:  "hsl(8, 70%, 40%)"    // H3 (Brick)
+      ,brand_link:      "hsl(12, 90%, 35%)"   // Link
+
+      // --- BORDERS ---
+      ,border_faint:    "hsl(45, 30%, 80%)"
+      ,border_default:  "hsl(45, 30%, 70%)"   // Pencil Grey
+      ,border_strong:   "hsl(12, 50%, 40%)"   
+
+      // --- STATE ---
+      ,state_success:   "hsl(120, 40%, 30%)"  
+      ,state_warning:   "hsl(25, 90%, 45%)"   
+      ,state_error:     "hsl(0, 75%, 35%)"    
+      ,state_info:      "hsl(210, 60%, 40%)"  
+
+      // --- SYNTAX ---
+      ,syntax_keyword:  "hsl(0, 75%, 35%)"    
+      ,syntax_string:   "hsl(100, 35%, 25%)"  
+      ,syntax_func:     "hsl(15, 85%, 35%)"   
+      ,syntax_comment:  "hsl(45, 20%, 50%)"   
+    };
+
+    // --- APPLY THEME ---
+    const palette = RT.config.theme;
+    const body = document.body;
+    const html = document.documentElement;
+
+    html.style.backgroundColor = palette.surface_0;
+    body.style.backgroundColor = palette.surface_0;
+    body.style.color = palette.content_main;
+
+    const s = body.style;
+    for (const [key, value] of Object.entries(palette)) {
+      s.setProperty(`--rt-${key.replace(/_/g, '-')}`, value);
+    }
+    
+    // Global overrides
+    const style_id = 'rt-global-overrides';
+    if (!document.getElementById(style_id)) {
+      const style = document.createElement('style');
+      style.id = style_id;
+      style.textContent = `
+        ::selection { background: var(--rt-surface-select); color: var(--rt-brand-primary); }
+        ::-moz-selection { background: var(--rt-surface-select); color: var(--rt-brand-primary); }
+        
+        ::-webkit-scrollbar { width: 12px; }
+        ::-webkit-scrollbar-track { background: var(--rt-surface-0); }
+        ::-webkit-scrollbar-thumb { 
+           background: var(--rt-border-default); 
+           border: 2px solid var(--rt-surface-0);
+           border-radius: 8px; 
+        }
+        ::-webkit-scrollbar-thumb:hover { background: var(--rt-brand-secondary); }
+
+        rt-article p, rt-article li {
+           text-shadow: 0px 0px 0.5px rgba(0,0,0, 0.2); 
+        }
+
+        .MathJax, .MathJax_Display, .mjx-chtml {
+            color: var(--rt-content-main) !important;
+            fill: var(--rt-content-main) !important;
+            stroke: var(--rt-content-main) !important;
+        }
+      `;
+      document.head.appendChild(style);
+    }
+  };
+  
+} )();
diff --git a/document/style/utility.js b/document/style/utility.js
new file mode 100644 (file)
index 0000000..258eb28
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+  General utilities for the StyleRT library.
+*/
+
+window.StyleRT = window.StyleRT || {};
+
+// --- DEBUG SYSTEM ---
+window.StyleRT.debug = {
+
+  // all debug messages enabled
+/*
+  active_tokens: new Set([
+    'style', 'layout', 'pagination'
+    ,'selector', 'config', 'error'
+    ,'term'
+  ]),
+*/
+  active_tokens: new Set([
+    'term'
+  ]),
+  
+  log: function(token, message) {
+    if (this.active_tokens.has(token)) {
+      console.log(`[StyleRT:${token}]`, message);
+    }
+  },
+
+  warn: function(token, message) {
+    if (this.active_tokens.has(token)) {
+      console.warn(`[StyleRT:${token}]`, message);
+    }
+  },
+  
+  // New: Always log errors regardless of token, but tag them
+  error: function(token, message) {
+    console.error(`[StyleRT:${token}] CRITICAL:`, message);
+  },
+  
+  enable: function(token) { this.active_tokens.add(token); console.log(`Enabled: ${token}`); },
+  disable: function(token) { this.active_tokens.delete(token); console.log(`Disabled: ${token}`); }
+};
+
+// --- UTILITIES ---
+window.StyleRT.utility = {
+  // --- FONT PHYSICS ---
+  measure_ink_ratio: function(target_font, ref_font = null) {
+    const debug = window.StyleRT.debug;
+    debug.log('layout', `Measuring ink ratio for ${target_font}`);
+
+    const canvas = document.createElement('canvas');
+    const ctx = canvas.getContext('2d');
+
+    if (!ref_font) {
+      const bodyStyle = window.getComputedStyle(document.body);
+      ref_font = bodyStyle.fontFamily;
+    }
+
+    const get_metrics = (font) => {
+      ctx.font = '100px ' + font; 
+      const metrics = ctx.measureText('M');
+      return {
+        ascent: metrics.actualBoundingBoxAscent, 
+        descent: metrics.actualBoundingBoxDescent 
+      };
+    };
+
+    const ref_m = get_metrics(ref_font);
+    const target_m = get_metrics(target_font);
+    
+    const ratio = ref_m.ascent / target_m.ascent;
+    // debug.log('layout', `Ink Ratio calculated: ${ratio.toFixed(3)}`);
+
+    return { 
+      ratio: ratio,
+      baseline_diff: ref_m.descent - target_m.descent 
+    };
+  },
+
+  // --- COLOR PHYSICS ---
+  is_color_light: function(color_string) {
+    const debug = window.StyleRT.debug;
+    
+    // 1. HSL Check
+    if (color_string.startsWith('hsl')) {
+      const numbers = color_string.match(/\d+/g);
+      if (numbers && numbers.length >= 3) {
+        const lightness = parseInt(numbers[2]);
+        return lightness > 50;
+      }
+    }
+
+    // 2. RGB Check
+    const rgb = color_string.match(/\d+/g);
+    if (!rgb) {
+      // debug.warn('color_layout', `Failed to parse color: "${color_string}". Defaulting to Light.`);
+      return true; 
+    }
+
+    const r = parseInt(rgb[0]);
+    const g = parseInt(rgb[1]);
+    const b = parseInt(rgb[2]);
+    const luma = (r * 299 + g * 587 + b * 114) / 1000;
+    return luma > 128;
+  },
+
+  is_block_content: function(element) {
+    return element.textContent.trim().includes('\n');
+  }
+};
diff --git a/document/style/version.txt b/document/style/version.txt
new file mode 100644 (file)
index 0000000..a85e614
--- /dev/null
@@ -0,0 +1 @@
+v0.3
diff --git a/env_developer b/env_developer
deleted file mode 100644 (file)
index 7067d75..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/usr/bin/env bash
-# env_developer — enter the project developer environment
-# (must be sourced)
-
-script_afp=$(realpath "${BASH_SOURCE[0]}")
-if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
-  echo "$script_afp:: This script must be sourced, not executed."
-  exit 1
-fi
-
-# enter project environment
-#
-  source tool_shared/bespoke/env
-
-# setup tools
-#
-  export PYTHON_HOME="$REPO_HOME/tool_shared/third_party/Python"
-  if [[ ":$PATH:" != *":$PYTHON_HOME/bin:"* ]]; then
-    export PATH="$PYTHON_HOME/bin:$PATH"
-  fi
-
-  RT_gcc="$REPO_HOME/tool_shared/third_party/RT_gcc/release"
-  if [[ ":$PATH:" != *":$RT_gcc:"* ]]; then
-    export PATH="$RT_gcc:$PATH"
-  fi
-
-# enter the role environment
-#
-  export ROLE=developer
-
-  tool="$REPO_HOME/$ROLE/tool"
-  if [[ ":$PATH:" != *":$tool:"* ]]; then
-    export PATH="$tool:$PATH"
-  fi
-
-  export ENV=$ROLE/tool/env
-
-  cd "$ROLE"
-  if [[ -f "tool/env" ]]; then
-    source "tool/env"
-    echo "in environment: $ENV"
-  else
-    echo "not found: $ENV"
-  fi
diff --git a/env_tester b/env_tester
deleted file mode 100644 (file)
index 5580c87..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/usr/bin/env bash
-# env_tester — enter the project tester environment
-# (must be sourced)
-
-script_afp=$(realpath "${BASH_SOURCE[0]}")
-if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
-  echo "$script_afp:: This script must be sourced, not executed."
-  exit 1
-fi
-
-# enter project environment
-#
-  source tool_shared/bespoke/env
-
-# setup tools
-#
-  export PYTHON_HOME="$REPO_HOME/tool_shared/third_party/Python"
-  if [[ ":$PATH:" != *":$PYTHON_HOME/bin:"* ]]; then
-    export PATH="$PYTHON_HOME/bin:$PATH"
-  fi
-
-  RT_gcc="$REPO_HOME/tool_shared/third_party/RT_gcc/release"
-  if [[ ":$PATH:" != *":$RT_gcc:"* ]]; then
-    export PATH="$RT_gcc:$PATH"
-  fi
-
-# enter the role environment
-#
-  export ROLE=tester
-
-  tool="$REPO_HOME/$ROLE/tool"
-  if [[ ":$PATH:" != *":$tool:"* ]]; then
-    export PATH="$tool:$PATH"
-  fi
-
-  export ENV=$ROLE/tool/env
-
-  cd "$ROLE"
-  if [[ -f "tool/env" ]]; then
-    source "tool/env"
-    echo "in environment: $ENV"
-  else
-    echo "not found: $ENV"
-  fi
diff --git a/env_toolsmith b/env_toolsmith
deleted file mode 100644 (file)
index ee09200..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env bash
-# env_toolsmith — enter the project toolsmith environment
-# (must be sourced)
-
-script_afp=$(realpath "${BASH_SOURCE[0]}")
-if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
-  echo "$script_afp:: This script must be sourced, not executed."
-  exit 1
-fi
-
-# enter project environment
-#
-  source tool_shared/bespoke/env
-
-# setup tools
-# initially these will not exist, as the toolsmith installs them
-#
-  export PYTHON_HOME="$REPO_HOME/tool_shared/third_party/Python"
-  if [[ ":$PATH:" != *":$PYTHON_HOME/bin:"* ]]; then
-    export PATH="$PYTHON_HOME/bin:$PATH"
-  fi
-
-  RT_gcc="$REPO_HOME/tool_shared/third_party/RT_gcc/release"
-  if [[ ":$PATH:" != *":$RT_gcc:"* ]]; then
-    export PATH="$RT_gcc:$PATH"
-  fi
-
-# enter the role environment
-#
-  export ROLE=toolsmith
-
-  TOOL_DIR="$REPO_HOME/tool"
-  if [[ ":$PATH:" != *":$TOOL_DIR:"* ]]; then
-    export PATH="$TOOL_DIR:$PATH"
-  fi
-
-  export ENV="tool/env"
-
-  cd "$REPO_HOME"
-  if [[ -f "tool/env" ]]; then
-    source "tool/env"
-    echo "in environment: $ENV"
-  else
-    echo "not found: $ENV"
-  fi
diff --git a/setup b/setup
new file mode 100644 (file)
index 0000000..ebc1244
--- /dev/null
+++ b/setup
@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+# setup - enter a project role environment
+# (must be sourced)
+
+script_afp=$(realpath "${BASH_SOURCE[0]}")
+if [ "${BASH_SOURCE[0]}" == "${0}" ]; then
+  echo "${script_afp}:: This script must be sourced, not executed."
+  exit 1
+fi
+
+project_roles="administrator consumer developer tester"
+
+print_usage(){
+  echo "usage: . setup <role>"
+  echo "known roles: ${project_roles}"
+}
+
+if [ -z "${1:-}" ] || [ "${1}" == "-h" ] || [ "${1}" == "--help" ]; then
+  print_usage
+  return 0
+fi
+
+role_is_valid=false
+for r in ${project_roles}; do
+  if [ "${1}" == "${r}" ]; then
+    role_is_valid=true
+    break
+  fi
+done
+
+if [ "${role_is_valid}" == "false" ]; then
+  echo "setup: unrecognized role or option '${1}'"
+  print_usage
+  return 1
+fi
+
+# setup the project
+#
+  source shared/tool/setup
+  if [[ -f "shared/authored/setup" ]]; then
+    source shared/authored/setup
+  fi
+
+# setup tools
+#
+  export PYTHON_HOME="${REPO_HOME}/shared/linked-project/Python"
+  if [[ ":${PATH}:" != *":${PYTHON_HOME}/bin:"* ]]; then
+    export PATH="${PYTHON_HOME}/bin:${PATH}"
+  fi
+
+  RT_gcc="${REPO_HOME}/shared/linked-project/RT_gcc/release"
+  if [[ ":${PATH}:" != *":${RT_gcc}:"* ]]; then
+    export PATH="${RT_gcc}:${PATH}"
+  fi
+
+# setup the role
+#
+  export ROLE="${1}"
+
+  tool="${REPO_HOME}/${ROLE}/tool"
+  if [[ ":${PATH}:" != *":${tool}:"* ]]; then
+    export PATH="${tool}:${PATH}"
+  fi
+
+  export SETUP="${ROLE}/tool/setup"
+
+  cd "${ROLE}" || return 1
+  if [ -f "tool/setup" ]; then
+    source "tool/setup"
+    echo "in environment: ${SETUP}"
+  else
+    echo "not found: ${SETUP}"
+  fi
index f14f140..aa7c35a 100755 (executable)
@@ -4,7 +4,7 @@
 import os, sys, shutil, stat, pwd, grp, subprocess
 
 HELP = """usage: scratchpad {ls|clear|help|make|write|size|find|lock|unlock} [ARGS]
-  ls               List scratchpad in an indented tree with perms and owner (quiet if missing).
+  ls| list         List scratchpad in an indented tree with perms and owner (quiet if missing).
   clear            Remove all contents of scratchpad/ except top-level .gitignore.
   clear NAME       Remove scratchpad/NAME only.
   make [NAME]      Ensure scratchpad/ exists with .gitignore; with NAME, mkdir scratchpad/NAME.
@@ -198,7 +198,7 @@ def CLI():
   if len(sys.argv) < 2:
     print(HELP); return
   cmd, *args = sys.argv[1:]
-  if cmd == "ls":
+  if cmd == "ls" or cmd =="list":
     if have_sp(): ls_tree(SP)
     else: return
   elif cmd == "clear":
diff --git a/tool_shared/document/packages.org b/tool_shared/document/packages.org
new file mode 100644 (file)
index 0000000..16cd13c
--- /dev/null
@@ -0,0 +1,2 @@
+
+sudo apt-get install bindfs cryptsetup