PageSourceSearch

https://www.cicciresearch.com/automation/

html cicciresearch.com collected 2026-10-08 05:53:13 UTC 97,901 bytes, 1,565 lines download raw bytes

1<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><link rel="canonical" href="https://www.cicciresearch.com/automation/"><link rel="icon" type="image/png" href="/logos/logo.png"><title>Cicci Research</title><link rel="stylesheet" href="/_astro/Footer.CLM6HcDu.css"><link rel="stylesheet" href="/_astro/automation.DLP8iH7R.css"></head><body class="bg-[#050816] text-white"><a href="https://www.cicciresearch.com/cdn-cgi/content?id=DSFgE3OxP0vbU5_ZDvfoih0OqJo25s6qHwSPvYSp4J8-1791438793.1032917-1.2.1.1-o0LSb7NkJUsSH4DQHwLgJO3OZxEDqfj.uV6vDpLWLSGnMz2PLqPOROBrgfNPUnHL" aria-hidden="true" rel="nofollow noopener" style="display: none !important; visibility: hidden !important"></a><nav class="site-navbar" data-site-navbar data-pagefind-ignore><div class="site-navbar__inner section-container"><a href="/" class="site-navbar__logo-link" aria-label="Cicci Research home"><img src="/logos/cicci-logo-hor.png" alt="Cicci Research" class="site-navbar__logo" width="320" height="100"></a><div class="site-navbar__desktop-links" aria-label="Main navigation"><div class="site-navbar__dropdown site-navbar__dropdown--active" data-desktop-dropdown><button class="site-navbar__dropdown-trigger" type="button" aria-expanded="false" data-dropdown-trigger><span>Products</span><svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="m6 8 4 4 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"></path></svg></button><div class="site-navbar__dropdown-menu site-navbar__dropdown-menu--products" data-dropdown-menu><a href="/measurement" class="site-navbar__product-link"><span class="site-navbar__product-thumb" aria-hidden="true"><img src="/images/products/arkeo-platform.png" alt="" loading="eager"></span><span class="site-navbar__product-copy"><strong>Measurement</strong><small>ARKEO measurement platform</small></span></a><a href="/automation" class="site-navbar__product-link is-active"><span class="site-navbar__product-thumb" aria-hidden="true"><img src="/images/automation/charon-system-overview.png" alt="" loading="eager"></span><span class="site-navbar__product-copy"><strong>Automation</strong><small>CHARON automation platform</small></span></a><div class="site-navbar__product-nested" data-product-nested><div class="site-navbar__product-parent-row"><a href="/light-sources" class="site-navbar__product-link site-navbar__product-link--parent"><span class="site-navbar__product-thumb" aria-hidden="true"><img src="/images/products/nav/arkeo-soaker-xl.png" alt="" loading="eager"></span><span class="site-navbar__product-copy"><strong>Light Sources</strong><small>Controlled LED illumination</small></span></a><button type="button" class="site-navbar__product-parent-toggle" aria-label="Show Light Sources products" aria-expanded="false" data-product-submenu-trigger><span aria-hidden="true">→</span></button></div><div class="site-navbar__product-submenu" data-product-submenu hidden><a href="/light-sources/arkeo-soaker-xl" class="site-navbar__product-subitem"><span class="site-navbar__product-subitem-thumb" aria-hidden="true"><img src="/images/products/nav/arkeo-soaker-xl.png" alt="" loading="lazy"></span><span class="site-navbar__product-subitem-copy"><strong>ARKEO Soaker XL</strong><small>Large-area stability illumination</small></span></a><a href="/light-sources/arkeo-soaker-ms" class="site-navbar__product-subitem"><span class="site-navbar__product-subitem-thumb" aria-hidden="true"><img src="/images/products/nav/arkeo-soaker-ms.png" alt="" loading="lazy"></span><span class="site-navbar__product-subitem-copy"><strong>ARKEO Soaker M/S</strong><small>Compact programmable LED sources</small></span></a><a href="/light-sources/arkeo-96multiled" class="site-navbar__product-subitem"><span class="site-navbar__product-subitem-thumb" aria-hidden="true"><img src="/images/products/nav/arkeo-96multiled.png" alt="" loading="lazy"></span><span class="site-navbar__product-subitem-copy"><strong>ARKEO 96multiled</strong><small>Multiwell and multi-plate illumination</small></span></a></div></div><div class="site-navbar__product-nested" data-product-nested><div class="site-navbar__product-parent-row"><a href="/environmental-chambers" class="site-navbar__product-link site-navbar__product-link--parent"><span class="site-navbar__product-thumb" aria-hidden="true"><img src="/images/products/nav/chamber-xxl.png" alt="" loading="eager"></span><span class="site-navbar__product-copy"><strong>Environmental Chambers</strong><small>Temperature &amp; atmosphere control</small></span></a><button type="button" class="site-navbar__product-parent-toggle" aria-label="Show Environmental Chambers products" aria-expanded="false" data-product-submenu-trigger><span aria-hidden="true">→</span></button></div><div class="site-navbar__product-submenu" data-product-submenu hidden><a href="/environmental-chambers/chamber-xxl" class="site-navbar__product-subitem"><span class="site-navbar__product-subitem-thumb" aria-hidden="true"><img src="/images/products/nav/chamber-xxl.png" alt="" loading="lazy"></span><span class="site-navbar__product-subitem-copy"><strong>Chamber XXL</strong><small>Large-format environmental control</small></span></a><a href="/environmental-chambers/chamber-m" class="site-navbar__product-subitem"><span class="site-navbar__product-subitem-thumb" aria-hidden="true"><img src="/images/products/nav/chamber-m.png" alt="" loading="lazy"></span><span class="site-navbar__product-subitem-copy"><strong>Chamber M</strong><small>Compact benchtop controlled chamber</small></span></a><a href="/environmental-chambers/chamber-4-comps" class="site-navbar__product-subitem"><span class="site-navbar__product-subitem-thumb" aria-hidden="true"><img src="/images/products/nav/chamber-4-comps.png" alt="" loading="lazy"></span><span class="site-navbar__product-subitem-copy"><strong>Chamber 4-comps</strong><small>Four-position chamber concept</small></span></a><a href="/environmental-chambers/chamber-x-ray" class="site-navbar__product-subitem"><span class="site-navbar__product-subitem-thumb" aria-hidden="true"><img src="/images/products/nav/chamber-x-ray.png" alt="" loading="lazy"></span><span class="site-navbar__product-subitem-copy"><strong>Chamber X-Ray</strong><small>In-operando X-ray compatible chamber</small></span></a></div></div><a href="/installations" class="site-navbar__menu-card-link"><span class="site-navbar__menu-card-copy"><strong>Installations</strong></span></a></div></div><div class="site-navbar__dropdown" data-desktop-dropdown><button class="site-navbar__dropdown-trigger" type="button" aria-expanded="false" data-dropdown-trigger><span>Research</span><svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="m6 8 4 4 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"></path></svg></button><div class="site-navbar__dropdown-menu site-navbar__dropdown-menu--cards" data-dropdown-menu><a href="/publications" class="site-navbar__menu-card-link"><span class="site-navbar__menu-card-copy"><strong>Publications</strong></span></a><a href="/projects" class="site-navbar__menu-card-link"><span class="site-navbar__menu-card-copy"><strong>Research Projects</strong></span></a><a href="/Conferences" class="site-navbar__menu-card-link"><span class="site-navbar__menu-card-copy"><strong>News &amp; Events</strong></span></a></div></div><div class="site-navbar__dropdown" data-desktop-dropdown><button class="site-navbar__dropdown-trigger" type="button" aria-expanded="false" data-dropdown-trigger><span>Support</span><svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="m6 8 4 4 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"></path></svg></button><div class="site-navbar__dropdown-menu site-navbar__dropdown-menu--cards" data-dropdown-menu><a href="/documentation" class="site-navbar__menu-card-link"><span class="site-navbar__menu-card-copy"><strong>Documentation</strong></span></a><a href="/remoteSupport" class="site-navbar__menu-card-link"><span class="site-navbar__menu-card-copy"><strong>Support services</strong></span></a><a href="/training" class="site-navbar__menu-card-link"><span class="site-navbar__menu-card-copy"><strong>Training</strong></span></a></div></div><a href="/AboutUs" class="site-navbar__top-link">About Us</a></div><div class="site-navbar__actions"><div class="site-search" data-site-search data-pagefind-ignore><button class="site-search__trigger" type="button" aria-label="Search Cicci Re
1search" aria-haspopup="dialog" data-search-open><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="11" cy="11" r="6.5" stroke="currentColor" stroke-width="2"></circle><path d="m16 16 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg><span class="site-search__trigger-label">Search</span><span class="site-search__trigger-shortcut">⌘K</span></button><div class="site-search__overlay" data-search-overlay role="dialog" aria-modal="true" aria-label="Search Cicci Research" hidden><button class="site-search__backdrop" type="button" aria-label="Close search" data-search-close></button><section class="site-search__panel"><div class="site-search__top"><div class="site-search__input-wrap"><svg class="site-search__input-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="11" cy="11" r="6.5" stroke="currentColor" stroke-width="2"></circle><path d="m16 16 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg><input type="search" autocomplete="off" spellcheck="false" placeholder="Search CICCI Research..." aria-label="Search the Cicci Re
1search website" aria-controls="site-search-results" data-search-input><button type="button" class="site-search__clear" aria-label="Clear search" title="Clear search" data-search-clear hidden><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M7 7l10 10M17 7 7 17" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg></button><button type="button" class="site-search__close" aria-label="Close search" title="Close search" data-search-close><span>Esc</span></button></div><p class="site-search__status" data-search-status aria-live="polite">Search pages, products, projects, publications and individual sections.</p></div><div class="site-search__body"><div class="site-search__recommendations" data-search-recommendations><section class="site-search__group"><div class="site-search__group-heading"><h3 class="site-search__group-title" id="quick-access">Quick access</h3></div><div class="site-search__menu-list"><a class="site-search__menu-row" href="/measurement"><span class="site-search__row-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="M6 5h12v14H6z" stroke="currentColor" stroke-width="1.6"></path><path d="M9 9h6M9 13h6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path></svg></span><span class="site-search__row-copy"><strong>Measurement</strong><small>ARKEO platform and measurement methods</small></span><span class="site-search__row-chevron" aria-hidden="true">›</span></a><a class="site-search__menu-row" href="/publications"><span class="site-search__row-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="M6 5h12v14H6z" stroke="currentColor" stroke-width="1.6"></path><path d="M9 9h6M9 13h6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path></svg></span><span class="site-search__row-copy"><strong>Publications</strong><small>Scientific papers and research references</small></span><span class="site-search__row-chevron" aria-hidden="true">›</span></a><a class="site-search__menu-row" href="/training"><span class="site-search__row-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="M6 5h12v14H6z" stroke="currentColor" stroke-width="1.6"></path><path d="M9 9h6M9 13h6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path></svg></span><span class="site-search__row-copy"><strong>Training</strong><small>Hands-on courses and educational activities</small></span><span class="site-search__row-chevron" aria-hidden="true">›</span></a><a class="site-search__menu-row" href="/Conferences"><span class="site-search__row-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="M6 5h12v14H6z" stroke="currentColor" stroke-width="1.6"></path><path d="M9 9h6M9 13h6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path></svg></span><span class="site-search__row-copy"><strong>News</strong><small>Conferences, events and research activities</small></span><span class="site-search__row-chevron" aria-hidden="true">›</span></a></div></section><section class="site-search__group" data-recent-searches-section hidden><div class="site-search__group-heading"><h3 class="site-search__group-title" id="recent-searches">Recent searches</h3><button type="button" class="site-search__text-action" data-clear-search-history>Clear all</button></div><div class="site-search__menu-list" data-recent-searches-list></div></section><section class="site-search__group"><div class="site-search__group-heading"><h3 class="site-search__group-title" id="latest-research">Latest research</h3></div><div class="site-search__menu-list"><button type="button" class="site-search__menu-row site-search__menu-row--button" data-search-suggestion="PVSPACE"><span class="site-search__row-icon site-search__row-icon--research" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="M9 4h6M10 4v5l-4 7a2 2 0 0 0 1.7 3h8.6A2 2 0 0 0 18 16l-4-7V4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M8.5 14h7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path></svg></span><span class="site-search__row-copy"><strong>PVSPACE</strong><small>New-generation photovoltaics for space</small></span><span class="site-search__row-chevron" aria-hidden="true">›</span></button><button type="button" class="site-search__menu-row site-search__menu-row--button" data-search-suggestion="ISOPHOS 2026"><span class="site-search__row-icon site-search__row-icon--research" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none">
1<path d="M9 4h6M10 4v5l-4 7a2 2 0 0 0 1.7 3h8.6A2 2 0 0 0 18 16l-4-7V4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M8.5 14h7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path></svg></span><span class="site-search__row-copy"><strong>ISOPHOS 2026</strong><small>International school on hybrid and organic photovoltaics</small></span><span class="site-search__row-chevron" aria-hidden="true">›</span></button><button type="button" class="site-search__menu-row site-search__menu-row--button" data-search-suggestion="ISOS 17"><span class="site-search__row-icon site-search__row-icon--research" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="M9 4h6M10 4v5l-4 7a2 2 0 0 0 1.7 3h8.6A2 2 0 0 0 18 16l-4-7V4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M8.5 14h7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path></svg></span><span class="site-search__row-copy"><strong>ISOS 17</strong><small>Stability protocols for organic and perovskite solar cells</small></span><span class="site-search__row-chevron" aria-hidden="true">›</span></button><button type="button" class="site-search__menu-row site-search__menu-row--button" data-search-suggestion="Rete IFV 2025"><span class="site-search__row-icon site-search__row-icon--research" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="M9 4h6M10 4v5l-4 7a2 2 0 0 0 1.7 3h8.6A2 2 0 0 0 18 16l-4-7V4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M8.5 14h7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path></svg></span><span class="site-search__row-copy"><strong>Rete IFV 2025</strong><small>Italian photovoltaic research network conference</small></span><span class="site-search__row-chevron" aria-hidden="true">›</span></button></div></section></div><div id="site-search-results" class="site-search__results" data-search-results role="listbox" aria-label="Search results"></div></div><footer class="site-search__footer"><div class="site-search__footer-shortcuts"><span><kbd>↑</kbd><kbd>↓</kbd> Navigate</span><span><kbd>↵</kbd> Open</span><span><kbd>Esc</kbd> Close</span></div><span class="site-search__footer-brand">CICCI Research</span></footer></section></div></div>
1<script>
2  (() => {
3    document.querySelectorAll("[data-site-search]").forEach((root) => {
4      if (!(root instanceof HTMLElement) || root.dataset.searchReady === "true") return;
5      root.dataset.searchReady = "true";
6
7      const overlay = root.querySelector("[data-search-overlay]");
8      const input = root.querySelector("[data-search-input]");
9      const results = root.querySelector("[data-search-results]");
10      const status = root.querySelector("[data-search-status]");
11      const recommendations = root.querySelector("[data-search-recommendations]");
12      const openButton = root.querySelector("[data-search-open]");
13      const clearButton = root.querySelector("[data-search-clear]");
14      const closeButtons = root.querySelectorAll("[data-search-close]");
15      const recentSection = root.querySelector("[data-recent-searches-section]");
16      const recentList = root.querySelector("[data-recent-searches-list]");
17      const clearHistoryButton = root.querySelector("[data-clear-search-history]");
18
19      if (
20        !(overlay instanceof HTMLElement) ||
21        !(input instanceof HTMLInputElement) ||
22        !(results instanceof HTMLElement) ||
23        !(status instanceof HTMLElement) ||
24        !(recommendations instanceof HTMLElement) ||
25        !(openButton instanceof HTMLButtonElement) ||
26        !(clearButton instanceof HTMLButtonElement) ||
27        !(recentSection instanceof HTMLElement) ||
28        !(recentList instanceof HTMLElement) ||
29        !(clearHistoryButton instanceof HTMLButtonElement)
30      ) return;
31
32      /*
33       * The fixed navbar uses backdrop-filter, which can constrain fixed
34       * descendants in Chromium/Safari. The modal belongs directly under body.
35       */
36      if (overlay.parentElement !== document.body) {
37        document.body.appendChild(overlay);
38      }
39
40      const RECENT_KEY = "cicci-search-recent-v2";
41      const PENDING_KEY = "cicci-search-pending-landing-v1";
42      const PENDING_MAX_AGE = 30000;
43
44      let pagefind = null;
45      let fallbackIndex = null;
46      let engine = null;
47      let debounceTimer = 0;
48      let lastFocused = null;
49      let selectedResultIndex = -1;
50
51      const escapeHtml = (value = "") =>
52        String(value).replace(/[&<>"']/g, (char) => ({
53          "&": "&amp;",
54          "<": "&lt;",
55          ">": "&gt;",
56          '"': "&quot;",
57          "'": "&#039;",
58        })[char]);
59
60      const normalize = (value = "") =>
61        String(value)
62          .normalize("NFKD")
63          .replace(/[\u0300-\u036f]/g, "")
64          .toLowerCase();
65
66      const isLocal =
67        location.hostname === "localhost" ||
68        location.hostname === "127.0.0.1";
69
70      /* -------------------------------------------------------------- */
71      /* Recent searches                                                */
72      /* -------------------------------------------------------------- */
73
74      const readRecentSearches = () => {
75        try {
76          const parsed = JSON.parse(localStorage.getItem(RECENT_KEY) || "[]");
77          return Array.isArray(parsed) ? parsed.slice(0, 4) : [];
78        } catch {
79          return [];
80        }
81      };
82
83      const writeRecentSearches = (items) => {
84        try {
85          localStorage.setItem(RECENT_KEY, JSON.stringify(items.slice(0, 4)));
86        } catch {
87          // Search must remain usable even when storage is unavailable.
88        }
89      };
90
91      const renderRecentSearches = () => {
92        const items = readRecentSearches();
93
94        recentSection.hidden = items.length === 0;
95
96        recentList.innerHTML = items
97          .map(
98            (term) => `
99              <div class="site-search__recent-row">
100                <button
101                  type="button"
102                  class="site-search__menu-row site-search__menu-row--button site-search__menu-row--recent"
103                  data-recent-search="${escapeHtml(term)}"
104                  title="Search again for ${escapeHtml(term)}"
105                >
106                  <span class="site-search__row-icon" aria-hidden="true">
107                    <svg viewBox="0 0 24 24" fill="none">
108                      <path
109                        d="M12 7v5l3 2M5.3 6.3A8 8 0 1 1 4 12"
110                        stroke="currentColor"
111                        stroke-width="1.6"
112                        stroke-linecap="round"
113                        stroke-linejoin="round"
114                      />
115                      <path
116                        d="M4 5v4h4"
117                        stroke="currentColor"
118                        stroke-width="1.6"
119                        stroke-linecap="round"
120                        stroke-linejoin="round"
121                      />
122                    </svg>
123                  </span>
124                  <span class="site-search__row-copy">
125                    <strong>${escapeHtml(term)}</strong>
126                    <small>Search again</small>
127                  </span>
128                </button>
129                <button
130                  type="button"
131                  class="site-search__recent-remove"
132                  aria-label="Remove recent search ${escapeHtml(term)}"
133                  title="Remove"
134                  data-remove-recent-search="${escapeHtml(term)}"
135                >
136                  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
137                    <path d="M7 7l10 10M17 7 7 17" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
138                  </svg>
139                </button>
140              </div>
141            `
142          )
143          .join("");
144      };
145
146      const rememberSearch = (term) => {
147        const clean = String(term || "").trim();
148        if (clean.length < 2) return;
149
150        const key = normalize(clean);
151        const previous = readRecentSearches().filter(
152          (item) => normalize(item) !== key
153        );
154
155        writeRecentSearches([clean, ...previous].slice(0, 4));
156        renderRecentSearches();
157      };
158
159      renderRecentSearches();
160
161      /* -------------------------------------------------------------- */
162      /* Landing highlight                                              */
163      /* -------------------------------------------------------------- */
164
165      const clearLandingHighlight = () => {
166        document
167          .querySelectorAll("mark.site-search__landing-mark")
168          .forEach((mark) => {
169            const parent = mark.parentNode;
170            mark.replaceWith(document.createTextNode(mark.textContent || ""));
171            if (parent instanceof HTMLElement) parent.normalize();
172          });
173      };
174
175      const findSectionByTitle = (sectionTitle) => {
176        const wanted = normalize(sectionTitle || "").trim();
177        if (!wanted) return null;
178
179        const headings = [
180          ...document.querySelectorAll(
181            "main h1, main h2, main h3, main h4, body h1, body h2, body h3, body h4"
182          ),
183        ];
184
185        const heading = headings.find((element) => {
186          if (!(element instanceof HTMLElement)) return false;
187          const candidate = normalize(element.textContent || "").trim();
188
189          return (
190            candidate === wanted ||
191            candidate.includes(wanted) ||
192            wanted.includes(candidate)
193          );
194        });
195
196        if (!(heading instanceof HTMLElement)) return null;
197
198        return (
199          heading.closest("section, article") ||
200          heading.parentElement ||
201          heading
202        );
203      };
204
205      const textNodesInside = (container) => {
206        if (!(container instanceof HTMLElement)) return [];
207
208        const walker = document.createTreeWalker(
209          container,
210          NodeFilter.SHOW_TEXT,
211          {
212            acceptNode(node) {
213              const parent = node.parentElement;
214
215              if (!parent) return NodeFilter.FILTER_REJECT;
216
217              if (
218                parent.closest(
219                  "script, style, noscript, nav, footer, .site-search__overlay, [data-pagefind-ignore]"
220                )
221              ) {
222                return NodeFilter.FILTER_REJECT;
223              }
224
225              if (!(node.nodeValue || "").trim()) {
226                return NodeFilter.FILTER_REJECT;
227              }
228
229              return NodeFilter.FILTER_ACCEPT;
230            },
231          }
232        );
233
234        const nodes = [];
235        let node;
236
237        while ((node = walker.nextNode())) {
238          nodes.push(node);
239        }
240
241        return nodes;
242      };
243
244      const markExactMatch = (node, term) => {
245        const source = node.nodeValue || "";
246        const index = source
247          .toLocaleLowerCase()
248          .indexOf(term.toLocaleLowerCase());
249
250        if (index < 0) return null;
251
252        const matchNode = node.splitText(index);
253        matchNode.splitText(term.length);
254
255        const mark = document.createElement("mark");
256        mark.className = "site-search__landing-mark";
257
258        matchNode.parentNode?.insertBefore(mark, matchNode);
259        mark.appendChild(matchNode);
260
261        return mark;
262      };
263
264      const focusSearchMatch = (
265        term,
266        preferredHash = "",
267        preferredSectionTitle = ""
268      ) => {
269        const cleanTerm = String(term || "").trim();
270        if (!cleanTerm) return false;
271
272        clearLandingHighlight();
273
274        const containers = [];
275
276        if (preferredHash) {
277          const id = decodeURIComponent(preferredHash.replace(/^#/, ""));
278          const anchor = document.getElementById(id);
279
280          if (anchor instanceof HTMLElement) {
281            containers.push(
282              anchor.closest("section, article") ||
283                anchor.parentElement ||
284                anchor
285            );
286          }
287        }
288
289        const preferredSection = findSectionByTitle(preferredSectionTitle);
290
291        if (
292          preferredSection instanceof HTMLElement &&
293          !containers.includes(preferredSection)
294        ) {
295          containers.push(preferredSection);
296        }
297
298        const main = document.querySelector("main");
299
300        if (
301          main instanceof HTMLElement &&
302          !containers.includes(main)
303        ) {
304          containers.push(main);
305        }
306
307        if (!containers.includes(document.body)) {
308          containers.push(document.body);
309        }
310
311        let mark = null;
312
313        for (const container of containers) {
314          for (const node of textNodesInside(container)) {
315            mark = markExactMatch(node, cleanTerm);
316            if (mark) break;
317          }
318
319          if (mark) break;
320        }
321
322        if (mark) {
323          window.setTimeout(() => {
324            mark.scrollIntoView({
325              behavior: "smooth",
326              block: "center",
327            });
328          }, 100);
329
330          return true;
331        }
332
333        /*
334         * If the sentence spans multiple inline nodes, we cannot safely wrap
335         * the exact phrase without restructuring the page. In that case we
336         * still scroll to the correct text block, but do not add a box.
337         */
338        const normalizedTerm = normalize(cleanTerm);
339
340        const blocks = [
341          ...document.querySelectorAll(
342            "main h1, main h2, main h3, main h4, main p, main li, main td, main th"
343          ),
344        ].filter(
345          (element) =>
346            element instanceof HTMLElement &&
347            !element.closest(
348              "nav, footer, .site-search__overlay, [data-pagefind-ignore]"
349            )
350        );
351
352        const preferredBlocks =
353          preferredSection instanceof HTMLElement
354            ? blocks.filter((element) => preferredSection.contains(element))
355            : [];
356
357        const ordered = [
358          ...preferredBlocks,
359          ...blocks.filter((element) => !preferredBlocks.includes(element)),
360        ];
361
362        const block = ordered.find((element) =>
363          normalize(element.textContent || "").includes(normalizedTerm)
364        );
365
366        if (block instanceof HTMLElement) {
367          window.setTimeout(() => {
368            block.scrollIntoView({
369              behavior: "smooth",
370              block: "center",
371            });
372          }, 100);
373
374          return true;
375        }
376
377        return false;
378      };
379
380      /*
381       * Cross-page searches use sessionStorage for one navigation only.
382       * There are no ?search= parameters in the public URL.
383       *
384       * The pending state is deleted BEFORE applying it, so a browser reload
385       * never recreates the highlight.
386       */
387      const consumePendingLanding = () => {
388        let pending = null;
389
390        try {
391          pending = JSON.parse(sessionStorage.getItem(PENDING_KEY) || "null");
392          sessionStorage.removeItem(PENDING_KEY);
393        } catch {
394          sessionStorage.removeItem(PENDING_KEY);
395        }
396
397        if (!pending || typeof pending !== "object") return;
398
399        if (
400          pending.path !== window.location.pathname ||
401          Date.now() - Number(pending.createdAt || 0) > PENDING_MAX_AGE
402        ) {
403          return;
404        }
405
406        window.setTimeout(() => {
407          focusSearchMatch(
408            pending.term || "",
409            pending.hash || "",
410            pending.sectionTitle || ""
411          );
412        }, 180);
413      };
414
415      consumePendingLanding();
416
417      document.addEventListener("astro:page-load", consumePendingLanding);
418
419      /* -------------------------------------------------------------- */
420      /* Search engines                                                 */
421      /* -------------------------------------------------------------- */
422
423      const loadFallbackIndex = async () => {
424        if (fallbackIndex) return fallbackIndex;
425
426        const response = await fetch("/search-index.json", {
427          cache: isLocal ? "no-store" : "default",
428        });
429
430        if (!response.ok) {
431          throw new Error(`search-index.json returned ${response.status}`);
432        }
433
434        const data = await response.json();
435
436        if (!Array.isArray(data)) {
437          throw new Error("search-index.json is not an array");
438        }
439
440        fallbackIndex = data;
441        return fallbackIndex;
442      };
443
444      const loadPagefind = async () => {
445        if (pagefind) return pagefind;
446
447        pagefind = await import("/pagefind/pagefind.js");
448        await pagefind.init();
449        return pagefind;
450      };
451
452      const loadEngine = async () => {
453        if (engine) return engine;
454
455        if (isLocal) {
456          try {
457            await loadFallbackIndex();
458            engine = "fallback";
459            return engine;
460          } catch (error) {
461            console.error("Local search index could not be loaded:", error);
462            status.textContent =
463              "Local search index is missing. Restart npm run dev.";
464            return null;
465          }
466        }
467
468        try {
469          await loadPagefind();
470          engine = "pagefind";
471          return engine;
472        } catch (pagefindError) {
473          console.warn("Pagefind unavailable, using static search index.", pagefindError);
474
475          try {
476            await loadFallbackIndex();
477            engine = "fallback";
478            return engine;
479          } catch (fallbackError) {
480            console.error("Fallback search index unavailable:", fallbackError);
481            status.textContent = "Search index is not available.";
482            return null;
483          }
484        }
485      };
486
487      /* -------------------------------------------------------------- */
488      /* UI state                                                       */
489      /* -------------------------------------------------------------- */
490
491      const updateClearButton = () => {
492        clearButton.hidden = input.value.length === 0;
493      };
494
495      const resetSelection = () => {
496        selectedResultIndex = -1;
497
498        results
499          .querySelectorAll(".site-search__result--selected")
500          .forEach((item) => {
501            item.classList.remove("site-search__result--selected");
502            item.removeAttribute("aria-selected");
503          });
504      };
505
506      const resetSearchView = () => {
507        results.innerHTML = "";
508        recommendations.hidden = false;
509        status.textContent = "Search across pages and individual sections.";
510        resetSelection();
511      };
512
513      const clearActiveSearch = ({ focusInput = true } = {}) => {
514        window.clearTimeout(debounceTimer);
515        input.value = "";
516        updateClearButton();
517        resetSearchView();
518        clearLandingHighlight();
519
520        if (focusInput) {
521          input.focus();
522        }
523      };
524
525      const openSearch = async () => {
526        lastFocused = document.activeElement;
527        overlay.hidden = false;
528        document.body.classList.add("site-search-open");
529        renderRecentSearches();
530        updateClearButton();
531
532        requestAnimationFrame(() => input.focus());
533        await loadEngine();
534      };
535
536      const closeSearch = ({
537        clearHighlight = true,
538        resetQuery = true,
539      } = {}) => {
540        overlay.hidden = true;
541        document.body.classList.remove("site-search-open");
542
543        if (resetQuery) {
544          input.value = "";
545          updateClearButton();
546          resetSearchView();
547        }
548
549        if (clearHighlight) {
550          clearLandingHighlight();
551        }
552
553        if (lastFocused instanceof HTMLElement) {
554          lastFocused.focus();
555        }
556      };
557
558      /* -------------------------------------------------------------- */
559      /* Search formatting/ranking                                      */
560      /* -------------------------------------------------------------- */
561
562      const contextualExcerpt = (text, term, maxLength = 210) => {
563        const source = String(text || "").replace(/\s+/g, " ").trim();
564        if (!source) return "";
565
566        const normalizedSource = normalize(source);
567        const normalizedTerm = normalize(term).trim();
568        const words = normalizedTerm.split(/\s+/).filter(Boolean);
569
570        let matchIndex = normalizedSource.indexOf(normalizedTerm);
571        let matchLength = term.length;
572
573        if (matchIndex < 0) {
574          const firstMatchingWord = words.find((word) =>
575            normalizedSource.includes(word)
576          );
577
578          if (firstMatchingWord) {
579            matchIndex = normalizedSource.indexOf(firstMatchingWord);
580            matchLength = firstMatchingWord.length;
581          }
582        }
583
584        if (matchIndex < 0) {
585          const fallback = source.slice(0, maxLength);
586          return escapeHtml(
587            fallback + (source.length > maxLength ? "…" : "")
588          );
589        }
590
591        let start = Math.max(0, matchIndex - Math.floor(maxLength * 0.38));
592        let end = Math.min(source.length, start + maxLength);
593
594        if (start > 0) {
595          const nextSpace = source.indexOf(" ", start);
596          if (nextSpace > -1 && nextSpace < matchIndex) {
597            start = nextSpace + 1;
598          }
599        }
600
601        if (end < source.length) {
602          const previousSpace = source.lastIndexOf(" ", end);
603          if (previousSpace > matchIndex + matchLength) {
604            end = previousSpace;
605          }
606        }
607
608        const excerpt = source.slice(start, end);
609        const normalizedExcerpt = normalize(excerpt);
610
611        let localIndex = normalizedExcerpt.indexOf(normalizedTerm);
612        let localLength = term.length;
613
614        if (localIndex < 0) {
615          const firstMatchingWord = words.find((word) =>
616            normalizedExcerpt.includes(word)
617          );
618
619          if (firstMatchingWord) {
620            localIndex = normalizedExcerpt.indexOf(firstMatchingWord);
621            localLength = firstMatchingWord.length;
622          }
623        }
624
625        let rendered;
626
627        if (localIndex >= 0) {
628          rendered =
629            escapeHtml(excerpt.slice(0, localIndex)) +
630            "<mark>" +
631            escapeHtml(excerpt.slice(localIndex, localIndex + localLength)) +
632            "</mark>" +
633            escapeHtml(excerpt.slice(localIndex + localLength));
634        } else {
635          rendered = escapeHtml(excerpt);
636        }
637
638        return `${start > 0 ? "…" : ""}${rendered}${end < source.length ? "…" : ""}`;
639      };
640
641      const searchFallback = async (term) => {
642        const index = await loadFallbackIndex();
643        const query = normalize(term).trim();
644        const queryWords = query.split(/\s+/).filter(Boolean);
645
646        let scored = index
647          .map((item) => {
648            const pageTitle = normalize(item.pageTitle);
649            const sectionTitle = normalize(item.sectionTitle);
650            const text = normalize(item.text);
651            const haystack = `${pageTitle} ${sectionTitle} ${text}`;
652
653            const matchedWords = queryWords.filter((word) =>
654              haystack.includes(word)
655            ).length;
656
657            if (matchedWords === 0) {
658              return null;
659            }
660
661            const allWordsMatch = matchedWords === queryWords.length;
662            const exactPhraseMatch = haystack.includes(query);
663
664            let score = 0;
665
666            if (sectionTitle === query) score += 240;
667            if (pageTitle === query) score += 210;
668            if (sectionTitle.startsWith(query)) score += 155;
669            if (pageTitle.startsWith(query)) score += 135;
670            if (sectionTitle.includes(query)) score += 110;
671            if (pageTitle.includes(query)) score += 90;
672            if (text.includes(query)) score += 60;
673
674            if (exactPhraseMatch) score += 80;
675            if (allWordsMatch) score += 55;
676
677            score += matchedWords * 18;
678
679            queryWords.forEach((word) => {
680              if (sectionTitle.includes(word)) score += 22;
681              if (pageTitle.includes(word)) score += 18;
682              if (text.includes(word)) score += 6;
683            });
684
685            return {
686              ...item,
687              score,
688              matchedWords,
689              allWordsMatch,
690            };
691          })
692          .filter(Boolean);
693
694        /*
695         * Prefer results containing every word. If none exist, gracefully
696         * fall back to the strongest partial matches instead of returning an
697         * empty result set for useful searches such as "ISOPHOS 2026".
698         */
699        const hasAllWordMatch = scored.some((item) => item.allWordsMatch);
700
701        if (hasAllWordMatch) {
702          scored = scored.filter((item) => item.allWordsMatch);
703        } else if (queryWords.length > 1) {
704          const minimumWords = Math.max(
705            1,
706            Math.ceil(queryWords.length / 2)
707          );
708
709          scored = scored.filter(
710            (item) => item.matchedWords >= minimumWords
711          );
712        }
713
714        scored.sort((a, b) => b.score - a.score);
715
716        const seen = new Set();
717
718        return scored
719          .filter((item) => {
720            const key = `${item.url}\u0000${item.sectionTitle}`;
721            if (seen.has(key)) return false;
722            seen.add(key);
723            return true;
724          })
725          .slice(0, 50)
726          .map((item) => ({
727            pageTitle: item.pageTitle,
728            sectionTitle: item.sectionTitle,
729            url: item.url,
730            excerpt: contextualExcerpt(item.text || item.excerpt, term),
731          }));
732      };
733
734      const searchPagefind = async (term) => {
735        const pf = await loadPagefind();
736        const search = await pf.search(term);
737
738        const pageData = await Promise.all(
739          search.results.map(async (result) => result.data())
740        );
741
742        const sectionResults = [];
743
744        pageData.forEach((data) => {
745          const pageTitle =
746            data.meta?.title ||
747            data.meta?.og_title ||
748            "Cicci Research";
749
750          if (Array.isArray(data.sub_results) && data.sub_results.length) {
751            data.sub_results.forEach((sub) => {
752              sectionResults.push({
753                pageTitle,
754                sectionTitle: sub.title || pageTitle,
755                url: sub.url || data.url,
756                excerpt: sub.excerpt || data.excerpt || "",
757              });
758            });
759          } else {
760            sectionResults.push({
761              pageTitle,
762              sectionTitle: pageTitle,
763              url: data.url,
764              excerpt: data.excerpt || "",
765            });
766          }
767        });
768
769        const seen = new Set();
770
771        return sectionResults.filter((item) => {
772          const key = `${item.url}\u0000${item.sectionTitle}`;
773          if (!item.url || seen.has(key)) return false;
774          seen.add(key);
775          return true;
776        });
777      };
778
779      const renderResult = (item, index) => {
780        const pageTitle = item.pageTitle || "Cicci Research";
781        const sectionTitle =
782          item.sectionTitle && item.sectionTitle !== pageTitle
783            ? item.sectionTitle
784            : "";
785
786        return `
787          <a
788            class="site-search__result"
789            href="${escapeHtml(item.url)}"
790            role="option"
791            aria-selected="false"
792            data-result-index="${index}"
793            data-result-page="${escapeHtml(pageTitle)}"
794            data-result-section="${escapeHtml(sectionTitle)}"
795          >
796            <span class="site-search__result-icon" aria-hidden="true">
797              <svg viewBox="0 0 24 24" fill="none">
798                <path d="M7 4h7l3 3v13H7z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" />
799                <path d="M14 4v4h4M10 12h4M10 15h4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
800              </svg>
801            </span>
802            <span class="site-search__result-copy">
803              <span class="site-search__result-breadcrumb">
804                ${escapeHtml(pageTitle)}
805                ${
806                  sectionTitle
807                    ? `<span aria-hidden="true">›</span><strong>${escapeHtml(sectionTitle)}</strong>`
808                    : ""
809                }
810              </span>
811              <span class="site-search__excerpt">${item.excerpt || ""}</span>
812            </span>
813          </a>
814        `;
815      };
816
817      const searchSite = async (term) => {
818        const trimmed = term.trim();
819
820        clearLandingHighlight();
821
822        if (trimmed.length < 2) {
823          resetSearchView();
824
825          if (trimmed.length === 1) {
826            status.textContent = "Type one more character to search.";
827          }
828
829          return;
830        }
831
832        recommendations.hidden = true;
833        results.innerHTML = "";
834        resetSelection();
835        status.textContent = "Searching…";
836
837        const selectedEngine = await loadEngine();
838        if (!selectedEngine) return;
839
840        try {
841          let items =
842            selectedEngine === "pagefind"
843              ? await searchPagefind(trimmed)
844              : await searchFallback(trimmed);
845
846          /*
847           * Pagefind is still the preferred production engine, but if a
848           * dynamic/static edge case returns no results, try the generated
849           * JSON index before showing "No results".
850           */
851          if (
852            selectedEngine === "pagefind" &&
853            items.length === 0
854          ) {
855            items = await searchFallback(trimmed);
856          }
857
858          if (input.value.trim() !== trimmed) return;
859
860          if (!items.length) {
861            results.innerHTML = `
862              <div class="site-search__empty">
863                <strong>No results for “${escapeHtml(trimmed)}”</strong>
864                <span>Try a shorter term or check the spelling.</span>
865              </div>
866            `;
867            status.textContent = "No matching sections.";
868            return;
869          }
870
871          results.innerHTML = items
872            .map((item, index) => renderResult(item, index))
873            .join("");
874
875          status.textContent =
876            `${items.length} matching section${items.length === 1 ? "" : "s"}.`;
877        } catch (error) {
878          console.error("Website search failed:", error);
879          results.innerHTML = "";
880          status.textContent = "Search could not be completed.";
881        }
882      };
883
884      /* -------------------------------------------------------------- */
885      /* Result navigation                                              */
886      /* -------------------------------------------------------------- */
887
888      const openResult = (link) => {
889        if (!(link instanceof HTMLAnchorElement)) return;
890
891        const targetUrl = new URL(link.href, window.location.href);
892        const currentUrl = new URL(window.location.href);
893        const term = input.value.trim();
894        const sectionTitle = link.getAttribute("data-result-section") || "";
895
896        rememberSearch(term);
897
898        if (targetUrl.pathname === currentUrl.pathname) {
899          closeSearch({
900            clearHighlight: false,
901            resetQuery: true,
902          });
903
904          window.setTimeout(() => {
905            focusSearchMatch(
906              term,
907              targetUrl.hash,
908              sectionTitle
909            );
910          }, 80);
911
912          return;
913        }
914
915        /*
916         * Store the landing request for exactly one navigation, then use a
917         * CLEAN URL. No query string is exposed and reload cannot repeat it.
918         */
919        try {
920          sessionStorage.setItem(
921            PENDING_KEY,
922            JSON.stringify({
923              path: targetUrl.pathname,
924              term,
925              sectionTitle,
926              hash: targetUrl.hash,
927              createdAt: Date.now(),
928            })
929          );
930        } catch {
931          // The target page still opens if sessionStorage is unavailable.
932        }
933
934        window.location.assign(targetUrl.pathname);
935      };
936
937      const selectResult = (nextIndex) => {
938        const links = [...results.querySelectorAll("a.site-search__result")];
939
940        if (!links.length) {
941          selectedResultIndex = -1;
942          return;
943        }
944
945        selectedResultIndex =
946          ((nextIndex % links.length) + links.length) % links.length;
947
948        links.forEach((link, index) => {
949          const selected = index === selectedResultIndex;
950          link.classList.toggle("site-search__result--selected", selected);
951          link.setAttribute("aria-selected", String(selected));
952        });
953
954        links[selectedResultIndex]?.scrollIntoView({
955          block: "nearest",
956        });
957      };
958
959      /* -------------------------------------------------------------- */
960      /* Events                                                         */
961      /* -------------------------------------------------------------- */
962
963      openButton.addEventListener("click", openSearch);
964
965      closeButtons.forEach((button) => {
966        button.addEventListener("click", () => {
967          closeSearch({
968            clearHighlight: true,
969            resetQuery: true,
970          });
971        });
972      });
973
974      clearButton.addEventListener("click", () => {
975        clearActiveSearch();
976      });
977
978      /*
979       * Browsers fire "search" when their native type=search cancel control is
980       * used. This guarantees that both our X and a browser-provided X clear
981       * the page highlight too.
982       */
983      input.addEventListener("search", () => {
984        if (input.value === "") {
985          clearActiveSearch();
986        }
987      });
988
989      input.addEventListener("input", () => {
990        window.clearTimeout(debounceTimer);
991        updateClearButton();
992        resetSelection();
993        clearLandingHighlight();
994
995        debounceTimer = window.setTimeout(() => {
996          searchSite(input.value);
997        }, 100);
998      });
999
1000      recommendations.addEventListener("click", (event) => {
1001        const suggestion =
1002          event.target instanceof Element
1003            ? event.target.closest("[data-search-suggestion]")
1004            : null;
1005
1006        if (suggestion instanceof HTMLButtonElement) {
1007          const query = suggestion.getAttribute("data-search-suggestion") || "";
1008
1009          input.value = query;
1010          updateClearButton();
1011          input.focus();
1012          searchSite(query);
1013        }
1014      });
1015
1016      recentList.addEventListener("click", (event) => {
1017        const item =
1018          event.target instanceof Element
1019            ? event.target.closest("[data-recent-search]")
1020            : null;
1021
1022        if (!(item instanceof HTMLButtonElement)) return;
1023
1024        const query = item.getAttribute("data-recent-search") || "";
1025
1026        input.value = query;
1027        updateClearButton();
1028        input.focus();
1029        searchSite(query);
1030      });
1031
1032      recentList.addEventListener("click", (event) => {
1033        const removeButton =
1034          event.target instanceof Element
1035            ? event.target.closest("[data-remove-recent-search]")
1036            : null;
1037
1038        if (!(removeButton instanceof HTMLButtonElement)) return;
1039
1040        event.stopPropagation();
1041
1042        const term =
1043          removeButton.getAttribute("data-remove-recent-search") || "";
1044        const key = normalize(term);
1045
1046        writeRecentSearches(
1047          readRecentSearches().filter(
1048            (item) => normalize(item) !== key
1049          )
1050        );
1051
1052        renderRecentSearches();
1053      });
1054
1055      clearHistoryButton.addEventListener("click", () => {
1056        try {
1057          localStorage.removeItem(RECENT_KEY);
1058        } catch {
1059          // Nothing else required.
1060        }
1061
1062        renderRecentSearches();
1063      });
1064
1065      results.addEventListener("click", (event) => {
1066        const link =
1067          event.target instanceof Element
1068            ? event.target.closest("a.site-search__result")
1069            : null;
1070
1071        if (!(link instanceof HTMLAnchorElement)) return;
1072
1073        event.preventDefault();
1074        openResult(link);
1075      });
1076
1077      input.addEventListener("keydown", (event) => {
1078        if (event.key === "ArrowDown") {
1079          event.preventDefault();
1080          selectResult(selectedResultIndex + 1);
1081          return;
1082        }
1083
1084        if (event.key === "ArrowUp") {
1085          event.preventDefault();
1086          selectResult(
1087            selectedResultIndex < 0
1088              ? results.querySelectorAll("a.site-search__result").length - 1
1089              : selectedResultIndex - 1
1090          );
1091          return;
1092        }
1093
1094        if (event.key === "Enter" && selectedResultIndex >= 0) {
1095          const selected = results.querySelector(
1096            `a.site-search__result[data-result-index="${selectedResultIndex}"]`
1097          );
1098
1099          if (selected instanceof HTMLAnchorElement) {
1100            event.preventDefault();
1101            openResult(selected);
1102          }
1103        }
1104      });
1105
1106      document.addEventListener("keydown", (event) => {
1107        const shortcut =
1108          (event.ctrlKey || event.metaKey) &&
1109          event.key.toLowerCase() === "k";
1110
1111        if (shortcut) {
1112          event.preventDefault();
1113
1114          if (overlay.hidden) {
1115            openSearch();
1116          } else {
1117            closeSearch({
1118              clearHighlight: true,
1119              resetQuery: true,
1120            });
1121          }
1122
1123          return;
1124        }
1125
1126        if (event.key === "Escape" && !overlay.hidden) {
1127          closeSearch({
1128            clearHighlight: true,
1129            resetQuery: true,
1130          });
1131        }
1132      });
1133    });
1134  })();
1135</script>
1135<button type="button" class="site-navbar__menu-button" aria-label="Open navigation menu" aria-expanded="false" aria-controls="mobile-navigation" data-menu-button><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path data-menu-open-icon d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path><path data-menu-close-icon hidden d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg></button></div></div><div id="mobile-navigation" class="site-navbar__mobile-menu" data-mobile-menu hidden><div class="site-navbar__mobile-inner section-container"><div class="site-navbar__mobile-group" data-mobile-group><button type="button" class="site-navbar__mobile-group-trigger" aria-expanded="false" data-mobile-group-trigger><span>Products</span><svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="m6 8 4 4 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"></path></svg></button><div class="site-navbar__mobile-submenu site-navbar__mobile-submenu--products" data-mobile-submenu hidden><a href="/measurement" class="site-navbar__mobile-product-link" data-mobile-link><span class="site-navbar__mobile-product-thumb" aria-hidden="true"><img src="/images/products/arkeo-platform.png" alt="" loading="lazy"></span><span><strong>Measurement</strong><small>ARKEO measurement platform</small></span></a><a href="/automation" class="site-navbar__mobile-product-link" data-mobile-link><span class="site-navbar__mobile-product-thumb" aria-hidden="true"><img src="/images/automation/charon-system-overview.png" alt="" loading="lazy"></span><span><strong>Automation</strong><small>CHARON automation platform</small></span></a><div class="site-navbar__mobile-product-nested" data-mobile-product-nested><div class="site-navbar__mobile-product-row"><a href="/light-sources" class="site-navbar__mobile-product-link" data-mobile-link><span class="site-navbar__mobile-product-thumb" aria-hidden="true"><img src="/images/products/nav/arkeo-soaker-xl.png" alt="" loading="lazy"></span><span><strong>Light Sources</strong><small>Controlled LED illumination</small></span></a><button type="button" class="site-navbar__mobile-product-submenu-trigger" aria-label="Show Light Sources" aria-expanded="false" data-mobile-product-submenu-trigger>→</button></div><div class="site-navbar__mobile-product-children" data-mobile-product-children hidden><a href="/light-sources/arkeo-soaker-xl" class="site-navbar__mobile-product-link site-navbar__mobile-product-link--child" data-mobile-link><span class="site-navbar__mobile-product-thumb" aria-hidden="true"><img src="/images/products/nav/arkeo-soaker-xl.png" alt="" loading="lazy"></span><span><strong>ARKEO Soaker XL</strong><small>Large-area stability illumination</small></span></a><a href="/light-sources/arkeo-soaker-ms" class="site-navbar__mobile-product-link site-navbar__mobile-product-link--child" data-mobile-link><span class="site-navbar__mobile-product-thumb" aria-hidden="true"><img src="/images/products/nav/arkeo-soaker-ms.png" alt="" loading="lazy"></span><span><strong>ARKEO Soaker M/S</strong><small>Compact programmable LED sources</small></span></a><a href="/light-sources/arkeo-96multiled" class="site-navbar__mobile-product-link site-navbar__mobile-product-link--child" data-mobile-link><span class="site-navbar__mobile-product-thumb" aria-hidden="true"><img src="/images/products/nav/arkeo-96multiled.png" alt="" loading="lazy"></span><span><strong>ARKEO 96multiled</strong><small>Multiwell and multi-plate illumination</small></span></a></div></div><div class="site-navbar__mobile-product-nested" data-mobile-product-nested><div class="site-navbar__mobile-product-row"><a href="/environmental-chambers" class="site-navbar__mobile-product-link" data-mobile-link><span class="site-navbar__mobile-product-thumb" aria-hidden="true"><img src="/images/products/nav/chamber-xxl.png" alt="" loading="lazy"></span><span><strong>Environmental Chambers</strong><small>Temperature &amp; atmosphere control</small></span></a><button type="button" class="site-navbar__mobile-product-submenu-trigger" aria-label="Show Environmental Chambers" aria-expanded="false" data-mobile-product-submenu-trigger>→</button></div><div class="site-navbar__mobile-product-children" data-mobile-product-children hidden><a href="/environmental-chambers/chamber-xxl" class="site-navbar__mobile-product-link site-navbar__mobile-product-link--child" data-mobile-link><span class="site-navbar__mobile-product-thumb" aria-hidden="true"><img src="/images/products/nav/chamber-xxl.png" alt="" loading="lazy"></span><span><strong>Chamber XXL</strong><small>Large-format environmental control</small></span></a><a href="/environmental-chambers/chamber-m" class="site-navbar__mobile-product-link site-navbar__mobile-product-link--child" data-mobile-link><span class="site-navbar__mobile-product-thumb" aria-hidden="true"><img src="/images/products/nav/chamber-m.png" alt="" loading="lazy"></span><span><strong>Chamber M</strong><small>Compact benchtop controlled chamber</small></span></a><a href="/environmental-chambers/chamber-4-comps" class="site-navbar__mobile-product-link site-navbar__mobile-product-link--child" data-mobile-link><span class="site-navbar__mobile-product-thumb" aria-hidden="true"><img src="/images/products/nav/chamber-4-comps.png" alt="" loading="lazy"></span><span><strong>Chamber 4-comps</strong><small>Four-position chamber concept</small></span></a><a href="/environmental-chambers/chamber-x-ray" class="site-navbar__mobile-product-link site-navbar__mobile-product-link--child" data-mobile-link><span class="site-navbar__mobile-product-thumb" aria-hidden="true"><img src="/images/products/nav/chamber-x-ray.png" alt="" loading="lazy"></span><span><strong>Chamber X-Ray</strong><small>In-operando X-ray compatible chamber</small></span></a></div></div><a href="/installations" class="site-navbar__mobile-menu-card-link" data-mobile-link><strong>Installations</strong></a></div></div><div class="site-navbar__mobile-group" data-mobile-group><button type="button" class="site-navbar__mobile-group-trigger" aria-expanded="false" data-mobile-group-trigger><span>Research</span><svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="m6 8 4 4 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"></path></svg></button><div class="site-navbar__mobile-submenu site-navbar__mobile-submenu--cards" data-mobile-submenu hidden><a href="/publications" class="site-navbar__mobile-menu-card-link" data-mobile-link><strong>Publications</strong></a><a href="/projects" class="site-navbar__mobile-menu-card-link" data-mobile-link><strong>Research Projects</strong></a><a href="/Conferences" class="site-navbar__mobile-menu-card-link" data-mobile-link><strong>News &amp; Events</strong></a></div></div><div class="site-navbar__mobile-group" data-mobile-group><button type="button" class="site-navbar__mobile-group-trigger" aria-expanded="false" data-mobile-group-trigger><span>Support</span><svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="m6 8 4 4 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"></path></svg></button><div class="site-navbar__mobile-submenu site-navbar__mobile-submenu--cards" data-mobile-submenu hidden><a href="/documentation" class="site-navbar__mobile-menu-card-link" data-mobile-link><strong>Documentation</strong></a><a href="/remoteSupport" class="site-navbar__mobile-menu-card-link" data-mobile-link><strong>Support services</strong></a><a href="/training" class="site-navbar__mobile-menu-card-link" data-mobile-link><strong>Training</strong></a></div></div><a href="/AboutUs" class="site-navbar__mobile-link" data-mobile-link>About Us</a></div></div></nav>
1135<script>
1136  (() => {
1137    document.querySelectorAll("[data-site-navbar]").forEach((navbar) => {
1138      if (!(navbar instanceof HTMLElement) || navbar.dataset.navReady === "true") return;
1139      navbar.dataset.navReady = "true";
1140
1141      const menuButton = navbar.querySelector("[data-menu-button]");
1142      const mobileMenu = navbar.querySelector("[data-mobile-menu]");
1143      const openIcon = navbar.querySelector("[data-menu-open-icon]");
1144      const closeIcon = navbar.querySelector("[data-menu-close-icon]");
1145      const desktopDropdowns = Array.from(navbar.querySelectorAll("[data-desktop-dropdown]"));
1146      const mobileGroups = Array.from(navbar.querySelectorAll("[data-mobile-group]"));
1147      const productNested = Array.from(navbar.querySelectorAll("[data-product-nested]"));
1148      const mobileProductNested = Array.from(navbar.querySelectorAll("[data-mobile-product-nested]"));
1149
1150      const closeProductNestedItem = (item) => {
1151        if (!(item instanceof HTMLElement)) return;
1152        const trigger = item.querySelector("[data-product-submenu-trigger]");
1153        const submenu = item.querySelector("[data-product-submenu]");
1154        item.classList.remove("is-open");
1155        if (trigger instanceof HTMLButtonElement) trigger.setAttribute("aria-expanded", "false");
1156        if (submenu instanceof HTMLElement) submenu.hidden = true;
1157      };
1158
1159      const closeProductNested = (except = null) => {
1160        productNested.forEach((item) => {
1161          if (item !== except) closeProductNestedItem(item);
1162        });
1163      };
1164
1165      const openProductNestedItem = (item) => {
1166        if (!(item instanceof HTMLElement)) return;
1167        const trigger = item.querySelector("[data-product-submenu-trigger]");
1168        const submenu = item.querySelector("[data-product-submenu]");
1169        closeProductNested(item);
1170        item.classList.add("is-open");
1171        if (trigger instanceof HTMLButtonElement) trigger.setAttribute("aria-expanded", "true");
1172        if (submenu instanceof HTMLElement) submenu.hidden = false;
1173      };
1174
1175      const closeDesktopDropdown = (dropdown) => {
1176        if (!(dropdown instanceof HTMLElement)) return;
1177        const trigger = dropdown.querySelector("[data-dropdown-trigger]");
1178        dropdown.classList.remove("is-open");
1179        closeProductNested();
1180        if (trigger instanceof HTMLButtonElement) trigger.setAttribute("aria-expanded", "false");
1181      };
1182
1183      const closeDesktopDropdowns = (except = null) => {
1184        desktopDropdowns.forEach((dropdown) => {
1185          if (dropdown !== except) closeDesktopDropdown(dropdown);
1186        });
1187      };
1188
1189      productNested.forEach((item) => {
1190        if (!(item instanceof HTMLElement)) return;
1191        const trigger = item.querySelector("[data-product-submenu-trigger]");
1192        if (!(trigger instanceof HTMLButtonElement)) return;
1193        item.addEventListener("mouseenter", () => openProductNestedItem(item));
1194        item.addEventListener("mouseleave", () => closeProductNestedItem(item));
1195        item.addEventListener("focusin", () => openProductNestedItem(item));
1196        item.addEventListener("focusout", (event) => {
1197          const next = event.relatedTarget;
1198          if (!(next instanceof Node) || !item.contains(next)) closeProductNestedItem(item);
1199        });
1200        trigger.addEventListener("click", (event) => {
1201          event.preventDefault();
1202          event.stopPropagation();
1203          const willOpen = !item.classList.contains("is-open");
1204          if (willOpen) openProductNestedItem(item);
1205          else closeProductNestedItem(item);
1206        });
1207      });
1208
1209      desktopDropdowns.forEach((dropdown) => {
1210        if (!(dropdown instanceof HTMLElement)) return;
1211        const trigger = dropdown.querySelector("[data-dropdown-trigger]");
1212        if (!(trigger instanceof HTMLButtonElement)) return;
1213        trigger.addEventListener("click", (event) => {
1214          event.stopPropagation();
1215          const willOpen = !dropdown.classList.contains("is-open");
1216          closeDesktopDropdowns(dropdown);
1217          dropdown.classList.toggle("is-open", willOpen);
1218          trigger.setAttribute("aria-expanded", String(willOpen));
1219        });
1220      });
1221
1222      const closeMobileGroup = (group) => {
1223        if (!(group instanceof HTMLElement)) return;
1224        const trigger = group.querySelector("[data-mobile-group-trigger]");
1225        const submenu = group.querySelector("[data-mobile-submenu]");
1226        group.classList.remove("is-open");
1227        if (trigger instanceof HTMLButtonElement) trigger.setAttribute("aria-expanded", "false");
1228        if (submenu instanceof HTMLElement) submenu.hidden = true;
1229      };
1230
1231      mobileGroups.forEach((group) => {
1232        if (!(group instanceof HTMLElement)) return;
1233        const trigger = group.querySelector("[data-mobile-group-trigger]");
1234        const submenu = group.querySelector("[data-mobile-submenu]");
1235        if (!(trigger instanceof HTMLButtonElement) || !(submenu instanceof HTMLElement)) return;
1236        trigger.addEventListener("click", () => {
1237          const willOpen = !group.classList.contains("is-open");
1238          mobileGroups.forEach((other) => { if (other !== group) closeMobileGroup(other); });
1239          group.classList.toggle("is-open", willOpen);
1240          trigger.setAttribute("aria-expanded", String(willOpen));
1241          submenu.hidden = !willOpen;
1242        });
1243      });
1244
1245      const closeMobileProductNested = (except = null) => {
1246        mobileProductNested.forEach((item) => {
1247          if (!(item instanceof HTMLElement) || item === except) return;
1248          item.classList.remove("is-open");
1249          const trigger = item.querySelector("[data-mobile-product-submenu-trigger]");
1250          const children = item.querySelector("[data-mobile-product-children]");
1251          if (trigger instanceof HTMLButtonElement) trigger.setAttribute("aria-expanded", "false");
1252          if (children instanceof HTMLElement) children.hidden = true;
1253        });
1254      };
1255
1256      mobileProductNested.forEach((item) => {
1257        if (!(item instanceof HTMLElement)) return;
1258        const trigger = item.querySelector("[data-mobile-product-submenu-trigger]");
1259        const children = item.querySelector("[data-mobile-product-children]");
1260        if (!(trigger instanceof HTMLButtonElement) || !(children instanceof HTMLElement)) return;
1261        trigger.addEventListener("click", () => {
1262          const willOpen = !item.classList.contains("is-open");
1263          closeMobileProductNested(item);
1264          item.classList.toggle("is-open", willOpen);
1265          trigger.setAttribute("aria-expanded", String(willOpen));
1266          children.hidden = !willOpen;
1267        });
1268      });
1269
1270      if (menuButton instanceof HTMLButtonElement && mobileMenu instanceof HTMLElement) {
1271        const setMobileMenu = (isOpen) => {
1272          menuButton.setAttribute("aria-expanded", String(isOpen));
1273          menuButton.setAttribute("aria-label", isOpen ? "Close navigation menu" : "Open navigation menu");
1274          mobileMenu.hidden = !isOpen;
1275          if (openIcon instanceof SVGElement) openIcon.hidden = isOpen;
1276          if (closeIcon instanceof SVGElement) closeIcon.hidden = !isOpen;
1277          if (!isOpen) {
1278            mobileGroups.forEach(closeMobileGroup);
1279            closeMobileProductNested();
1280          }
1281        };
1282        setMobileMenu(false);
1283        menuButton.addEventListener("click", () => {
1284          setMobileMenu(menuButton.getAttribute("aria-expanded") !== "true");
1285          closeDesktopDropdowns();
1286        });
1287        navbar.querySelectorAll("[data-mobile-link]").forEach((link) => {
1288          link.addEventListener("click", () => setMobileMenu(false));
1289        });
1290        window.addEventListener("resize", () => {
1291          if (window.innerWidth >= 1024) setMobileMenu(false);
1292        });
1293      }
1294
1295      document.addEventListener("click", (event) => {
1296        if (!(event.target instanceof Node)) return;
1297        if (!navbar.contains(event.target)) closeDesktopDropdowns();
1298      });
1299
1300      document.addEventListener("keydown", (event) => {
1301        if (event.key !== "Escape") return;
1302        closeDesktopDropdowns();
1303        if (menuButton instanceof HTMLButtonElement && mobileMenu instanceof HTMLElement) {
1304          menuButton.setAttribute("aria-expanded", "false");
1305          menuButton.setAttribute("aria-label", "Open navigation menu");
1306          mobileMenu.hidden = true;
1307          if (openIcon instanceof SVGElement) openIcon.hidden = false;
1308          if (closeIcon instanceof SVGElement) closeIcon.hidden = true;
1309          mobileGroups.forEach(closeMobileGroup);
1310          closeMobileProductNested();
1311        }
1312      });
1313    });
1314  })();
1315</script>
1315<main class="charon-page" data-astro-cid-adqwrtdf><div class="section-container charon-shell" data-astro-cid-adqwrtdf><aside class="charon-toc" aria-label="Page sections" data-astro-cid-adqwrtdf><div class="charon-toc-inner" data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>On this page</span><a href="#overview" class="is-active" data-astro-cid-adqwrtdf>Overview</a><a href="#process" data-astro-cid-adqwrtdf>Process</a><a href="#modularity" data-astro-cid-adqwrtdf>Modularity</a><a href="#specifications" data-astro-cid-adqwrtdf>Specifications</a><a href="#software" data-astro-cid-adqwrtdf>Software</a><a href="#monitoring" data-astro-cid-adqwrtdf>In-situ monitoring</a><a href="#publications" data-astro-cid-adqwrtdf>Publications</a></div></aside><div class="charon-content" data-astro-cid-adqwrtdf><section id="overview" class="charon-hero scroll-section" aria-labelledby="charon-title" data-astro-cid-adqwrtdf><div class="charon-hero-copy" data-astro-cid-adqwrtdf><div class="mb-5 text-sm uppercase tracking-[0.3em] text-blue-400" data-astro-cid-adqwrtdf>Solar-cell manufacturing automation</div><h1 id="charon-title" class="mb-7 text-5xl font-bold leading-[0.95] tracking-tight sm:text-6xl lg:text-7xl" data-astro-cid-adqwrtdf>Charon</h1><p class="max-w-3xl text-xl leading-relaxed text-gray-300" data-astro-cid-adqwrtdf>A modular blade-coating platform combining automated liquid deposition, heated substrate handling and inline drying in a compact system suitable for glovebox operation.</p><div class="mt-10 flex flex-wrap gap-4" data-astro-cid-adqwrtdf><a href="#process" class="rounded-2xl bg-blue-600 px-7 py-4 font-medium transition hover:bg-blue-500" data-astro-cid-adqwrtdf>Explore the system</a><a href="#specifications" class="rounded-2xl border border-blue-500/50 px-7 py-4 font-medium text-blue-300 transition hover:bg-blue-500/10" data-astro-cid-adqwrtdf>Technical data</a></div></div><figure class="charon-hero-image" data-astro-cid-adqwrtdf><button type="button" class="charon-photo-button" data-charon-photo data-full-src="/images/automation/charon-system-overview.png" data-full-alt="Charon blade coating automation system installed in a laboratory" data-full-caption="Charon laboratory system" aria-label="Open Charon system image" data-astro-cid-adqwrtdf><img src="/images/automation/charon-system-overview.png" alt="Charon blade coating automation system installed in a laboratory" loading="eager" data-astro-cid-adqwrtdf></button><figcaption data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>A4 heated vacuum chuck</span><span data-astro-cid-adqwrtdf>Glovebox-compatible format</span></figcaption></figure></section><section id="process" class="scroll-section charon-section" data-astro-cid-adqwrtdf><div class="charon-heading" data-astro-cid-adqwrtdf><div class="text-sm uppercase tracking-[0.3em] text-blue-400" data-astro-cid-adqwrtdf>Automated workflow</div><h2 data-astro-cid-adqwrtdf id="from-liquid-dosing-to-inline-drying">From liquid dosing to inline drying</h2><p data-astro-cid-adqwrtdf>Charon coordinates the principal stages of blade coating inside one programmable process.</p></div><div class="charon-step-grid" data-astro-cid-adqwrtdf><article class="charon-card charon-step-card" data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>01</span><h3 data-astro-cid-adqwrtdf id="fix-the-substrate">Fix the substrate</h3><p data-astro-cid-adqwrtdf>The substrate is held on the heated A4 vacuum chuck throughout the coating process.</p></article><article class="charon-card charon-step-card" data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>02</span><h3 data-astro-cid-adqwrtdf id="dose-the-material">Dose the material</h3><p data-astro-cid-adqwrtdf>An automatic syringe pump dispenses the selected solution at a controlled rate.</p></article><article class="charon-card charon-step-card" data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>03</span><h3 data-astro-cid-adqwrtdf id="apply-the-film">Apply the film</h3><p data-astro-cid-adqwrtdf>The mechanized blade moves across the substrate with programmable speed and gap height.</p></article><article class="charon-card charon-step-card" data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>04</span><h3 data-astro-cid-adqwrtdf id="dry-inline">Dry inline</h3><p data-astro-cid-adqwrtdf>Infrared lamps and heated Air or Nitrogen support controlled solvent removal after coating.</p></article></div></section><section id="modularity" class="scroll-section charon-section" data-astro-cid-adqwrtdf><div class="charon-feature-panel" data-astro-cid-adqwrtdf><div class="charon-feature-copy" data-astro-cid-adqwrtdf><div class="text-sm uppercase tracking-[0.3em] text-blue-400" data-astro-cid-adqwrtdf>Modular architecture</div><h2 data-astro-cid-adqwrtdf id="configured-around-the-deposition-process">Configured around the deposition process</h2><p data-astro-cid-adqwrtdf>Charon follows the same modular philosophy as ARKEO. The platform can combine Cicci Re
1315search modules, compatible ARKEO overhead modules and selected third-party equipment within one integrated system.</p><ul data-astro-cid-adqwrtdf><li data-astro-cid-adqwrtdf>Modular hardware configuration</li><li data-astro-cid-adqwrtdf>Expandable process and monitoring modules</li><li data-astro-cid-adqwrtdf>Unified control from the Charon interface</li><li data-astro-cid-adqwrtdf>Compact integration for glovebox environments</li></ul></div><button type="button" class="charon-feature-image" data-charon-photo data-full-src="/images/automation/charon-compact-system.png" data-full-alt="Compact enclosed version of the Charon automation platform" data-full-caption="Compact modular Charon configuration" aria-label="Open compact Charon image" data-astro-cid-adqwrtdf><img src="/images/automation/charon-compact-system.png" alt="Compact enclosed version of the Charon automation platform" loading="lazy" data-astro-cid-adqwrtdf></button></div></section><section id="specifications" class="scroll-section charon-section" data-astro-cid-adqwrtdf><div class="charon-heading" data-astro-cid-adqwrtdf><div class="text-sm uppercase tracking-[0.3em] text-blue-400" data-astro-cid-adqwrtdf>Technical data</div><h2 data-astro-cid-adqwrtdf id="core-specifications">Core specifications</h2></div><div class="charon-spec-grid" data-astro-cid-adqwrtdf><article class="charon-card charon-spec-card" data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>Working area</span><strong data-astro-cid-adqwrtdf>Up to 300 × 210 mm</strong></article><article class="charon-card charon-spec-card" data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>Vacuum chuck</span><strong data-astro-cid-adqwrtdf>A4 format</strong></article><article class="charon-card charon-spec-card" data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>Chuck temperature</span><strong data-astro-cid-adqwrtdf>25–150 °C</strong></article><article class="charon-card charon-spec-card" data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>Drawing speed</span><strong data-astro-cid-adqwrtdf>0–500 mm/s</strong></article><article class="charon-card charon-spec-card" data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>Speed resolution</span><strong data-astro-cid-adqwrtdf>1 mm/s</strong></article><article class="charon-card charon-spec-card" data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>Speed tolerance</span><strong data-astro-cid-adqwrtdf>1%</strong></article><article class="charon-card charon-spec-card" data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>Gap height</span><strong data-astro-cid-adqwrtdf>15–3000 μm</strong></article><article class="charon-card charon-spec-card" data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>Gap tolerance</span><strong data-astro-cid-adqwrtdf>±10 μm</strong></article></div></section><section id="software" class="scroll-section charon-section" data-astro-cid-adqwrtdf><div class="charon-software-grid" data-astro-cid-adqwrtdf><button type="button" class="charon-software-image" data-charon-photo data-full-src="/images/automation/charon-software-interface.png" data-full-alt="Charon LabVIEW touchscreen control interface" data-full-caption="Example interface for coating, heated airflow and infrared-lamp control" aria-label="Open Charon software interface image" data-astro-cid-adqwrtdf><img src="/images/automation/charon-software-interface.png" alt="Charon LabVIEW touchscreen control interface" loading="lazy" data-astro-cid-adqwrtdf></button><div data-astro-cid-adqwrtdf><div class="mb-5 text-sm uppercase tracking-[0.3em] text-blue-400" data-astro-cid-adqwrtdf>Integrated software</div><h2 class="mb-6 text-4xl font-bold leading-tight lg:text-5xl" data-astro-cid-adqwrtdf id="one-interface-for-the-complete-routine">One interface for the complete routine</h2><p class="text-lg leading-relaxed text-gray-300" data-astro-cid-adqwrtdf>The LabVIEW-based software provides real-time monitoring and control of the coating system. Users can build customized recipes that coordinate movement, syringe dispensing, chuck temperature, heated gas flow and infrared drying.</p><div class="charon-software-points" data-astro-cid-adqwrtdf><span data-astro-cid-adqwrtdf>Custom recipes</span><span data-astro-cid-adqwrtdf>Real-time control</span><span data-astro-cid-adqwrtdf>Coordinated modules</span><span data-astro-cid-adqwrtdf>Touchscreen operation</span></div></div></div></section><section id="monitoring" class="scroll-section charon-section" data-astro-cid-adqwrtdf><div class="charon-heading" data-astro-cid-adqwrtdf><div class="text-sm uppercase tracking-[0.3em] text-blue-400" data-astro-cid-adqwrtdf>Optional modules</div><h2 data-astro-cid-adqwrtdf id="in-situ-monitoring-and-characterization">In-situ monitoring and characterization</h2><p data-astro-cid-adqwrtdf>Monitoring accessories can be added according to the required deposition workflow.</p></div><div class="charon-module-grid" data-astro-cid-adqwrtdf><article class="charon-card" data-astro-cid-adqwrtdf><h3 data-astro-cid-adqwrtdf id="scientific-camera">Scientific camera</h3><p data-astro-cid-adqwrtdf>Visual in-situ monitoring of coating and film formation.</p></article><article class="charon-card" data-astro-cid-adqwrtdf><h3 data-astro-cid-adqwrtdf id="spectroradiometer">Spectroradiometer</h3><p data-astro-cid-adqwrtdf>Optical monitoring during deposition and drying.</p></article><article class="charon-card" data-astro-cid-adqwrtdf><h3 data-astro-cid-adqwrtdf id="pl-light-source">PL light source</h3><p data-astro-cid-adqwrtdf>Integrated excitation for in-situ photoluminescence workflows.</p></article><article class="charon-card" data-astro-cid-adqwrtdf><h3 data-astro-cid-adqwrtdf id="third-party-equipment">Third-party equipment</h3><p data-astro-cid-adqwrtdf>Selected external instruments can be integrated into the same control environment.</p></article></div><div class="charon-gallery" aria-label="Charon image gallery" data-astro-cid-adqwrtdf><figure data-astro-cid-adqwrtdf><button type="button" data-charon-photo data-full-src="/images/automation/charon-system-overview.png" data-full-alt="Charon blade coating automation system installed in a laboratory" data-full-caption="Charon laboratory system" aria-label="Open image: Charon laboratory system" data-astro-cid-adqwrtdf><img src="/images/automation/charon-system-overview.png" alt="Charon blade coating automation system installed in a laboratory" loading="lazy" data-astro-cid-adqwrtdf></button><figcaption data-astro-cid-adqwrtdf>Charon laboratory system</figcaption></figure><figure data-astro-cid-adqwrtdf><button type="button" data-charon-photo data-full-src="/images/automation/charon-coating-head.png" data-full-alt="Close view of the Charon heated chuck, coating head and inline dryer" data-full-caption="Coating and inline-drying area" aria-label="Open image: Coating and inline-drying area" data-astro-cid-adqwrtdf><img src="/images/automation/charon-coating-head.png" alt="Close view of the Charon heated chuck, coating head and inline dryer" loading="lazy" data-astro-cid-adqwrtdf></button><figcaption data-astro-cid-adqwrtdf>Coating and inline-drying area</figcaption></figure><figure data-astro-cid-adqwrtdf><button type="button" data-charon-photo data-full-src="/images/automation/charon-compact-system.png" data-full-alt="Compact enclosed version of the Charon automation platform" data-full-caption="Compact modular configuration" aria-label="Open image: Compact modular configuration" data-astro-cid-adqwrtdf><img src="/images/automation/charon-compact-system.png" alt="Compact enclosed version of the Charon automation platform" loading="lazy" data-astro-cid-adqwrtdf></button><figcaption data-astro-cid-adqwrtdf>Compact modular configuration</figcaption></figure><figure data-astro-cid-adqwrtdf><button type="button" data-charon-photo data-full-src="/images/automation/charon-blade-assembly.png" data-full-alt="Mechanical blade coating assembly used in the Charon system" data-full-caption="Mechanical blade-coating assembly" aria-label="Open image: Mechanical blade-coating assembly" data-astro-cid-adqwrtdf><img src="/images/automation/charon-blade-assembly.png" alt="Mechanical blade coating assembly used in the Charon system" loading="lazy" data-astro-cid-adqwrtdf></button><figcaption data-astro-cid-adqwrtdf>Mechanical blade-coating assembly</figcaption></figure><figure data-astro-cid-adqwrtdf><button type="button" data-charon-photo data-full-src="/images/automation/charon-cad-integration.png" data-full-alt="CAD view showing the Charon coating assembly integrated into the automation platform" data-full-caption="CAD view of the modular coating assembly" aria-label="Open image: CAD view of the modular coating assembly" data-astro-cid-adqwrtdf><img src="/images/automation/charon-cad-integration.png" alt="CAD view showing the Charon coating assembly integrated into the automation platform" loading="lazy" data-astro-cid-adqwrtdf></button><figcaption data-astro-cid-adqwrtdf>CAD view of the modular coating assembly</figcaption></figure></div></section><section id="publications" class="scroll-section charon-section" data-charon-publications-root data-astro-cid-adqwrtdf><div class="charon-publications-heading" data-astro-cid-adqwrtdf><div data-astro-cid-adqwrtdf><div class="mb-4 text-sm uppercase tracking-[0.3em] text-blue-400" data-astro-cid-adqwrtdf>Featured Charon</div><h2 data-astro-cid-adqwrtdf id="charon-in-scientific-research">Charon in scientific research</h2><p data-astro-cid-adqwrtdf>Peer-reviewed articles and scientific outputs in which Charon was used for scalable coating, controlled drying or automated device fabrication.</p></div><a href="/publications/" class="charon-publications-all" data-astro-cid-adqwrtdf>View all publications<svg viewBox="0 0 24 24" aria-hidden="true" data-astro-cid-adqwrtdf><path d="M7 17 17 7M8 7h9v9" data-astro-cid-adqwrtdf></path></svg></a></div>
1315<div class="charon-publications-status" aria-live="polite" data-astro-cid-adqwrtdf><span class="charon-publications-loader" aria-hidden="true" data-astro-cid-adqwrtdf></span><span data-charon-publications-status data-astro-cid-adqwrtdf>Loading Charon publications…</span></div><div class="charon-publications-grid" data-charon-publications-list aria-busy="true" data-astro-cid-adqwrtdf></div><div class="charon-publications-message" data-charon-publications-empty hidden data-astro-cid-adqwrtdf><strong data-astro-cid-adqwrtdf>No tagged publications found</strong><p data-astro-cid-adqwrtdf>Add the tag <code data-astro-cid-adqwrtdf>Charon</code> to the relevant Zotero records and check that they belong to the collection used by the website.</p></div><div class="charon-publications-message" data-charon-publications-error hidden data-astro-cid-adqwrtdf><strong data-astro-cid-adqwrtdf>Publications could not be loaded</strong><p data-astro-cid-adqwrtdf>The Zotero publication service is temporarily unavailable.</p></div></section><section class="charon-contact" data-astro-cid-adqwrtdf><div data-astro-cid-adqwrtdf><div class="mb-4 text-sm uppercase tracking-[0.3em] text-blue-400" data-astro-cid-adqwrtdf>Configure Charon</div><h2 data-astro-cid-adqwrtdf id="build-the-automation-system-around-your-process">Build the automation system around your process.</h2></div><a href="/cdn-cgi/l/email-protection#d7beb9b1b897b4beb4b4bea5b2a4b2b6a5b4bff9bea3" data-astro-cid-adqwrtdf>Contact Cicci Research</a></section></div></div></main><div class="charon-lightbox" data-charon-lightbox aria-hidden="true" data-astro-cid-adqwrtdf><button type="button" class="charon-lightbox-close" data-charon-lightbox-close aria-label="Close image" data-astro-cid-adqwrtdf>×</button><div class="charon-lightbox-content" data-astro-cid-adqwrtdf><img data-charon-lightbox-image src="" alt="" data-astro-cid-adqwrtdf><p data-charon-lightbox-caption data-astro-cid-adqwrtdf></p></div></div>
vendor: 108 bytes, line 1315
1315<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script>
1315<script>
1316  function initCharonPage() {
1317    const page = document.querySelector(".charon-page");
1318
1319    if (!page || page.dataset.initialized === "true") {
1320      return;
1321    }
1322
1323    page.dataset.initialized = "true";
1324
1325    const navLinks = Array.from(document.querySelectorAll(".charon-toc a"));
1326    const sections = Array.from(document.querySelectorAll(".scroll-section"));
1327
1328    if (navLinks.length && sections.length) {
1329      const setActiveLink = (id) => {
1330        navLinks.forEach((link) => {
1331          link.classList.toggle("is-active", link.getAttribute("href") === `#${id}`);
1332        });
1333      };
1334
1335      const observer = new IntersectionObserver(
1336        (entries) => {
1337          const visible = entries
1338            .filter((entry) => entry.isIntersecting)
1339            .sort((a, b) => b.intersectionRatio - a.intersectionRatio);
1340
1341          if (visible[0]) {
1342            setActiveLink(visible[0].target.id);
1343          }
1344        },
1345        {
1346          rootMargin: "-24% 0px -58% 0px",
1347          threshold: [0.05, 0.2, 0.45],
1348        },
1349      );
1350
1351      sections.forEach((section) => observer.observe(section));
1352    }
1353
1354    const lightbox = document.querySelector("[data-charon-lightbox]");
1355    const lightboxImage = document.querySelector("[data-charon-lightbox-image]");
1356    const lightboxCaption = document.querySelector("[data-charon-lightbox-caption]");
1357
1358    const closeLightbox = () => {
1359      if (!lightbox || !lightboxImage || !lightboxCaption) return;
1360
1361      lightbox.classList.remove("is-open");
1362      lightbox.setAttribute("aria-hidden", "true");
1363      document.body.classList.remove("charon-lightbox-open");
1364      lightboxImage.src = "";
1365      lightboxImage.alt = "";
1366      lightboxCaption.textContent = "";
1367    };
1368
1369    document.addEventListener("click", (event) => {
1370      const target = event.target;
1371      if (!(target instanceof Element)) return;
1372
1373      const photo = target.closest("[data-charon-photo]");
1374
1375      if (photo && lightbox && lightboxImage && lightboxCaption) {
1376        lightboxImage.src = photo.dataset.fullSrc || "";
1377        lightboxImage.alt = photo.dataset.fullAlt || "";
1378        lightboxCaption.textContent = photo.dataset.fullCaption || "";
1379
1380        lightbox.classList.add("is-open");
1381        lightbox.setAttribute("aria-hidden", "false");
1382        document.body.classList.add("charon-lightbox-open");
1383        return;
1384      }
1385
1386      if (
1387        target.matches("[data-charon-lightbox-close]") ||
1388        target.matches("[data-charon-lightbox]")
1389      ) {
1390        closeLightbox();
1391      }
1392    });
1393
1394    document.addEventListener("keydown", (event) => {
1395      if (event.key === "Escape") {
1396        closeLightbox();
1397      }
1398    });
1399
1400    const publicationRoot = page.querySelector("[data-charon-publications-root]");
1401
1402    if (publicationRoot) {
1403      initialiseCharonPublications(publicationRoot);
1404    }
1405  }
1406
1407  function createCharonExternalIcon() {
1408    const namespace = "http://www.w3.org/2000/svg";
1409    const svg = document.createElementNS(namespace, "svg");
1410    svg.setAttribute("viewBox", "0 0 24 24");
1411    svg.setAttribute("aria-hidden", "true");
1412
1413    const path = document.createElementNS(namespace, "path");
1414    path.setAttribute("d", "M7 17 17 7M8 7h9v9");
1415    svg.append(path);
1416
1417    return svg;
1418  }
1419
1420  function createCharonPublicationCard(publication) {
1421    const article = document.createElement("article");
1422    article.className = "charon-publication-card";
1423
1424    const topMeta = document.createElement("div");
1425    topMeta.className = "charon-publication-meta-top";
1426
1427    const year = document.createElement("span");
1428    year.className = "charon-publication-year";
1429    year.textContent = publication.year ? String(publication.year) : "Year unavailable";
1430    topMeta.append(year);
1431
1432    if (publication.type) {
1433      const type = document.createElement("span");
1434      type.className = "charon-publication-type";
1435      type.textContent = publication.type;
1436      topMeta.append(type);
1437    }
1438
1439    const heading = document.createElement("h3");
1440
1441    if (publication.url) {
1442      const titleLink = document.createElement("a");
1443      titleLink.href = publication.url;
1444      titleLink.target = "_blank";
1445      titleLink.rel = "noopener noreferrer";
1446      titleLink.textContent = publication.title || "Untitled publication";
1447      heading.append(titleLink);
1448    } else {
1449      heading.textContent = publication.title || "Untitled publication";
1450    }
1451
1452    article.append(topMeta, heading);
1453
1454    if (publication.authorDisplay) {
1455      const authors = document.createElement("p");
1456      authors.className = "charon-publication-authors";
1457      authors.textContent = publication.authorDisplay;
1458
1459      if (Array.isArray(publication.authors) && publication.authors.length) {
1460        authors.title = publication.authors.join(", ");
1461      }
1462
1463      article.append(authors);
1464    }
1465
1466    if (publication.journal) {
1467      const journal = document.createElement("p");
1468      journal.className = "charon-publication-journal";
1469      journal.textContent = publication.journal;
1470      article.append(journal);
1471    }
1472
1473    if (publication.charonRole) {
1474      const role = document.createElement("p");
1475      role.className = "charon-publication-role";
1476      role.textContent = publication.charonRole;
1477      article.append(role);
1478    }
1479
1480    const actions = document.createElement("div");
1481    actions.className = "charon-publication-actions";
1482
1483    const doi = document.createElement("span");
1484    doi.className = "charon-publication-doi";
1485    doi.textContent = publication.doi ? `DOI: ${publication.doi}` : "Zotero record";
1486    actions.append(doi);
1487
1488    if (publication.url) {
1489      const publicationLink = document.createElement("a");
1490      publicationLink.className = "charon-publication-link";
1491      publicationLink.href = publication.url;
1492      publicationLink.target = "_blank";
1493      publicationLink.rel = "noopener noreferrer";
1494      publicationLink.append(
1495        document.createTextNode("View publication"),
1496        createCharonExternalIcon(),
1497      );
1498      actions.append(publicationLink);
1499    }
1500
1501    article.append(actions);
1502    return article;
1503  }
1504
1505  async function initialiseCharonPublications(root) {
1506    if (root.dataset.publicationsReady === "true") {
1507      return;
1508    }
1509
1510    root.dataset.publicationsReady = "true";
1511
1512    const list = root.querySelector("[data-charon-publications-list]");
1513    const status = root.querySelector("[data-charon-publications-status]");
1514    const statusContainer = root.querySelector(".charon-publications-status");
1515    const empty = root.querySelector("[data-charon-publications-empty]");
1516    const error = root.querySelector("[data-charon-publications-error]");
1517
1518    if (!list || !status || !statusContainer || !empty || !error) {
1519      return;
1520    }
1521
1522    try {
1523      const response = await fetch("/api/charon-publications", {
1524        headers: {
1525          Accept: "application/json",
1526        },
1527        cache: "no-store",
1528      });
1529
1530      if (!response.ok) {
1531        throw new Error(`Publication API returned HTTP ${response.status}`);
1532      }
1533
1534      const data = await response.json();
1535      const publications = Array.isArray(data.publications)
1536        ? data.publications
1537        : [];
1538
1539      list.replaceChildren(...publications.map(createCharonPublicationCard));
1540      list.setAttribute("aria-busy", "false");
1541      statusContainer.classList.add("is-ready");
1542      status.textContent = `${publications.length} Charon publication${
1543        publications.length === 1 ? "" : "s"
1544      }`;
1545      empty.hidden = publications.length !== 0;
1546      error.hidden = true;
1547    } catch (publicationError) {
1548      console.error("Unable to load Charon publications:", publicationError);
1549      list.replaceChildren();
1550      list.setAttribute("aria-busy", "false");
1551      statusContainer.classList.add("is-error");
1552      status.textContent = "Publication database unavailable";
1553      empty.hidden = true;
1554      error.hidden = false;
1555    }
1556  }
1557
1558  if (document.readyState === "loading") {
1559    document.addEventListener("DOMContentLoaded", initCharonPage, { once: true });
1560  } else {
1561    initCharonPage();
1562  }
1563
1564  document.addEventListener("astro:page-load", initCharonPage);
1565</script>
1565<footer id="contact" class="border-t border-white/10 bg-black/30 mt-32"><div class="section-container py-20"><div class="grid lg:grid-cols-4 gap-14"><!-- Company --><div><div class="flex items-center gap-3 mb-6"><img src="/logos/logo.png" class="h-12" alt="Cicci Research"><div><div class="font-bold text-2xl tracking-wide">CICCI</div><div class="text-xs text-blue-400 tracking-[0.25em]">RESEARCH</div></div></div><p class="text-gray-400 leading-relaxed text-sm">Advanced scientific instrumentation for photovoltaic, optoelectronic and materials characterization.</p></div><!-- Products --><div><h3 class="text-lg font-semibold mb-6" id="products">Products</h3><div class="flex flex-col gap-4 text-gray-400 text-sm"><a href="https://docs.cicciresearch.com/arkeo-multichannel-manual/" class="hover:text-blue-400 transition">ARKEO Platform</a><a href="https://docs.cicciresearch.com/products/light-sources/" class="hover:text-blue-400 transition">Light Sources</a><a href="https://docs.cicciresearch.com/products/chambers/" class="hover:text-blue-400 transition">Environmental Chambers</a><a href="https://docs.cicciresearch.com/arkeo-manual/software/overview/" class="hover:text-blue-400 transition">Software</a></div></div><!-- Resources --><div><h3 class="text-lg font-semibold mb-6" id="resources">Resources</h3><div class="flex flex-col gap-4 text-gray-400 text-sm"><a href="https://www.cicciresearch.com/documentation/" class="hover:text-blue-400 transition">Documentation</a><a href="https://www.cicciresearch.com/publications/" class="hover:text-blue-400 transition">Publications</a><a href="#" class="hover:text-blue-400 transition">Application Notes</a><a href="/brand-kit/" class="hover:text-blue-400 transition">Downloads</a></div></div><!-- Contact --><div><h3 class="text-lg font-semibold mb-6" id="contact-2">Contact</h3><div class="flex flex-col gap-4 text-gray-400 text-sm"><div>Cicci Research s.r.l.</div><a href="https://maps.app.goo.gl/kVWVQnnuE81SsR6h9" target="_blank" rel="noopener noreferrer" class="hover:text-blue-400 transition">Via Giordania 227, Grosseto, Italy</a><a href="/cdn-cgi/l/email-protection#51383f373e11323832323823342234302332397f3825" class="hover:text-blue-400 transition"><span class="__cf_email__" data-cfemail="afc6c1c9c0efccc6ccccc6ddcadccaceddccc781c6db">[email&#160;protected]</span></a><a href="https://wa.me/393429038762" class="hover:text-blue-400 transition">+39 342 903 8762</a><a href="https://www.cicciresearch.com/" class="hover:text-blue-400 transition">www.cicciresearch.com</a></div></div></div><!-- Bottom --><div class="border-t border-white/10 mt-16 pt-8 flex flex-col lg:flex-row justify-between gap-5 text-sm text-gray-500"><div>© 2026 Cicci Research s.r.l. All rights reserved.</div><a href="/privacyCookies">Privacy & Cookies</a><div class="flex gap-6"><a href="https://www.linkedin.com/company/cicci-research-srl" target="_blank" rel="noopener noreferrer" class="hover:text-blue-400 transition">LinkedIn</a><a href="https://github.com/cicciresearch" target="_blank" rel="noopener noreferrer" class="hover:text-blue-400 transition">GitHub</a><a href="https://www.youtube.com/@cicciresearch8847" target="_blank" rel="noopener noreferrer" class="hover:text-blue-400 transition">YouTube</a></div></div></div></footer><!-- Cloudflare Pages Analytics -->
vendor: 108 bytes, line 1565
1565<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script>
vendor: 99 bytes, line 1565
1565<script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "
15653e98fbbb6cc04595a339effc7746daf7
vendor: 13 bytes, line 1565
1565"}'></script>
1565<!-- Cloudflare Pages Analytics -->
1565<script>(function(){function c(){var b=a.contentDocument||(a.contentWindow&&a.contentWindow.document);if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'a472e248ec1a9d52',t:'MTc5MTQzODc5Mw=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script>
1565</body></html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.