{"id":12187,"date":"2026-05-07T12:19:21","date_gmt":"2026-05-07T10:19:21","guid":{"rendered":"https:\/\/spgoo.org\/?page_id=12187"},"modified":"2026-05-07T12:19:40","modified_gmt":"2026-05-07T10:19:40","slug":"editeur-de-fct-utility","status":"publish","type":"page","link":"https:\/\/spgoo.org\/?page_id=12187","title":{"rendered":"Editeur de fct Utility"},"content":{"rendered":"<!-- MathJax -->\r\n<script>\r\nwindow.MathJax = {\r\n  tex: { inlineMath: [['$','$'], ['\\\\(','\\\\)']], displayMath: [['$$','$$']] },\r\n  startup: { typeset: false }\r\n};\r\n<\/script>\r\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/mathjax@3\/es5\/tex-chtml.js\" async><\/script>\r\n\r\n<!-- Fonts -->\r\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=IBM+Plex+Mono:ital,wght@0,300;0,400;0,600;1,400&#038;family=IBM+Plex+Sans:wght@300;400;500;600&#038;display=swap\" rel=\"stylesheet\">\r\n\r\n<style>\r\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n   DESIGN SYSTEM \u2014 Scientific \/ Monospace Laboratory UI\r\n   \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\r\n:root {\r\n  --bg:        #0e0f14;\r\n  --bg1:       #13151c;\r\n  --bg2:       #1a1d27;\r\n  --bg3:       #22263a;\r\n  --border:    #2a2f45;\r\n  --border2:   #353c58;\r\n  --text:      #d6daf0;\r\n  --muted:     #6b7399;\r\n  --accent:    #5b8af5;\r\n  --accent2:   #3a6be0;\r\n  --green:     #4ecb8d;\r\n  --red:       #e05c6c;\r\n  --amber:     #f0a050;\r\n  --purple:    #9b6cf5;\r\n  --font-mono: 'IBM Plex Mono', monospace;\r\n  --font-sans: 'IBM Plex Sans', sans-serif;\r\n  --r:         6px;\r\n  --r2:        10px;\r\n  --shadow:    0 4px 24px rgba(0,0,0,.45);\r\n}\r\n\r\n* { box-sizing: border-box; margin: 0; padding: 0; }\r\nbody {\r\n  background: var(--bg);\r\n  color: var(--text);\r\n  font-family: var(--font-sans);\r\n  min-height: 100vh;\r\n  font-size: 14px;\r\n  line-height: 1.55;\r\n}\r\n\r\n\/* \u2500\u2500 SCROLLBAR \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n::-webkit-scrollbar { width: 6px; height: 6px; }\r\n::-webkit-scrollbar-track { background: var(--bg1); }\r\n::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }\r\n\r\n\/* \u2500\u2500 LAYOUT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n.shell { display: grid; grid-template-columns: 260px 1fr; min-height: 100vh; }\r\n\r\n\/* \u2500\u2500 SIDEBAR \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n#sidebar {\r\n  background: var(--bg1);\r\n  border-right: 1px solid var(--border);\r\n  display: flex; flex-direction: column;\r\n  padding: 0;\r\n  position: sticky; top: 0; height: 100vh; overflow-y: auto;\r\n}\r\n.sidebar-head {\r\n  padding: 22px 20px 16px;\r\n  border-bottom: 1px solid var(--border);\r\n}\r\n.sidebar-head h1 {\r\n  font-family: var(--font-mono);\r\n  font-size: 13px; font-weight: 600;\r\n  color: var(--accent); letter-spacing: .08em;\r\n  text-transform: uppercase;\r\n}\r\n.sidebar-head p { font-size: 11px; color: var(--muted); margin-top: 3px; }\r\n\r\n.sidebar-section { padding: 12px 12px 6px; }\r\n.sidebar-section-lbl {\r\n  font-family: var(--font-mono);\r\n  font-size: 9px; letter-spacing: .15em; text-transform: uppercase;\r\n  color: var(--muted); padding: 0 8px; margin-bottom: 6px;\r\n}\r\n\r\n.class-item {\r\n  display: flex; align-items: center; justify-content: space-between;\r\n  padding: 9px 10px 9px 12px;\r\n  border-radius: var(--r); cursor: pointer; margin-bottom: 2px;\r\n  border: 1px solid transparent;\r\n  transition: all .18s;\r\n  gap: 8px;\r\n}\r\n.class-item:hover { background: var(--bg3); border-color: var(--border); }\r\n.class-item.active {\r\n  background: rgba(91,138,245,.12);\r\n  border-color: rgba(91,138,245,.3);\r\n}\r\n.class-item-id {\r\n  font-family: var(--font-mono); font-size: 12px; font-weight: 600;\r\n  color: var(--accent); flex-shrink: 0;\r\n}\r\n.class-item-lbl { font-size: 11px; color: var(--muted); flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }\r\n.class-item-badge {\r\n  font-family: var(--font-mono); font-size: 9px;\r\n  background: var(--bg3); border: 1px solid var(--border);\r\n  color: var(--muted); padding: 2px 6px; border-radius: 10px; flex-shrink: 0;\r\n}\r\n.class-item-del {\r\n  background: none; border: none; cursor: pointer;\r\n  color: var(--muted); font-size: 14px; line-height: 1;\r\n  padding: 2px 4px; border-radius: 4px;\r\n  transition: color .15s, background .15s;\r\n  flex-shrink: 0;\r\n}\r\n.class-item-del:hover { color: var(--red); background: rgba(224,92,108,.1); }\r\n\r\n.btn-add-class {\r\n  display: flex; align-items: center; gap: 8px;\r\n  margin: 8px 12px 16px;\r\n  padding: 9px 14px;\r\n  background: none; border: 1px dashed var(--border2);\r\n  color: var(--muted); border-radius: var(--r);\r\n  cursor: pointer; font-family: var(--font-mono);\r\n  font-size: 11px; transition: all .18s; width: calc(100% - 24px);\r\n  justify-content: center; letter-spacing: .04em;\r\n}\r\n.btn-add-class:hover { border-color: var(--accent); color: var(--accent); background: rgba(91,138,245,.06); }\r\n\r\n\/* \u2500\u2500 MAIN \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n#main {\r\n  display: flex; flex-direction: column;\r\n  background: var(--bg);\r\n  overflow: hidden;\r\n}\r\n\r\n\/* \u2500\u2500 TOPBAR \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n.topbar {\r\n  display: flex; align-items: center; justify-content: space-between;\r\n  padding: 18px 28px;\r\n  border-bottom: 1px solid var(--border);\r\n  background: var(--bg1);\r\n  gap: 16px;\r\n}\r\n.topbar-left { display: flex; flex-direction: column; gap: 3px; }\r\n.topbar-classid {\r\n  font-family: var(--font-mono); font-size: 20px; font-weight: 600;\r\n  color: var(--accent); letter-spacing: .04em;\r\n}\r\n.topbar-label { font-size: 12px; color: var(--muted); }\r\n.topbar-actions { display: flex; gap: 8px; }\r\n\r\n\/* \u2500\u2500 BUTTONS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n.btn {\r\n  display: inline-flex; align-items: center; gap: 6px;\r\n  padding: 8px 16px; border-radius: var(--r);\r\n  font-family: var(--font-mono); font-size: 12px; font-weight: 400;\r\n  cursor: pointer; border: 1px solid; transition: all .18s;\r\n  letter-spacing: .03em; white-space: nowrap;\r\n}\r\n.btn-primary {\r\n  background: var(--accent); border-color: var(--accent2); color: #fff;\r\n}\r\n.btn-primary:hover { background: var(--accent2); }\r\n.btn-ghost {\r\n  background: transparent; border-color: var(--border2); color: var(--text);\r\n}\r\n.btn-ghost:hover { background: var(--bg3); border-color: var(--accent); color: var(--accent); }\r\n.btn-danger {\r\n  background: transparent; border-color: var(--border2); color: var(--muted);\r\n}\r\n.btn-danger:hover { background: rgba(224,92,108,.1); border-color: var(--red); color: var(--red); }\r\n.btn-sm { padding: 5px 10px; font-size: 11px; }\r\n.btn-icon { padding: 6px 8px; }\r\n\r\n\/* \u2500\u2500 TABLE AREA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n.table-wrap { flex: 1; overflow: auto; padding: 24px 28px; }\r\n\r\n\/* \u2500\u2500 FUNCTIONS TABLE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n.fn-table {\r\n  width: 100%; border-collapse: collapse;\r\n  font-size: 12.5px;\r\n}\r\n.fn-table thead tr {\r\n  border-bottom: 2px solid var(--border);\r\n}\r\n.fn-table thead th {\r\n  font-family: var(--font-mono);\r\n  font-size: 10px; font-weight: 600;\r\n  text-transform: uppercase; letter-spacing: .1em;\r\n  color: var(--muted); padding: 8px 12px;\r\n  text-align: left; white-space: nowrap;\r\n  background: var(--bg1);\r\n}\r\n.fn-table thead th:first-child { border-radius: var(--r) 0 0 0; }\r\n.fn-table thead th:last-child  { border-radius: 0 var(--r) 0 0; }\r\n\r\n.fn-table tbody tr {\r\n  border-bottom: 1px solid var(--border);\r\n  transition: background .12s;\r\n}\r\n.fn-table tbody tr:hover { background: var(--bg1); }\r\n.fn-table tbody tr:last-child { border-bottom: none; }\r\n\r\n.fn-table td {\r\n  padding: 12px 12px;\r\n  vertical-align: middle;\r\n}\r\n\r\n.fn-name {\r\n  font-family: var(--font-mono); font-weight: 600;\r\n  color: var(--text); min-width: 100px;\r\n}\r\n.fn-formula { min-width: 150px; }\r\n.fn-domain {\r\n  font-family: var(--font-mono); font-size: 11px;\r\n  color: var(--muted); white-space: nowrap;\r\n}\r\n.fn-param {\r\n  font-family: var(--font-mono); font-size: 11px; color: var(--purple);\r\n}\r\n.fn-actions { display: flex; gap: 6px; justify-content: flex-end; white-space: nowrap; }\r\n\r\n.code-snippet {\r\n  font-family: var(--font-mono); font-size: 10.5px;\r\n  background: var(--bg2); border: 1px solid var(--border);\r\n  border-radius: 4px; padding: 2px 6px; color: var(--green);\r\n  max-width: 200px; overflow: hidden; text-overflow: ellipsis;\r\n  white-space: nowrap; display: block;\r\n  cursor: default; transition: background .15s;\r\n}\r\n.code-snippet:hover { background: var(--bg3); white-space: normal; max-width: 350px; }\r\n\r\n\/* Empty state *\/\r\n.empty-state {\r\n  text-align: center; padding: 60px 20px;\r\n  color: var(--muted); font-family: var(--font-mono); font-size: 13px;\r\n}\r\n.empty-state svg { opacity: .25; margin-bottom: 16px; }\r\n\r\n\/* \u2500\u2500 MODAL OVERLAY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n.overlay {\r\n  position: fixed; inset: 0; z-index: 100;\r\n  background: rgba(8,9,14,.75); backdrop-filter: blur(4px);\r\n  display: flex; align-items: flex-start; justify-content: center;\r\n  padding: 32px 16px; overflow-y: auto;\r\n  opacity: 0; pointer-events: none; transition: opacity .2s;\r\n}\r\n.overlay.open { opacity: 1; pointer-events: auto; }\r\n\r\n.modalys {\r\n  background: var(--bg1);\r\n  border: 1px solid var(--border2);\r\n  border-radius: var(--r2);\r\n  width: 100%; max-width: 820px;\r\n  box-shadow: var(--shadow);\r\n  transform: translateY(-12px); transition: transform .22s ease;\r\n}\r\n.overlay.open .modalys { transform: translateY(0); }\r\n\r\n.modalys-header {\r\n  display: flex; align-items: center; justify-content: space-between;\r\n  padding: 20px 24px 16px;\r\n  border-bottom: 1px solid var(--border);\r\n}\r\n.modalys-header h2 {\r\n  font-family: var(--font-mono); font-size: 14px; font-weight: 600;\r\n  color: var(--accent); letter-spacing: .05em;\r\n}\r\n.modalys-close {\r\n  background: none; border: none; cursor: pointer;\r\n  color: var(--muted); font-size: 20px; line-height: 1;\r\n  padding: 2px 6px; border-radius: var(--r);\r\n  transition: color .15s, background .15s;\r\n}\r\n.modalys-close:hover { color: var(--text); background: var(--bg3); }\r\n\r\n.modalys-body { padding: 20px 24px; display: flex; flex-direction: column; gap: 20px; }\r\n\r\n\/* \u2500\u2500 FORM \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n.form-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }\r\n.form-grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; }\r\n.form-group { display: flex; flex-direction: column; gap: 5px; }\r\n.form-group.full { grid-column: 1 \/ -1; }\r\n\r\nlabel {\r\n  font-family: var(--font-mono); font-size: 10px; font-weight: 600;\r\n  text-transform: uppercase; letter-spacing: .1em; color: var(--muted);\r\n}\r\nlabel .req { color: var(--red); margin-left: 2px; }\r\n\r\ninput[type=text], input[type=number], textarea, select {\r\n  background: var(--bg2); border: 1px solid var(--border2);\r\n  color: var(--text); border-radius: var(--r);\r\n  padding: 8px 11px; font-family: var(--font-mono); font-size: 12px;\r\n  transition: border-color .15s, box-shadow .15s;\r\n  outline: none; width: 100%;\r\n}\r\ninput:focus, textarea:focus, select:focus {\r\n  border-color: var(--accent); box-shadow: 0 0 0 2px rgba(91,138,245,.18);\r\n}\r\ntextarea { resize: vertical; min-height: 64px; line-height: 1.5; }\r\ntextarea.code { min-height: 52px; color: var(--green); }\r\n\r\n.field-hint { font-size: 10px; color: var(--muted); margin-top: 2px; font-style: italic; }\r\n\r\n\/* \u2500\u2500 SECTION DIVIDER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n.section-divider {\r\n  display: flex; align-items: center; gap: 12px;\r\n  margin: 4px 0;\r\n}\r\n.section-divider span {\r\n  font-family: var(--font-mono); font-size: 10px; font-weight: 600;\r\n  text-transform: uppercase; letter-spacing: .12em;\r\n  color: var(--muted); white-space: nowrap;\r\n}\r\n.section-divider::before, .section-divider::after {\r\n  content: ''; flex: 1; height: 1px; background: var(--border);\r\n}\r\n\r\n\/* \u2500\u2500 PARAM BUILDER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n.param-builder { display: flex; flex-direction: column; gap: 8px; }\r\n.param-row {\r\n  display: grid; grid-template-columns: 1fr 1fr 1fr 1fr 32px;\r\n  gap: 8px; align-items: center;\r\n}\r\n.param-row-lbl {\r\n  font-family: var(--font-mono); font-size: 10px; color: var(--muted);\r\n  grid-column: 1 \/ -1; display: grid;\r\n  grid-template-columns: 1fr 1fr 1fr 1fr 32px;\r\n  gap: 8px;\r\n}\r\n.param-row-lbl span { font-size: 9px; text-transform: uppercase; letter-spacing: .1em; }\r\n.btn-del-param {\r\n  background: none; border: 1px solid var(--border);\r\n  color: var(--muted); border-radius: var(--r);\r\n  cursor: pointer; font-size: 13px; height: 32px; width: 32px;\r\n  display: flex; align-items: center; justify-content: center;\r\n  transition: all .15s;\r\n}\r\n.btn-del-param:hover { border-color: var(--red); color: var(--red); background: rgba(224,92,108,.08); }\r\n.btn-add-param {\r\n  display: flex; align-items: center; gap: 6px;\r\n  background: none; border: 1px dashed var(--border2); color: var(--muted);\r\n  border-radius: var(--r); padding: 7px 12px; cursor: pointer;\r\n  font-family: var(--font-mono); font-size: 11px; transition: all .15s;\r\n  align-self: flex-start;\r\n}\r\n.btn-add-param:hover { border-color: var(--green); color: var(--green); background: rgba(78,203,141,.06); }\r\n\r\n\/* \u2500\u2500 MODAL FOOTER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n.modalys-footer {\r\n  display: flex; justify-content: flex-end; gap: 10px;\r\n  padding: 16px 24px;\r\n  border-top: 1px solid var(--border);\r\n}\r\n\r\n\/* \u2500\u2500 TOAST \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n#toast-stack {\r\n  position: fixed; bottom: 24px; right: 24px; z-index: 200;\r\n  display: flex; flex-direction: column-reverse; gap: 8px;\r\n}\r\n.toast {\r\n  font-family: var(--font-mono); font-size: 12px;\r\n  padding: 11px 18px; border-radius: var(--r);\r\n  background: var(--bg2); border: 1px solid var(--border2);\r\n  color: var(--text); box-shadow: var(--shadow);\r\n  display: flex; align-items: center; gap: 10px;\r\n  animation: toast-in .25s ease both;\r\n  max-width: 340px;\r\n}\r\n.toast.ok  { border-color: var(--green); }\r\n.toast.err { border-color: var(--red); }\r\n.toast-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }\r\n.toast.ok  .toast-dot { background: var(--green); }\r\n.toast.err .toast-dot { background: var(--red); }\r\n@keyframes toast-in { from { opacity:0; transform:translateX(16px); } to { opacity:1; transform:none; } }\r\n\r\n\/* \u2500\u2500 CONFIRM DIALOG \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n.confirm-modal {\r\n  background: var(--bg1); border: 1px solid var(--border2);\r\n  border-radius: var(--r2); max-width: 400px; width: 100%;\r\n  padding: 28px; box-shadow: var(--shadow); text-align: center;\r\n}\r\n.confirm-modal h3 {\r\n  font-family: var(--font-mono); font-size: 14px;\r\n  color: var(--text); margin-bottom: 10px;\r\n}\r\n.confirm-modal p { font-size: 13px; color: var(--muted); margin-bottom: 24px; }\r\n.confirm-modal .actions { display: flex; gap: 10px; justify-content: center; }\r\n\r\n\/* \u2500\u2500 PLACEHOLDER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n.no-class-selected {\r\n  display: flex; flex-direction: column; align-items: center;\r\n  justify-content: center; flex: 1; gap: 16px; color: var(--muted);\r\n  font-family: var(--font-mono);\r\n}\r\n.no-class-selected .big { font-size: 48px; opacity: .15; }\r\n.no-class-selected p { font-size: 13px; }\r\n\r\n\/* \u2500\u2500 LOADING \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n.loader {\r\n  display: flex; align-items: center; justify-content: center;\r\n  height: 120px; gap: 8px; color: var(--muted);\r\n  font-family: var(--font-mono); font-size: 12px;\r\n}\r\n.spinner {\r\n  width: 18px; height: 18px; border: 2px solid var(--border2);\r\n  border-top-color: var(--accent); border-radius: 50%;\r\n  animation: spin .7s linear infinite;\r\n}\r\n@keyframes spin { to { transform: rotate(360deg); } }\r\n\r\n\/* \u2500\u2500 TAGS display \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n.tag {\r\n  display: inline-block; font-family: var(--font-mono); font-size: 10px;\r\n  background: var(--bg3); border: 1px solid var(--border);\r\n  color: var(--muted); padding: 1px 6px; border-radius: 4px; margin: 1px;\r\n}\r\n\r\n\/* MathJax override *\/\r\nmjx-container { font-size: inherit !important; }\r\n<\/style>\r\n\r\n\r\n<div class=\"shell\">\r\n  <!-- \u2500\u2500 SIDEBAR \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n  <aside id=\"sidebar\">\r\n    <div class=\"sidebar-head\">\r\n      <h1>U_f Manager<\/h1>\r\n      <p>Fonctions d&#8217;utilit\u00e9 \u2014 <span class='tooltipsall tooltipsincontent classtoolTips3'>MongoDB<\/span><\/p>\r\n    <\/div>\r\n\r\n    <div class=\"sidebar-section\">\r\n      <div class=\"sidebar-section-lbl\">Classes<\/div>\r\n      <div id=\"class-list\">\r\n        <div class=\"loader\"><div class=\"spinner\"><\/div> chargement\u2026<\/div>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <button class=\"btn-add-class\" onclick=\"openClassModal()\">\r\n      + Nouvelle classe\r\n    <\/button>\r\n  <\/aside>\r\n\r\n  <!-- \u2500\u2500 MAIN \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n  <div id=\"main\">\r\n    <div id=\"content\">\r\n      <div class=\"no-class-selected\">\r\n        <div class=\"big\">\u222b<\/div>\r\n        <p>S\u00e9lectionnez une classe dans la barre lat\u00e9rale<\/p>\r\n      <\/div>\r\n    <\/div>\r\n  <\/div>\r\n<\/div>\r\n\r\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n     MODAL : Nouvelle \/ Modifier une classe\r\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n<div class=\"overlay\" id=\"modal-class\">\r\n  <div class=\"modalys\">\r\n    <div class=\"modalys-header\">\r\n      <h2 id=\"modal-class-title\">Nouvelle classe<\/h2>\r\n      <button class=\"modalys-close\" onclick=\"closeModal('modal-class')\">\u00d7<\/button>\r\n    <\/div>\r\n    <div class=\"modalys-body\">\r\n      <div class=\"form-grid-2\">\r\n        <div class=\"form-group\">\r\n          <label>Identifiant <span class=\"req\">*<\/span><\/label>\r\n          <input type=\"text\" id=\"cls-id\" placeholder=\"ex: U_f2\" \/>\r\n          <span class=\"field-hint\">Format recommand\u00e9 : U_f2, U_f3, \u2026<\/span>\r\n        <\/div>\r\n        <div class=\"form-group\">\r\n          <label>Libell\u00e9<\/label>\r\n          <input type=\"text\" id=\"cls-label\" placeholder=\"ex: Fonctions \u00e0 utilit\u00e9 born\u00e9e\" \/>\r\n        <\/div>\r\n      <\/div>\r\n    <\/div>\r\n    <div class=\"modalys-footer\">\r\n      <button class=\"btn btn-ghost\" onclick=\"closeModal('modal-class')\">Annuler<\/button>\r\n      <button class=\"btn btn-primary\" onclick=\"saveClass()\">Enregistrer<\/button>\r\n    <\/div>\r\n  <\/div>\r\n<\/div>\r\n\r\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n     MODAL : Ajouter \/ Modifier une fonction\r\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n<div class=\"overlay\" id=\"modal-fn\">\r\n  <div class=\"modalys\">\r\n    <div class=\"modalys-header\">\r\n      <h2 id=\"modal-fn-title\">Nouvelle fonction d&#8217;utilit\u00e9<\/h2>\r\n      <button class=\"modalys-close\" onclick=\"closeModal('modal-fn')\">\u00d7<\/button>\r\n    <\/div>\r\n    <div class=\"modalys-body\">\r\n\r\n      <!-- Identit\u00e9 -->\r\n      <div class=\"section-divider\"><span>Identit\u00e9<\/span><\/div>\r\n      <div class=\"form-grid-2\">\r\n        <div class=\"form-group\">\r\n          <label>Nom <span class=\"req\">*<\/span><\/label>\r\n          <input type=\"text\" id=\"fn-nom\" placeholder=\"CARA ou $\\text{CRRA}$\" \/>\r\n          <span class=\"field-hint\">Supporte le $ \u2026 $ MathJax<\/span>\r\n        <\/div>\r\n        <div class=\"form-group\">\r\n          <label>Formule <span class=\"req\">*<\/span><\/label>\r\n          <input type=\"text\" id=\"fn-formule\" placeholder=\"$U(x) = 1 - e^{-ax}$\" \/>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <!-- Domaine -->\r\n      <div class=\"section-divider\"><span>Domaine de d\u00e9finition<\/span><\/div>\r\n      <div class=\"form-grid-3\">\r\n        <div class=\"form-group\">\r\n          <label>Borne inf\u00e9rieure <span class=\"req\">*<\/span><\/label>\r\n          <input type=\"number\" id=\"fn-dom-inf\" step=\"any\" placeholder=\"0\" \/>\r\n        <\/div>\r\n        <div class=\"form-group\">\r\n          <label>Borne sup\u00e9rieure <span class=\"req\">*<\/span><\/label>\r\n          <input type=\"number\" id=\"fn-dom-sup\" step=\"any\" placeholder=\"6\" \/>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <!-- Param\u00e8tres -->\r\n      <div class=\"section-divider\"><span>Param\u00e8tres<\/span><\/div>\r\n      <div class=\"param-builder\" id=\"param-builder\">\r\n        <div class=\"param-row-lbl\">\r\n          <span>Identifiant JS<\/span>\r\n          <span>Label MathJax<\/span>\r\n          <span>Borne min<\/span>\r\n          <span>Borne max<\/span>\r\n          <span><\/span>\r\n        <\/div>\r\n        <!-- rows injected by JS -->\r\n      <\/div>\r\n      <button class=\"btn-add-param\" onclick=\"addParamRow()\">+ param\u00e8tre<\/button>\r\n\r\n      <!-- Contraintes -->\r\n      <div class=\"section-divider\"><span>Contraintes<\/span><\/div>\r\n      <div class=\"form-grid-2\">\r\n        <div class=\"form-group\">\r\n          <label>Contraintes JS (une par ligne)<\/label>\r\n          <textarea id=\"fn-cont\" placeholder=\"a > 0\r\nb != 0&#8243;><\/textarea>\r\n          <span class=\"field-hint\">ex: a > 0<\/span>\r\n        <\/div>\r\n        <div class=\"form-group\">\r\n          <label>Contraintes MathJax (une par ligne)<\/label>\r\n          <textarea id=\"fn-cont-l\" placeholder=\"a > 0\r\n\\beta \\neq 0&#8243;><\/textarea>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <!-- U(x) -->\r\n      <div class=\"section-divider\"><span>Fonction U(x)<\/span><\/div>\r\n      <div class=\"form-group\">\r\n        <label>Code JS \u2014 U_f(x) <span class=\"req\">*<\/span><\/label>\r\n        <textarea class=\"code\" id=\"fn-uf\" placeholder=\"return 1 - Math.exp(-a * x);\"><\/textarea>\r\n        <span class=\"field-hint\">Variables disponibles : x + les param\u00e8tres d\u00e9finis ci-dessus<\/span>\r\n      <\/div>\r\n\r\n      <!-- Mesures de risque -->\r\n      <div class=\"section-divider\"><span>Mesures de risque \u2014 Labels MathJax<\/span><\/div>\r\n      <div class=\"form-grid-2\">\r\n        <div class=\"form-group\">\r\n          <label>AA \u2014 Aversion absolue (Label)<\/label>\r\n          <input type=\"text\" id=\"fn-aa-l\" placeholder=\"$a$\" \/>\r\n        <\/div>\r\n        <div class=\"form-group\">\r\n          <label>AR \u2014 Aversion relative (Label)<\/label>\r\n          <input type=\"text\" id=\"fn-ar-l\" placeholder=\"$ax$\" \/>\r\n        <\/div>\r\n        <div class=\"form-group\">\r\n          <label>P \u2014 Prime (Label)<\/label>\r\n          <input type=\"text\" id=\"fn-p-l\" placeholder=\"$a$\" \/>\r\n        <\/div>\r\n        <div class=\"form-group\">\r\n          <label>T \u2014 Tol\u00e9rance (Label)<\/label>\r\n          <input type=\"text\" id=\"fn-t-l\" placeholder=\"$\\frac{1}{a}$\" \/>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <!-- Mesures de risque \u2014 code -->\r\n      <div class=\"section-divider\"><span>Mesures de risque \u2014 Code JS<\/span><\/div>\r\n      <div class=\"form-grid-2\">\r\n        <div class=\"form-group\">\r\n          <label>A_f \u2014 Aversion absolue<\/label>\r\n          <textarea class=\"code\" id=\"fn-a-f\" placeholder=\"return a;\"><\/textarea>\r\n        <\/div>\r\n        <div class=\"form-group\">\r\n          <label>R_f \u2014 Aversion relative<\/label>\r\n          <textarea class=\"code\" id=\"fn-r-f\" placeholder=\"return a * x;\"><\/textarea>\r\n        <\/div>\r\n        <div class=\"form-group\">\r\n          <label>P_f \u2014 Prime<\/label>\r\n          <textarea class=\"code\" id=\"fn-p-f\" placeholder=\"return a;\"><\/textarea>\r\n        <\/div>\r\n        <div class=\"form-group\">\r\n          <label>T_f \u2014 Tol\u00e9rance<\/label>\r\n          <textarea class=\"code\" id=\"fn-t-f\" placeholder=\"return 1 \/ a;\"><\/textarea>\r\n        <\/div>\r\n      <\/div>\r\n\r\n    <\/div><!-- \/modal-body -->\r\n    <div class=\"modal-footer\">\r\n      <button class=\"btn btn-ghost\" onclick=\"closeModal('modal-fn')\">Annuler<\/button>\r\n      <button class=\"btn btn-primary\" onclick=\"saveFn()\">Enregistrer<\/button>\r\n    <\/div>\r\n  <\/div>\r\n<\/div>\r\n\r\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n     MODAL : Confirmation suppression\r\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n<div class=\"overlay\" id=\"modal-confirm\">\r\n  <div class=\"confirm-modal\">\r\n    <h3 id=\"confirm-title\">Confirmer la suppression<\/h3>\r\n    <p id=\"confirm-msg\">Cette action est irr\u00e9versible.<\/p>\r\n    <div class=\"actions\">\r\n      <button class=\"btn btn-ghost\" onclick=\"closeModal('modal-confirm')\">Annuler<\/button>\r\n      <button class=\"btn btn-danger\" id=\"confirm-ok\">Supprimer<\/button>\r\n    <\/div>\r\n  <\/div>\r\n<\/div>\r\n\r\n<!-- Toast stack -->\r\n<div id=\"toast-stack\"><\/div>\r\n\r\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n     JAVASCRIPT\r\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n<script>\r\n'use strict';\r\n\r\n\/* \u2500\u2500 CONFIG \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nconst API = 'http:\/\/localhost:3001\/api';\r\n\r\n\/* \u2500\u2500 STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nlet classes      = [];          \/\/ [{classId, label, functions:[\u2026]}]\r\nlet activeClass  = null;        \/\/ classId string\r\nlet editingFnId  = null;        \/\/ _id of fn being edited (null = new)\r\nlet editingClsId = null;        \/\/ classId being edited (null = new)\r\n\r\n\/* \u2500\u2500 INIT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\ndocument.addEventListener('DOMContentLoaded', () => {\r\n  \/\/loadClasses();\r\n\r\n  \/\/ Close overlays on backdrop click\r\n  document.querySelectorAll('.overlay').forEach(el => {\r\n    el.addEventListener('click', e => { if (e.target === el) closeModal(el.id); });\r\n  });\r\n});\r\n\r\n\/* \u2500\u2500 API HELPERS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nasync function api(method, path, body) {\r\n  const opts = {\r\n    method,\r\n    headers: { 'Content-Type': 'application\/json' },\r\n  };\r\n  if (body !== undefined) opts.body = JSON.stringify(body);\r\n  const res = await fetch(API + path, opts);\r\n  const data = await res.json();\r\n  if (!res.ok) throw new Error(data.error || res.statusText);\r\n  return data;\r\n}\r\n\r\n\/* \u2500\u2500 LOAD CLASSES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nasync function loadClasses() {\r\n  try {\r\n    classes = await api('GET', '\/classes');\r\n    renderSidebar();\r\n    if (activeClass) renderMain(activeClass);\r\n  } catch (e) {\r\n    toast('Erreur chargement : ' + e.message, 'err');\r\n    document.getElementById('class-list').innerHTML =\r\n      '<div style=\"padding:12px;color:var(--red);font-size:11px;font-family:var(--font-mono)\">Connexion API \u00e9chou\u00e9e.<br>V\u00e9rifiez que le backend tourne.<\/div>';\r\n  }\r\n}\r\n\r\n\/* \u2500\u2500 SIDEBAR RENDER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nfunction renderSidebar() {\r\n  const el = document.getElementById('class-list');\r\n  if (!classes.length) {\r\n    el.innerHTML = '<div style=\"padding:10px 12px;color:var(--muted);font-size:11px;font-family:var(--font-mono)\">Aucune classe<\/div>';\r\n    return;\r\n  }\r\n  el.innerHTML = classes.map(c => `\r\n    <div class=\"class-item ${c.classId === activeClass ? 'active' : ''}\"\r\n         onclick=\"selectClass('${c.classId}')\">\r\n      <span class=\"class-item-id\">${c.classId}<\/span>\r\n      <span class=\"class-item-lbl\">${c.label || '\u2014'}<\/span>\r\n      <span class=\"class-item-badge\">${c.functions.length}<\/span>\r\n      <button class=\"class-item-del\" title=\"Supprimer la classe\"\r\n        onclick=\"event.stopPropagation();confirmDeleteClass('${c.classId}')\">\u00d7<\/button>\r\n    <\/div>\r\n  `).join('');\r\n}\r\n\r\n\/* \u2500\u2500 SELECT CLASS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nfunction selectClass(classId) {\r\n  activeClass = classId;\r\n  renderSidebar();\r\n  renderMain(classId);\r\n}\r\n\r\n\/* \u2500\u2500 MAIN RENDER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nfunction renderMain(classId) {\r\n  const cls = classes.find(c => c.classId === classId);\r\n  if (!cls) return;\r\n\r\n  document.getElementById('content').innerHTML = `\r\n    <div class=\"topbar\">\r\n      <div class=\"topbar-left\">\r\n        <div class=\"topbar-classid\">${cls.classId}<\/div>\r\n        <div class=\"topbar-label\">${cls.label || 'Sans libell\u00e9'} \u2014 ${cls.functions.length} fonction(s)<\/div>\r\n      <\/div>\r\n      <div class=\"topbar-actions\">\r\n        <button class=\"btn btn-ghost btn-sm\" onclick=\"openClassModal('${cls.classId}')\">\u270e Modifier<\/button>\r\n        <button class=\"btn btn-primary btn-sm\" onclick=\"openFnModal(null)\">+ Ajouter une fonction<\/button>\r\n      <\/div>\r\n    <\/div>\r\n    <div class=\"table-wrap\">\r\n      ${cls.functions.length === 0 ? renderEmpty() : renderTable(cls.functions)}\r\n    <\/div>\r\n  `;\r\n\r\n  \/\/ Typeset MathJax after render\r\n  if (window.MathJax) MathJax.typesetPromise([document.getElementById('content')]);\r\n}\r\n\r\nfunction renderEmpty() {\r\n  return `<div class=\"empty-state\">\r\n    <svg width=\"56\" height=\"56\" fill=\"none\" viewBox=\"0 0 56 56\">\r\n      <rect x=\"8\" y=\"8\" width=\"40\" height=\"40\" rx=\"6\" stroke=\"currentColor\" stroke-width=\"2\"\/>\r\n      <path d=\"M20 28h16M28 20v16\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\/>\r\n    <\/svg>\r\n    <p>Aucune fonction d'utilit\u00e9 dans cette classe.<br>Cliquez sur \u00ab + Ajouter une fonction \u00bb.<\/p>\r\n  <\/div>`;\r\n}\r\n\r\nfunction renderTable(fns) {\r\n  return `\r\n  <table class=\"fn-table\">\r\n    <thead>\r\n      <tr>\r\n        <th>Nom<\/th>\r\n        <th>Formule<\/th>\r\n        <th>Dom. d\u00e9f.<\/th>\r\n        <th>Param\u00e8tres<\/th>\r\n        <th>U_f(x)<\/th>\r\n        <th>AA_L<\/th>\r\n        <th>AR_L<\/th>\r\n        <th>P_L<\/th>\r\n        <th>T_L<\/th>\r\n        <th style=\"text-align:right\">Actions<\/th>\r\n      <\/tr>\r\n    <\/thead>\r\n    <tbody>\r\n      ${fns.map(fn => `\r\n        <tr>\r\n          <td class=\"fn-name\">${renderMath(fn.Nom)}<\/td>\r\n          <td class=\"fn-formula\">${renderMath(fn.formule)}<\/td>\r\n          <td class=\"fn-domain\">[${fn.dom_def_inf}, ${fn.dom_def_sup}]<\/td>\r\n          <td class=\"fn-param\">${renderParams(fn)}<\/td>\r\n          <td><code class=\"code-snippet\" title=\"${esc(fn.U_f)}\">${esc(fn.U_f)}<\/code><\/td>\r\n          <td>${renderMath(fn.AA_L)}<\/td>\r\n          <td>${renderMath(fn.AR_L)}<\/td>\r\n          <td>${renderMath(fn.P_L)}<\/td>\r\n          <td>${renderMath(fn.T_L)}<\/td>\r\n          <td class=\"fn-actions\">\r\n            <button class=\"btn btn-ghost btn-sm btn-icon\" title=\"Modifier\"\r\n              onclick=\"openFnModal('${fn._id}')\">\u270e<\/button>\r\n            <button class=\"btn btn-danger btn-sm btn-icon\" title=\"Supprimer\"\r\n              onclick=\"confirmDeleteFn('${fn._id}', '${esc(fn.Nom)}')\">\u2715<\/button>\r\n          <\/td>\r\n        <\/tr>\r\n      `).join('')}\r\n    <\/tbody>\r\n  <\/table>`;\r\n}\r\n\r\nfunction renderMath(str) {\r\n  if (!str) return '<span style=\"color:var(--muted)\">\u2014<\/span>';\r\n  return `<span>${esc(str)}<\/span>`;\r\n}\r\n\r\nfunction renderParams(fn) {\r\n  if (!fn.param || !fn.param.length) return '<span style=\"color:var(--muted)\">\u2014<\/span>';\r\n  return fn.param.map((p, i) => {\r\n    const lbl = fn.param_l?.[i] || p;\r\n    const dom = fn.dom_param?.[i];\r\n    const range = dom ? `\u2208 [${dom[0]}, ${dom[1]}]` : '';\r\n    return `<span class=\"tag\" title=\"${range}\">${p}<\/span>`;\r\n  }).join('');\r\n}\r\n\r\nfunction esc(s) {\r\n  return String(s || '').replace(\/&\/g,'&').replace(\/<\/g,'<').replace(\/>\/g,'>').replace(\/\"\/g,'\"');\r\n}\r\n\r\n\/* \u2500\u2500 CLASS MODAL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nfunction openClassModal(classId = null) {\r\n  editingClsId = classId;\r\n  const existing = classId ? classes.find(c => c.classId === classId) : null;\r\n\r\n  document.getElementById('modal-class-title').textContent =\r\n    classId ? `Modifier ${classId}` : 'Nouvelle classe';\r\n  document.getElementById('cls-id').value    = existing?.classId || '';\r\n  document.getElementById('cls-label').value = existing?.label   || '';\r\n  document.getElementById('cls-id').disabled = !!classId;\r\n\r\n  openModal('modal-class');\r\n}\r\n\r\nasync function saveClass() {\r\n  const classId = document.getElementById('cls-id').value.trim();\r\n  const label   = document.getElementById('cls-label').value.trim();\r\n  if (!classId) { toast('L\\'identifiant est requis', 'err'); return; }\r\n\r\n  try {\r\n    if (editingClsId) {\r\n      await api('PUT', `\/classes\/${editingClsId}`, { label });\r\n      toast(`Classe ${editingClsId} mise \u00e0 jour`);\r\n    } else {\r\n      await api('POST', '\/classes', { classId, label });\r\n      toast(`Classe ${classId} cr\u00e9\u00e9e`);\r\n      activeClass = classId;\r\n    }\r\n    closeModal('modal-class');\r\n    await loadClasses();\r\n  } catch (e) { toast(e.message, 'err'); }\r\n}\r\n\r\n\/* \u2500\u2500 FUNCTION MODAL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nfunction openFnModal(fnId) {\r\n  editingFnId = fnId;\r\n  const cls = classes.find(c => c.classId === activeClass);\r\n  const fn  = fnId ? cls?.functions.find(f => f._id === fnId) : null;\r\n\r\n  document.getElementById('modal-fn-title').textContent =\r\n    fnId ? `Modifier \u2014 ${fn?.Nom || ''}` : 'Nouvelle fonction d\\'utilit\u00e9';\r\n\r\n  \/\/ Fill fields\r\n  setVal('fn-nom',    fn?.Nom       || '');\r\n  setVal('fn-formule',fn?.formule   || '');\r\n  setVal('fn-dom-inf',fn?.dom_def_inf ?? '');\r\n  setVal('fn-dom-sup',fn?.dom_def_sup ?? '');\r\n  setVal('fn-uf',     fn?.U_f       || '');\r\n  setVal('fn-aa-l',   fn?.AA_L      || '');\r\n  setVal('fn-ar-l',   fn?.AR_L      || '');\r\n  setVal('fn-p-l',    fn?.P_L       || '');\r\n  setVal('fn-t-l',    fn?.T_L       || '');\r\n  setVal('fn-a-f',    fn?.A_f       || '');\r\n  setVal('fn-r-f',    fn?.R_f       || '');\r\n  setVal('fn-p-f',    fn?.P_f       || '');\r\n  setVal('fn-t-f',    fn?.T_f       || '');\r\n  setVal('fn-cont',   (fn?.cont  || []).join('\\n'));\r\n  setVal('fn-cont-l', (fn?.cont_L || []).join('\\n'));\r\n\r\n  \/\/ Build param rows\r\n  buildParamUI(fn);\r\n\r\n  openModal('modal-fn');\r\n}\r\n\r\nfunction setVal(id, v) { document.getElementById(id).value = v; }\r\n\r\n\/* \u2500\u2500 PARAM UI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nfunction buildParamUI(fn) {\r\n  const builder = document.getElementById('param-builder');\r\n  \/\/ Remove existing rows (keep header)\r\n  builder.querySelectorAll('.param-row').forEach(r => r.remove());\r\n\r\n  const params   = fn?.param     || [];\r\n  const paramsL  = fn?.param_l   || [];\r\n  const domParam = fn?.dom_param || [];\r\n\r\n  params.forEach((p, i) => addParamRow(p, paramsL[i] || p, domParam[i]?.[0], domParam[i]?.[1]));\r\n}\r\n\r\nfunction addParamRow(id = '', lbl = '', minV = '', maxV = '') {\r\n  const builder = document.getElementById('param-builder');\r\n  const row = document.createElement('div');\r\n  row.className = 'param-row';\r\n  row.innerHTML = `\r\n    <input type=\"text\" placeholder=\"ex: a\" value=\"${esc(id)}\"  class=\"pr-id\" \/>\r\n    <input type=\"text\" placeholder=\"ex: \\\\alpha\" value=\"${esc(lbl)}\" class=\"pr-lbl\" \/>\r\n    <input type=\"number\" placeholder=\"0.1\" step=\"any\" value=\"${minV !== '' ? minV : ''}\" class=\"pr-min\" \/>\r\n    <input type=\"number\" placeholder=\"3.0\" step=\"any\" value=\"${maxV !== '' ? maxV : ''}\" class=\"pr-max\" \/>\r\n    <button class=\"btn-del-param\" onclick=\"this.parentNode.remove()\" title=\"Supprimer\">\u00d7<\/button>\r\n  `;\r\n  builder.appendChild(row);\r\n}\r\n\r\nfunction collectParams() {\r\n  const rows = document.querySelectorAll('#param-builder .param-row');\r\n  const param = [], param_l = [], dom_param = [];\r\n  rows.forEach(r => {\r\n    const id  = r.querySelector('.pr-id').value.trim();\r\n    const lbl = r.querySelector('.pr-lbl').value.trim();\r\n    const mn  = parseFloat(r.querySelector('.pr-min').value);\r\n    const mx  = parseFloat(r.querySelector('.pr-max').value);\r\n    if (id) {\r\n      param.push(id);\r\n      param_l.push(lbl || id);\r\n      dom_param.push([isNaN(mn) ? null : mn, isNaN(mx) ? null : mx]);\r\n    }\r\n  });\r\n  return { param, param_l, dom_param };\r\n}\r\n\r\n\/* \u2500\u2500 SAVE FUNCTION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nasync function saveFn() {\r\n  const nom    = document.getElementById('fn-nom').value.trim();\r\n  const formule= document.getElementById('fn-formule').value.trim();\r\n  const inf    = parseFloat(document.getElementById('fn-dom-inf').value);\r\n  const sup    = parseFloat(document.getElementById('fn-dom-sup').value);\r\n  const uf     = document.getElementById('fn-uf').value.trim();\r\n\r\n  if (!nom || !formule || isNaN(inf) || isNaN(sup) || !uf) {\r\n    toast('Nom, formule, domaine et U_f(x) sont requis', 'err'); return;\r\n  }\r\n\r\n  const { param, param_l, dom_param } = collectParams();\r\n\r\n  const body = {\r\n    Nom: nom, formule, dom_def_inf: inf, dom_def_sup: sup,\r\n    param, param_l, dom_param,\r\n    cont:   document.getElementById('fn-cont').value.split('\\n').map(s=>s.trim()).filter(Boolean),\r\n    cont_L: document.getElementById('fn-cont-l').value.split('\\n').map(s=>s.trim()).filter(Boolean),\r\n    U_f:   uf,\r\n    AA_L:  document.getElementById('fn-aa-l').value.trim(),\r\n    AR_L:  document.getElementById('fn-ar-l').value.trim(),\r\n    P_L:   document.getElementById('fn-p-l').value.trim(),\r\n    T_L:   document.getElementById('fn-t-l').value.trim(),\r\n    A_f:   document.getElementById('fn-a-f').value.trim(),\r\n    R_f:   document.getElementById('fn-r-f').value.trim(),\r\n    P_f:   document.getElementById('fn-p-f').value.trim(),\r\n    T_f:   document.getElementById('fn-t-f').value.trim(),\r\n  };\r\n\r\n  try {\r\n    if (editingFnId) {\r\n      await api('PUT', `\/classes\/${activeClass}\/functions\/${editingFnId}`, body);\r\n      toast('Fonction mise \u00e0 jour');\r\n    } else {\r\n      await api('POST', `\/classes\/${activeClass}\/functions`, body);\r\n      toast('Fonction ajout\u00e9e');\r\n    }\r\n    closeModal('modal-fn');\r\n    await loadClasses();\r\n  } catch (e) { toast(e.message, 'err'); }\r\n}\r\n\r\n\/* \u2500\u2500 DELETE CLASS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nfunction confirmDeleteClass(classId) {\r\n  document.getElementById('confirm-title').textContent = `Supprimer la classe ${classId} ?`;\r\n  document.getElementById('confirm-msg').textContent =\r\n    'Toutes les fonctions d\\'utilit\u00e9 de cette classe seront supprim\u00e9es.';\r\n  document.getElementById('confirm-ok').onclick = async () => {\r\n    try {\r\n      await api('DELETE', `\/classes\/${classId}`);\r\n      if (activeClass === classId) {\r\n        activeClass = null;\r\n        document.getElementById('content').innerHTML = `\r\n          <div class=\"no-class-selected\">\r\n            <div class=\"big\">\u222b<\/div>\r\n            <p>S\u00e9lectionnez une classe dans la barre lat\u00e9rale<\/p>\r\n          <\/div>`;\r\n      }\r\n      closeModal('modal-confirm');\r\n      toast(`Classe ${classId} supprim\u00e9e`);\r\n      await loadClasses();\r\n    } catch (e) { toast(e.message, 'err'); }\r\n  };\r\n  openModal('modal-confirm');\r\n}\r\n\r\n\/* \u2500\u2500 DELETE FUNCTION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nfunction confirmDeleteFn(fnId, fnNom) {\r\n  document.getElementById('confirm-title').textContent = 'Supprimer la fonction ?';\r\n  document.getElementById('confirm-msg').textContent = `\"${fnNom}\" sera supprim\u00e9e de la classe ${activeClass}.`;\r\n  document.getElementById('confirm-ok').onclick = async () => {\r\n    try {\r\n      await api('DELETE', `\/classes\/${activeClass}\/functions\/${fnId}`);\r\n      closeModal('modal-confirm');\r\n      toast('Fonction supprim\u00e9e');\r\n      await loadClasses();\r\n    } catch (e) { toast(e.message, 'err'); }\r\n  };\r\n  openModal('modal-confirm');\r\n}\r\n\r\n\/* \u2500\u2500 MODAL HELPERS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nfunction openModal(id)  { document.getElementById(id).classList.add('open'); }\r\nfunction closeModal(id) { document.getElementById(id).classList.remove('open'); }\r\n\r\n\/* \u2500\u2500 TOAST \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\nfunction toast(msg, type = 'ok') {\r\n  const stack = document.getElementById('toast-stack');\r\n  const el    = document.createElement('div');\r\n  el.className = `toast ${type}`;\r\n  el.innerHTML = `<div class=\"toast-dot\"><\/div><span>${esc(msg)}<\/span>`;\r\n  stack.appendChild(el);\r\n  setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateX(12px)'; el.style.transition = 'all .3s'; setTimeout(() => el.remove(), 300); }, 3200);\r\n}\r\n\r\n\/* \u2500\u2500 KEYBOARD \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\ndocument.addEventListener('keydown', e => {\r\n  if (e.key === 'Escape') document.querySelectorAll('.overlay.open').forEach(o => o.classList.remove('open'));\r\n});\r\n<\/script>\r\n\n\n\n<p><\/p>\n<script type=\"text\/javascript\"> toolTips('.classtoolTips3','<a style=\"text-decoration: none;\" href=\"https:\/\/www.mongodb.com\/fr-fr\"><img style=\"width: 180px; height: 50px;\" src=\"\/wp-content\/uploads\/2025\/01\/mongodb-logo-rgb-scaled.jpg\" \/><\/a>'); <\/script>","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-12187","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/spgoo.org\/index.php?rest_route=\/wp\/v2\/pages\/12187","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/spgoo.org\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/spgoo.org\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/spgoo.org\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/spgoo.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=12187"}],"version-history":[{"count":2,"href":"https:\/\/spgoo.org\/index.php?rest_route=\/wp\/v2\/pages\/12187\/revisions"}],"predecessor-version":[{"id":12189,"href":"https:\/\/spgoo.org\/index.php?rest_route=\/wp\/v2\/pages\/12187\/revisions\/12189"}],"wp:attachment":[{"href":"https:\/\/spgoo.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=12187"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}