{"id":261180,"date":"2026-09-25T23:15:54","date_gmt":"2026-09-25T23:15:54","guid":{"rendered":"https:\/\/ecoleouzoud.ac.ma\/?page_id=261180"},"modified":"2026-09-28T00:08:29","modified_gmt":"2026-09-28T00:08:29","slug":"espace-parents-ar","status":"publish","type":"page","link":"https:\/\/ecoleouzoud.ac.ma\/?page_id=261180","title":{"rendered":"Espace Parents Ar"},"content":{"rendered":"<p>[et_pb_section fb_built=\u00a0\u00bb1&Prime; fullwidth=\u00a0\u00bbon\u00a0\u00bb _builder_version=\u00a0\u00bb4.27.6&Prime; _module_preset=\u00a0\u00bbdefault\u00a0\u00bb custom_margin=\u00a0\u00bb0px|20px||20px|false|true\u00a0\u00bb locked=\u00a0\u00bboff\u00a0\u00bb global_colors_info=\u00a0\u00bb{}\u00a0\u00bb][et_pb_fullwidth_code _builder_version=\u00a0\u00bb4.27.9&Prime; _module_preset=\u00a0\u00bbdefault\u00a0\u00bb global_colors_info=\u00a0\u00bb{}\u00a0\u00bb]<style>\n:root {\n  --blue:        #3B82F6;\n  --blue-dark:   #1D4ED8;\n  --blue-mid:    #60A5FA;\n  --blue-light:  #EFF6FF; \n  --blue-xlight: #F8FAFC;\n  \n  --orange:       #F97316;\n  --orange-dark:  #C2410C;\n  --orange-light: #FFF7ED;\n  \n  --red:         #EF4444;\n  --red-dark:    #B91C1C;\n  --red-light:   #FEE2E2;\n  \n  --amber:       #F59E0B;\n  --amber-light: #FEF3C7;\n  --purple:      #8B5CF6;\n  --purple-light:#F5F3FF;\n\n  --text:        #1E293B; \n  --text-2:      #475569;\n  --muted:       #64748B;\n  --border:      #E2E8F0;\n  --bg:          #F8FAFC;\n  --white:       #FFFFFF;\n\n  --sidebar:     260px;\n  --r:           12px;\n  --r-lg:        16px;\n  --s1: 0 1px 3px 0 rgba(15,23,42,0.03), 0 1px 2px 0 rgba(15,23,42,0.02);\n  --s2: 0 4px 6px -1px rgba(15,23,42,0.05), 0 2px 4px -1px rgba(15,23,42,0.03);\n  --s3: 0 20px 25px -5px rgba(15,23,42,0.08), 0 10px 10px -5px rgba(15,23,42,0.03);\n  --s-blue: 0 4px 14px 0 rgba(59,130,246,0.15);\n}\n\n.ouzoud-scope * { box-sizing:border-box; margin:0; padding:0; }\n.ouzoud-scope {\n  font-family:'Inter',sans-serif;\n  color:var(--text);\n  background:var(--bg);\n  min-height:82vh;\n  position:relative;\n  overflow:hidden;\n  border-radius:16px;\n  border:1px solid var(--border);\n}\n.ouzoud-scope h1,.ouzoud-scope h2,.ouzoud-scope h3,.ouzoud-scope h4 {\n  font-family:'Plus Jakarta Sans',sans-serif;\n  line-height:1.2;\n}\n.ouzoud-scope button { cursor:pointer; font-family:'Inter',sans-serif; }\n.ouzoud-scope input,.ouzoud-scope select,.ouzoud-scope textarea { font-family:'Inter',sans-serif; }\n\n.login-page { min-height:82vh; display:flex; background:#fff; }\n.login-left { flex:0.9; display:flex; flex-direction:column; justify-content:flex-start; padding:72px 52px 48px 52px; }\n.login-left h1 { font-size:28px; font-weight:800; color:var(--text); margin-bottom:8px; letter-spacing:-.5px; }\n.login-left > p { color:var(--muted); font-size:14.5px; margin-bottom:10px; line-height:1.6; }\n.lf-tabs { display:flex; gap:4px; margin-bottom:24px; background:#F1F5F9; padding:4px; border-radius:10px; }\n.lf-tab { flex:1; padding:10px 8px; border:none; border-radius:8px; background:transparent; font-size:13px; font-weight:600; color:var(--muted); transition:all .2s cubic-bezier(0.4, 0, 0.2, 1); white-space:nowrap; }\n.lf-tab.active { background:#fff; color:var(--blue-dark); box-shadow:var(--s1); }\n.lf-tab.admin-tab-btn.active { color:var(--purple); }\n.lf-tab.teacher-tab-btn.active { color:#059669; }\n.lf-input.lf-select { height:46px; font-size:14px; line-height:1.5; padding-top:0; padding-bottom:0; appearance:none; -webkit-appearance:none; cursor:pointer; padding-right:48px; background:#fff url(\"data:image\/svg+xml,%3Csvg xmlns='http:\/\/www.w3.org\/2000\/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2364748B' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'\/%3E%3C\/svg%3E\") no-repeat right 16px center; font-weight:600; }\n#ouzoud-login[dir=\"rtl\"] .lf-input.lf-select { padding-right:18px; padding-left:44px; background-position:left 16px center; font-family:'Cairo',sans-serif; font-size:15px; line-height:1.9; text-align:right; }\n#ouzoud-login[dir=\"rtl\"] .lf-input.lf-select option { font-family:'Cairo',sans-serif; direction:rtl; }\n.lf-btn.teacher { background:linear-gradient(135deg,#10B981,#059669); box-shadow:0 4px 14px rgba(16,185,129,.25); }\n.lf-group { margin-bottom:20px; }\n.lf-grid { margin-bottom:20px; }\n.lf-label { display:block; font-size:13px; font-weight:600; color:var(--text-2); margin-bottom:8px; }\n\n\/* Labels bilingues (FR\/AR sur la m\u00eame ligne) *\/\n.lf-label-bilingual { display:flex; justify-content:space-between; align-items:baseline; gap:16px; font-size:13px; font-weight:600; margin-bottom:8px; }\n.lf-label-bilingual .fr { color:var(--text-2); text-align:left; flex:1; }\n.lf-label-bilingual .ar { color:var(--text-2); text-align:right; direction:rtl; unicode-bidi:isolate; flex:1; font-family:'Cairo',sans-serif; }\n\n.lf-input { width:100%; border:1.5px solid var(--border); border-radius:12px; padding:14px 18px; font-size:15px; color:var(--text); background:#fff; transition:all .2s; height:52px; }\n.lf-input:focus { outline:none; border-color:var(--blue); box-shadow:0 0 0 4px rgba(59,130,246,.1); }\n.lf-input-wrap { position:relative; }\n.lf-input-wrap .lf-input { padding-right:48px; }\n.lf-eye { position:absolute; right:14px; top:50%; transform:translateY(-50%); background:none; border:none; padding:4px; cursor:pointer; color:var(--muted); line-height:1; display:flex; align-items:center; justify-content:center; transition:color .15s; }\n.lf-eye:hover { color:var(--blue-dark); }\n\n.lf-btn { width:100%; padding:15px; background:linear-gradient(135deg, var(--blue), var(--blue-dark)); color:#fff; border:none; border-radius:12px; font-size:14.5px; font-weight:700; margin-top:6px; transition:all .2s cubic-bezier(0.4, 0, 0.2, 1); box-shadow:var(--s-blue); height:52px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n.lf-btn:hover { transform:translateY(-1px); background:linear-gradient(135deg, #2563EB, #1E40AF); box-shadow:0 6px 20px rgba(59,130,246,0.3); }\n.lf-btn.admin { background:linear-gradient(135deg,var(--purple),#5B21B6); box-shadow:0 4px 20px rgba(124,58,237,.25); }\n.lf-btn.admin:hover { background:linear-gradient(135deg, #7C3AED, #4C1D95); box-shadow:0 6px 22px rgba(124,58,237,.4); }\n\n.login-right { \n  flex:1.5; \n  background:linear-gradient(135deg, #EFF6FF 0%, #FFF7ED 100%); \n  display:flex; \n  flex-direction:column; \n  justify-content:flex-start; \n  padding:32px 40px 48px 40px; \n  color:var(--text); \n  position:relative; \n  overflow:hidden; \n  border-left:1px solid var(--border);\n}\n.login-right::before { content:''; position:absolute; width:460px; height:400px; border-radius:50%; background:linear-gradient(135deg, rgba(59,130,246,0.05), rgba(249,115,22,0.05)); top:-100px; right:-100px; }\n.login-right-inner { position:relative; z-index:1; max-width:100%; }\n\n.login-right h2 { \n  font-size:26px; \n  font-weight:800; \n  color:var(--blue-dark);\n  margin-top: 15px;\n  margin-bottom:14px; \n  line-height:1.2; \n}\n.login-right > .login-right-inner > p { \n  font-size:13px; \n  color:var(--text-2); \n  margin-bottom:10px; \n  line-height:1.6;\n  white-space:nowrap;\n}\n\n.lr-feat { \n  display:flex; \n  align-items:stretch; \n  gap:0; \n  margin-bottom:20px; \n  background:#fff; \n  padding:16px; \n  border-radius:12px; \n  border:1px solid rgba(226, 232, 240, 0.8); \n  box-shadow:0 4px 6px -1px rgba(15,23,42,0.02);\n}\n.lr-feat-col { flex:1; min-width:0; }\n.lr-feat-col.lr-feat-fr { flex:1.2; }\n.lr-feat-col.lr-feat-ar { flex:0.8; }\n.lr-feat-fr { display:flex; align-items:flex-start; gap:14px; padding-right:16px; border-right:1px dashed var(--border); }\n.lr-feat-ar { display:flex; align-items:center; gap:12px; padding-left:16px; }\n.lr-feat-icon { width:36px; height:36px; border-radius:8px; display:flex; align-items:center; justify-content:center; font-size:16px; flex-shrink:0; line-height:1; text-align:center; }\n\n.lr-feat:nth-child(3) .lr-feat-icon { background:#FEE2E2; color:var(--red); }\n.lr-feat:nth-child(4) .lr-feat-icon { background:var(--blue-light); color:var(--blue-dark); }\n.lr-feat:nth-child(5) .lr-feat-icon { background:var(--orange-light); color:var(--orange); }\n.lr-feat:nth-child(6) .lr-feat-icon { background:#ECFDF5; color:#059669; }\n.lr-feat-icon svg { width:20px; height:20px; }\n\n.lr-feat h4 { font-size:14.5px; font-weight:700; color:var(--text); margin-bottom:4px; }\n.lr-feat p { font-size:13px; color:var(--muted); line-height:1.4; }\n.lr-feat-body { flex:1; min-width:0; }\n\n\/* [AR] Traduction arabe (police Cairo) affich\u00e9e sous chaque ligne de la page d'accueil *\/\n.ar-tr {\n  display:block;\n  font-family:'Cairo',sans-serif;\n  direction:rtl;\n  unicode-bidi:isolate;\n  text-align:right;\n  white-space:normal;\n  font-weight:600;\n  opacity:.75;\n}\n.login-left > p, .login-right-inner > p { white-space:normal; }\n.lf-tab .ar-tr { font-size:10.5px; margin-top:3px; font-weight:600; text-align:center; }\n.lf-label .ar-tr { font-size:11.5px; margin-top:4px; color:var(--muted); font-weight:500; opacity:1; }\n.lf-btn .ar-tr { font-size:11.5px; margin-top:3px; font-weight:600; opacity:.92; text-align:center; }\n\n\/* Blocs \"titre + sous-titre\" arabes, affich\u00e9s en dessous du bloc fran\u00e7ais correspondant *\/\n.ar-block { margin-top:16px; padding-top:16px; border-top:1px dashed var(--border); direction:rtl; unicode-bidi:isolate; }\n.ar-block .ar-title { font-family:'Cairo',sans-serif; font-weight:800; color:var(--text); text-align:right; line-height:1.3; margin-bottom:6px; }\n.ar-block .ar-subtitle { font-family:'Cairo',sans-serif; font-weight:500; color:var(--muted); text-align:right; line-height:1.6; }\n\n.login-left .ar-block { margin-top:4px; padding-top:10px; margin-bottom:22px; }\n.login-left .ar-block .ar-title { font-size:22px; letter-spacing:0; }\n.login-left .ar-block .ar-subtitle { font-size:13px; }\n\n.login-right .ar-block { border-top-color:rgba(226,232,240,.9); margin-top:4px; padding-top:10px; margin-bottom:26px; }\n.login-right .ar-block .ar-title { font-size:19px; color:var(--blue-dark); }\n.login-right .ar-block .ar-subtitle { font-size:12px; color:var(--text-2); }\n\n.lr-feat .ar-block { margin:0; padding:0; border-top:none; width:100%; }\n.lr-feat .ar-block .ar-title { font-size:13.5px; margin-bottom:2px; }\n.lr-feat .ar-block .ar-subtitle { font-size:11.5px; line-height:1.4; }\n\n\/* [FIX] Cartes de la page de connexion (lr-feat) : le markup utilise .ar-block-inline,\n   qui n'avait aucune r\u00e8gle d\u00e9di\u00e9e (les titres\/sous-titres restaient sans police Cairo\n   ni marge) \u2014 on reprend le m\u00eame style que .lr-feat .ar-block ci-dessus. *\/\n.lr-feat .ar-block-inline { width:100%; }\n.lr-feat .ar-block-inline .ar-title { font-family:'Cairo',sans-serif; font-weight:800; color:var(--text); text-align:right; line-height:1.3; font-size:13.5px; margin-bottom:5px; }\n.lr-feat .ar-block-inline .ar-subtitle { font-family:'Cairo',sans-serif; font-weight:500; color:var(--muted); text-align:right; line-height:1.4; font-size:11.5px; }\n\n\/* Sections bilingues avec titres et sous-titres align\u00e9s horizontalement *\/\n.bilingual-section { display:flex; flex-direction:column; gap:4px; }\n.bilingual-title { display:flex; justify-content:space-between; align-items:baseline; gap:16px; }\n.bilingual-title .fr { font-family:'Plus Jakarta Sans',sans-serif; font-weight:800; color:var(--text); text-align:left; line-height:1.2; flex:1; }\n.bilingual-title .ar { font-family:'Cairo',sans-serif; font-weight:800; color:var(--text); text-align:right; direction:rtl; unicode-bidi:isolate; flex:1; }\n\n.bilingual-subtitle { display:flex; flex-direction:column; gap:8px; }\n.bilingual-subtitle .fr { font-family:'Inter',sans-serif; font-weight:400; color:var(--muted); text-align:left; line-height:1.6; width:100%; font-size:14.5px; margin-top:10px; margin-bottom:24px; }\n.bilingual-subtitle .ar { font-family:'Cairo',sans-serif; font-weight:500; color:var(--muted); text-align:right; direction:rtl; unicode-bidi:isolate; width:100%; font-size:13px; line-height:1.6; margin-top:14px; margin-bottom:24px; }\n\n.login-left .bilingual-title .fr { font-size:24px; letter-spacing:-.5px; }\n.login-left .bilingual-title .ar { font-size:22px; }\n\n.login-right .bilingual-title .fr { font-size:22px; }\n.login-right .bilingual-title .ar { font-size:20px; color:var(--blue-dark); }\n\n\/* Ajustements pour laisser la place \u00e0 la 2e ligne (arabe) sans casser les boutons\/onglets *\/\n.lf-tabs { row-gap:4px; }\n.lf-tab { white-space:normal; line-height:1.3; padding:8px 6px; }\n.lf-btn { height:auto; min-height:52px; white-space:normal; line-height:1.3; padding:12px 15px; }\n\n\/* [AR] Espace parent, espace enseignant et espace admin \u2014 traduction arabe (police Cairo)\n   sous chaque libell\u00e9 fran\u00e7ais, m\u00eame principe que la page d'accueil.\n   Les trois espaces (#ouzoud-parent-app, #ouzoud-admin-app, #ouzoud-teacher-app) partagent\n   les m\u00eames r\u00e8gles de mise en forme ci-dessous. *\/\n#ouzoud-parent-app .ar-tr,\n#ouzoud-admin-app .ar-tr,\n#ouzoud-teacher-app .ar-tr { font-size:11.5px; line-height:1.45; margin-top:2px; letter-spacing:0; text-transform:none; font-style:normal; }\n#ouzoud-parent-app .ar-tr.ar-l,\n#ouzoud-admin-app .ar-tr.ar-l,\n#ouzoud-teacher-app .ar-tr.ar-l { text-align:left; }\n#ouzoud-parent-app .ar-tr.ar-c,\n#ouzoud-admin-app .ar-tr.ar-c,\n#ouzoud-teacher-app .ar-tr.ar-c { text-align:center; }\n#ouzoud-parent-app .nav-txt,\n#ouzoud-admin-app .nav-txt,\n#ouzoud-teacher-app .nav-txt { display:flex; flex-direction:column; align-items:center; line-height:1.25; }\n#ouzoud-parent-app .nav-txt .ar-tr,\n#ouzoud-admin-app .nav-txt .ar-tr,\n#ouzoud-teacher-app .nav-txt .ar-tr { font-size:10.5px; text-align:center; margin-top:1px; }\n#ouzoud-parent-app .main-nav-btn,\n#ouzoud-admin-app .main-nav-btn { height:auto; min-height:44px; }\n#ouzoud-parent-app .m-brand-name .ar-tr,\n#ouzoud-admin-app .m-brand-name .ar-tr { font-size:11.5px; font-weight:700; text-align:center; margin-top:1px; }\n#ouzoud-teacher-app .t-brand-name .ar-tr { font-size:11px; font-weight:700; text-align:center; margin-top:1px; opacity:.9; }\n#ouzoud-parent-app .kpi-label .ar-tr,\n#ouzoud-admin-app .kpi-label .ar-tr { font-size:11px; text-align:left; margin-top:1px; }\n\/* Tableaux Absences et Cahier de Devoirs : texte arabe align\u00e9 \u00e0 gauche (cellules) *\/\n#p-absences .tbl .ar-tr,\n#a-absences .tbl .ar-tr,\n#p-devoirs .tbl .ar-tr,\n#a-devoirs .tbl .ar-tr,\n#ouzoud-teacher-app .tbl .ar-tr { text-align:left; }\n\/* En-t\u00eates de cartes : fran\u00e7ais \u00e0 gauche, arabe \u00e0 droite sur la m\u00eame ligne \u2192 titre et sous-titre coll\u00e9s (espacement minimal) *\/\n#ouzoud-parent-app .card-header h4,\n#ouzoud-parent-app .card-header p,\n#ouzoud-admin-app .card-header h4,\n#ouzoud-admin-app .card-header p,\n#ouzoud-teacher-app .card-header h4,\n#ouzoud-teacher-app .card-header p { display:flex; justify-content:space-between; align-items:baseline; gap:12px; margin:0; }\n#ouzoud-parent-app .card-header h4,\n#ouzoud-admin-app .card-header h4,\n#ouzoud-teacher-app .card-header h4 { line-height:1.3; }\n#ouzoud-parent-app .card-header p,\n#ouzoud-admin-app .card-header p,\n#ouzoud-teacher-app .card-header p { line-height:1.3; }\n#ouzoud-parent-app .card-header h4 .ar-tr,\n#ouzoud-admin-app .card-header h4 .ar-tr,\n#ouzoud-teacher-app .card-header h4 .ar-tr { font-size:14.5px; font-weight:800; color:var(--text); opacity:1; margin-top:0; line-height:1.3; }\n#ouzoud-parent-app .card-header p .ar-tr,\n#ouzoud-admin-app .card-header p .ar-tr,\n#ouzoud-teacher-app .card-header p .ar-tr { font-size:12px; font-weight:500; color:var(--muted); opacity:1; margin-top:0; line-height:1.3; }\n\/* Formulaires (espaces Admin et Enseignant) : libell\u00e9 arabe sous le libell\u00e9 fran\u00e7ais *\/\n#ouzoud-admin-app .form-label .ar-tr,\n#ouzoud-teacher-app .form-label .ar-tr,\n.modal-box .form-label .ar-tr { font-size:11px; font-weight:500; color:var(--muted); opacity:1; margin-top:2px; }\n#ouzoud-admin-app .page-title h4 .ar-tr,\n#ouzoud-teacher-app .page-title h4 .ar-tr { font-size:13px; font-weight:700; opacity:.85; margin-top:1px; }\n#ouzoud-admin-app .btn .ar-tr,\n#ouzoud-teacher-app .btn .ar-tr,\n.modal-box .btn .ar-tr { font-size:11px; font-weight:600; opacity:.92; margin-top:2px; }\n@media(max-width:600px){\n  \/* T\u00e9l\u00e9phone : retour \u00e0 l'empilement (fran\u00e7ais puis arabe) pour \u00e9viter des lignes trop \u00e9troites *\/\n  #ouzoud-parent-app .card-header h4,\n  #ouzoud-parent-app .card-header p,\n  #ouzoud-admin-app .card-header h4,\n  #ouzoud-admin-app .card-header p,\n  #ouzoud-teacher-app .card-header h4,\n  #ouzoud-teacher-app .card-header p { display:block; }\n  #ouzoud-parent-app .card-header h4 .ar-tr,\n  #ouzoud-parent-app .card-header p .ar-tr,\n  #ouzoud-admin-app .card-header h4 .ar-tr,\n  #ouzoud-admin-app .card-header p .ar-tr,\n  #ouzoud-teacher-app .card-header h4 .ar-tr,\n  #ouzoud-teacher-app .card-header p .ar-tr { margin-top:1px; }\n}\n#ouzoud-parent-app .tbl th .th-txt,\n#ouzoud-admin-app .tbl th .th-txt,\n#ouzoud-teacher-app .tbl th .th-txt { display:inline-flex; flex-direction:column; align-items:flex-start; }\n\/* Tableaux Absences et Cahier de Devoirs : en-t\u00eates de colonnes (texte arabe) centr\u00e9s *\/\n#p-absences .tbl th .th-txt,\n#a-absences .tbl th .th-txt,\n#p-devoirs .tbl th .th-txt,\n#a-devoirs .tbl th .th-txt,\n#ouzoud-teacher-app .tbl th .th-txt { align-items:center; }\n#ouzoud-parent-app .tbl th .ar-tr,\n#ouzoud-admin-app .tbl th .ar-tr,\n#ouzoud-teacher-app .tbl th .ar-tr { font-size:10.5px; font-weight:600; text-align:center; align-self:center; opacity:.85; }\n#ouzoud-parent-app .tbl td .ar-tr,\n#ouzoud-admin-app .tbl td .ar-tr,\n#ouzoud-teacher-app .tbl td .ar-tr { font-size:11px; margin-top:4px; }\n#ouzoud-parent-app #p-absences-pagination-info .ar-tr,\n#ouzoud-parent-app #p-devoirs-pagination-info .ar-tr,\n#ouzoud-parent-app #p-annonces-pagination-info .ar-tr,\n#ouzoud-admin-app [id$=\"-pagination-info\"] .ar-tr,\n#ouzoud-teacher-app [id$=\"-pagination-info\"] .ar-tr { font-size:11px; text-align:left; margin-top:1px; }\n\n.app-workspace { display:none; min-height:82vh; }\n.app-workspace.active { display:flex; }\n\n.sidebar { width:var(--sidebar); border-right:1px solid var(--border); display:flex; flex-direction:column; padding:0; overflow:hidden; background:#fff; }\n.sb-header { padding:18px 16px 16px; border-bottom:1px solid var(--border); display:flex; align-items:center; gap:10px; }\n.sb-brand-text .name { font-family:'Plus Jakarta Sans',sans-serif; font-size:16px; font-weight:800; color:var(--blue-dark); }\n.sb-user { margin:12px; padding:12px; background:var(--blue-light); border:1px solid rgba(59,130,246,0.12); border-radius:10px; display:flex; align-items:center; gap:10px; }\n.sb-user.admin-mode { background:var(--purple-light); border-color:rgba(124,58,237,0.1); }\n.sb-av { width:36px; height:36px; border-radius:50%; background:linear-gradient(135deg,var(--blue),var(--blue-dark)); display:flex; align-items:center; justify-content:center; color:#fff; font-weight:800; font-size:13px; flex-shrink:0; }\n.sb-av.admin-mode { background:linear-gradient(135deg,var(--purple),#5B21B6); }\n.sb-un { font-size:13px; font-weight:700; color:var(--text); }\n.sb-ur { font-size:11px; color:var(--muted); margin-top:1px; }\n.sb-nav { padding:8px 10px; flex:1; overflow-y:auto; }\n.sb-sec { font-size:11px; font-weight:700; text-transform:uppercase; color:var(--muted); padding:12px 8px 6px; letter-spacing:.05em; }\n.sb-link { display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:8px; font-size:13.5px; font-weight:500; color:var(--text-2); width:100%; text-align:left; border:none; background:none; margin-bottom:2px; transition:all .15s ease; }\n.sb-link:hover { background:#F1F5F9; color:var(--text); }\n.sb-link.active { background:var(--blue-light); color:var(--blue-dark); font-weight:600; }\n.sb-link.admin-active.active { background:var(--purple-light); color:var(--purple); }\n.sb-link .sb-icon { width:24px; height:24px; display:flex; align-items:center; justify-content:center; font-size:14px; flex-shrink:0; }\n.sb-footer { padding:12px; border-top:1px solid var(--border); }\n.sb-logout { display:flex; align-items:center; justify-content:center; gap:8px; width:100%; padding:12px; border-radius:8px; font-size:13.5px; font-weight:600; color:var(--muted); border:1px solid var(--border); background:#fff; transition:all .15s ease; }\n.sb-logout:hover { color:var(--red); border-color:var(--red); background:var(--red-light); }\n\n.main-body { flex:1; background:var(--bg); display:flex; flex-direction:column; overflow:hidden; }\n.topbar { height:60px; background:#fff; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; padding:0 24px; flex-shrink:0; }\n.topbar-left h3 { font-size:16px; font-weight:700; color:var(--text); }\n\n.topbar-date, .date-left, .tbl td.date-cell { \n  direction: ltr !important; \n  text-align: left !important; \n}\n\n\/* [FIX] Espace parent (AR) : dates centr\u00e9es dans les tableaux Devoirs \/ Absences *\/\n#ouzoud-parent-app .tbl td.date-cell {\n  text-align: center !important;\n}\n\n.topbar-date { font-size:13px; color:var(--muted); font-weight:600; background:#F1F5F9; padding:6px 14px; border-radius:20px; }\n.content-area { padding:24px; flex:1; overflow-y:auto; }\n.page-pane { display:none; }\n.page-pane.active { display:block; }\n\n.card { background:#fff; border:1px solid var(--border); border-radius:var(--r-lg); padding:24px; box-shadow:var(--s1); margin-bottom:20px; }\n.card-header { margin-bottom:16px; }\n.card-header h4 { font-size:16px; font-weight:700; color:var(--text); }\n.card-header p { font-size:13px; color:var(--muted); margin-top:2px; }\n\n.g4 { display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-bottom:20px; }\n.g21 { display:grid; grid-template-columns:2fr 1fr; gap:20px; }\n.g-devoirs { display:grid; grid-template-columns:360px 1fr; gap:20px; align-items:start; }\n.kpi { padding:18px; border-radius:var(--r-lg); background:#fff; border:1px solid var(--border); display:flex; align-items:center; gap:14px; box-shadow:var(--s1); transition:all .2s ease; }\n.kpi:hover { box-shadow:var(--s2); transform:translateY(-1px); }\n.kpi-icon { width:46px; height:46px; border-radius:12px; display:flex; align-items:center; justify-content:center; font-size:20px; flex-shrink:0; }\n.kpi-icon svg { width:22px; height:22px; }\n.kpi-icon.blue  { background:var(--blue-light); color:var(--blue-dark); }\n.kpi-icon.orange { background:var(--orange-light); color:var(--orange-dark); }\n.kpi-icon.amber { background:var(--amber-light); color:var(--amber); }\n.kpi-icon.red   { background:var(--red-light); color:var(--red); }\n.kpi-val { font-family:'Plus Jakarta Sans',sans-serif; font-size:24px; font-weight:800; color:var(--text); }\n.kpi-label { font-size:12px; color:var(--muted); font-weight:500; margin-top:2px; }\n\n.tbl { width:100%; border-collapse:collapse; table-layout:fixed; }\n.tbl td, .tbl th { overflow:hidden; }\n.tbl th { font-size:11px; font-weight:700; text-transform:uppercase; color:var(--muted); padding:12px 14px; text-align:left; background:#F8FAFC; border-bottom:1px solid var(--border); letter-spacing:.05em; white-space:nowrap; }\n.tbl td { padding:14px; border-bottom:1px solid var(--border); font-size:13.5px; color:var(--text-2); word-break:break-word; }\n\/* Dates et p\u00e9riodes : texte un peu plus petit, sur une seule ligne (\u00e9crans > 600px) *\/\n@media (min-width:601px) {\n  #a-absences .tbl td[data-label=\"Date\"], #a-absences .tbl td[data-label=\"P\u00e9riode\"],\n  #a-devoirs .tbl td[data-label=\"Date Butoir\"],\n  #ouzoud-teacher-app .tbl td[data-label=\"Date Butoir\"],\n  #p-absences .tbl td[data-label=\"Date d'absence\"], #p-absences .tbl td[data-label=\"P\u00e9riode\"],\n  #p-devoirs .tbl td.date-cell { font-size:12.5px; white-space:nowrap; word-break:normal; }\n}\n.tbl tr:last-child td { border-bottom:none; }\n.tbl tbody tr:hover td { background:#F8FAFC; }\n\/* [BULK] Suppression en lot *\/\n.tbl th.bulk-th, .tbl td.bulk-cell { width:44px; padding-left:14px; padding-right:0; text-align:center; }\n.bulk-cb { width:16px; height:16px; cursor:pointer; accent-color:var(--blue); vertical-align:middle; margin:0; }\n.bulk-lbl { display:inline-flex; align-items:center; gap:8px; cursor:pointer; margin:0; }\n.bulk-lbl-txt { display:none; }\n.bulk-bar { display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin:0 0 10px; padding:10px 12px; background:#FEF2F2; border:1px solid #FECACA; border-radius:10px; }\n.bulk-bar .bulk-count { font-size:13px; font-weight:700; color:#991B1B; margin-right:auto; }\n.bulk-bar .bulk-del-btn { background:var(--red); color:#fff; border:1px solid var(--red); }\n.bulk-bar .bulk-del-btn:disabled { opacity:.6; cursor:not-allowed; }\n.tbl tbody tr:has(input.bulk-cb:checked) td { background:#FEF2F2; }\n\/* Scroll indicator on tbl-wrap *\/\n\/* [FIX32] Filtres et recherche dans les registres (absences \/ annonces) *\/\n.reg-filters { display:flex; flex-wrap:wrap; gap:8px; align-items:flex-end; margin-bottom:12px; }\n.reg-filters .reg-search { position:relative; flex:1 1 220px; min-width:180px; }\n.reg-filters .reg-search .reg-ico { position:absolute; left:12px; top:50%; transform:translateY(-50%); color:var(--muted); pointer-events:none; display:flex; }\n.reg-filters .reg-search .form-input { padding-left:36px; }\n.reg-filters .form-input, .reg-filters .form-select { height:38px; font-size:13px; padding-top:0; padding-bottom:0; }\n.reg-filters .reg-select { flex:0 0 180px; width:180px; min-width:0; }\n.reg-filters .btn { height:38px; flex-shrink:0; }\n.reg-count { font-size:12.5px; color:var(--muted); margin:-4px 0 10px; min-height:16px; }\n@media (max-width: 640px) {\n  .reg-filters .reg-select { flex:1 1 45%; width:auto; min-width:0; }\n  .reg-filters .btn { flex:1 1 100%; }\n}\n.tbl-wrap { position:relative; }\n.tbl-wrap::after {\n  content:'';\n  position:absolute;\n  top:0; right:0; bottom:0;\n  width:28px;\n  background:linear-gradient(to right, transparent, rgba(255,255,255,0.9));\n  pointer-events:none;\n  opacity:0;\n  transition:opacity .2s;\n  border-radius:0 8px 8px 0;\n}\n.tbl-wrap.can-scroll::after { opacity:1; }\n\n.chip { display:inline-flex; align-items:center; font-size:11.5px; font-weight:600; padding:4px 10px; border-radius:20px; white-space:nowrap; }\n.chip-blue  { background:var(--blue-light);   color:var(--blue-dark); }\n.chip-orange { background:var(--orange-light);  color:var(--orange-dark); }\n.chip-red   { background:var(--red-light);    color:var(--red); }\n.chip-amber { background:var(--amber-light);  color:var(--amber); }\n\n.form-group { margin-bottom:18px; }\n.form-label { display:block; font-size:13px; font-weight:600; color:var(--text-2); margin-bottom:8px; }\n\n.form-input, .form-select, .form-textarea { \n  width:100%; \n  border:1.5px solid var(--border); \n  border-radius:12px; \n  padding:12px 16px; \n  font-size:14.5px; \n  color:var(--text); \n  background:#fff; \n  transition:all .2s; \n}\n.form-input, .form-select {\n  height:48px;\n}\n.form-input:focus, .form-select:focus, .form-textarea:focus { \n  outline:none; \n  border-color:var(--blue); \n  box-shadow:0 0 0 4px rgba(59,130,246,.08); \n}\n.form-textarea { min-height:110px; resize:vertical; line-height:1.5; }\n.form-row { display:grid; grid-template-columns:1fr 1fr; gap:16px; }\n\n.form-stack-full { display:block; }\n.form-stack-full .form-group { margin-bottom:18px; width:100%; }\n\n\/* Recherche rapide d'\u00e9l\u00e8ve (formulaire de saisie d'absence) *\/\n.eleve-search-wrap { position:relative; }\n.eleve-search-wrap .form-input { padding-left:42px; }\n.eleve-search-icon { position:absolute; left:14px; top:50%; transform:translateY(-50%); font-size:15px; color:var(--muted); pointer-events:none; }\n.eleve-search-results {\n  position:absolute; left:0; right:0; top:calc(100% + 6px); z-index:30;\n  background:#fff; border:1.5px solid var(--border); border-radius:12px;\n  box-shadow:var(--s2); max-height:260px; overflow-y:auto; display:none;\n}\n.eleve-search-results.open { display:block; }\n.eleve-search-item { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:10px 14px; cursor:pointer; font-size:14px; }\n.eleve-search-item:hover, .eleve-search-item.active { background:var(--blue-light); }\n.eleve-search-item .esi-name { font-weight:600; color:var(--text); }\n.eleve-search-item .esi-classe { font-size:12px; color:var(--muted); background:var(--bg); border:1px solid var(--border); border-radius:999px; padding:2px 8px; white-space:nowrap; }\n.eleve-search-empty { padding:12px 14px; font-size:13px; color:var(--muted); text-align:center; }\n.eleve-search-hint { font-size:12px; color:var(--muted); margin-top:6px; display:block; }\n\n\/* Toggle \"saisie multiple\" + checklist d'\u00e9l\u00e8ves (formulaire d'absence) *\/\n.abs-mode-toggle-group { margin-bottom:14px; }\n.abs-mode-toggle { display:flex; align-items:center; gap:9px; cursor:pointer; font-size:13.5px; font-weight:600; color:var(--text-2); user-select:none; }\n.abs-mode-toggle input[type=\"checkbox\"] { width:17px; height:17px; cursor:pointer; accent-color: var(--blue, #2563eb); }\n.abs-multi-actions { display:flex; align-items:center; gap:8px; margin-bottom:8px; flex-wrap:wrap; }\n.abs-multi-action-btn { font-size:12px; font-weight:600; padding:5px 10px; border-radius:8px; border:1.5px solid var(--border); background:#fff; color:var(--text-2); cursor:pointer; }\n.abs-multi-action-btn:hover { background:var(--bg); }\n.abs-multi-count { font-size:12px; color:var(--muted); margin-left:auto; }\n.abs-multi-list { border:1.5px solid var(--border); border-radius:12px; max-height:280px; overflow-y:auto; background:#fff; }\n.abs-multi-item { display:flex; align-items:center; gap:10px; padding:9px 14px; border-bottom:1px solid var(--border); cursor:pointer; font-size:14px; }\n.abs-multi-item:last-child { border-bottom:none; }\n.abs-multi-item:hover { background:var(--bg); }\n.abs-multi-item input[type=\"checkbox\"] { width:16px; height:16px; cursor:pointer; accent-color: var(--blue, #2563eb); flex-shrink:0; }\n.abs-multi-item span { color:var(--text); }\n.abs-multi-item.checked { background:var(--blue-light); }\n\/* [FIX31] Liste d'appel : chaque \u00e9l\u00e8ve = Pr\u00e9sent (vert) \/ Absent (rouge) + p\u00e9riode propre \u00e0 l'\u00e9l\u00e8ve *\/\n.abs-multi-item { flex-wrap:nowrap; }\n.abs-multi-item .abs-name { flex:1; min-width:120px; color:var(--text); }\n.abs-multi-item .abs-state { font-size:11.5px; font-weight:700; padding:3px 11px; border-radius:999px; background:#D1FAE5; color:#065F46; white-space:nowrap; }\n#abs-multi-list .abs-multi-item.checked { background:#FEF2F2; }\n#abs-multi-list .abs-multi-item.checked .abs-state { background:#FEE2E2; color:#991B1B; }\n\n.btn { display:inline-flex; align-items:center; justify-content:center; gap:8px; border:none; border-radius:12px; padding:12px 24px; font-size:14.5px; font-weight:600; transition:all .15s ease; cursor:pointer; height:48px; }\n.btn-blue { background:linear-gradient(135deg,var(--blue),var(--blue-dark)); color:#fff; box-shadow:0 2px 6px rgba(59,130,246,0.15); }\n.btn-blue:hover { transform:translateY(-1px); background:linear-gradient(135deg, #2563EB, #1E40AF); box-shadow:var(--s-blue); }\n.btn-purple { background:linear-gradient(135deg,var(--purple),#5B21B6); color:#fff; }\n.btn-purple:hover { transform:translateY(-1px); background:linear-gradient(135deg, #7C3AED, #4C1D95); }\n.btn-outline { background:#fff; border:1.5px solid var(--border); color:var(--muted); }\n.btn-outline:hover { border-color:var(--blue); color:var(--blue-dark); background:var(--blue-light); }\n.btn-sm { padding:8px 16px; font-size:13px; border-radius:8px; height:36px; }\n.ouzoud-file-btn { white-space:nowrap; }\n.btn-lg { padding:14px 28px; font-size:15.5px; font-weight:700; border-radius:12px; height:52px; }\n\n.ann-card { border-radius:var(--r); border:1px solid var(--border); padding:16px; background:#fff; margin-bottom:12px; border-left:4px solid var(--blue); }\n.ann-card.urgent { border-left-color:var(--red); }\n\n.modal-ov { position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(15,23,42,0.2); backdrop-filter:blur(4px); z-index:999999; display:none; align-items:center; justify-content:center; padding:24px; box-sizing:border-box; }\n\/* [FIX#20] : les fen\u00eatres (fiche parent\/enseignant, import CSV) peuvent contenir beaucoup\n   de champs (ex: toutes les classes cochables). Sans hauteur max ni d\u00e9filement propre,\n   le bas du formulaire (boutons Annuler\/Enregistrer) sortait de l'\u00e9cran et devenait\n   inaccessible. On limite donc la hauteur de la fen\u00eatre et on affiche une barre de\n   d\u00e9filement verticale, fine et discr\u00e8te, uniquement quand le contenu d\u00e9passe. *\/\n.modal-box {\n  background:#fff; border-radius:16px; padding:26px; width:100%; max-width:500px;\n  box-shadow:var(--s3); border:1px solid var(--border);\n  max-height:calc(100vh - 48px); overflow-y:auto;\n  scrollbar-width:thin; scrollbar-color:#CBD5E1 transparent;\n}\n.modal-box::-webkit-scrollbar { width:8px; }\n.modal-box::-webkit-scrollbar-track { background:transparent; }\n.modal-box::-webkit-scrollbar-thumb { background:#CBD5E1; border-radius:8px; }\n.modal-box::-webkit-scrollbar-thumb:hover { background:#94A3B8; }\n.modal-header { display:flex; align-items:flex-start; justify-content:space-between; margin-bottom:20px; }\n\/* [FIX34] Les fen\u00eatres sont d\u00e9plac\u00e9es dans <body> (hors .ouzoud-scope) : on r\u00e9tablit box-sizing et marges pour qu'elles s'affichent pareil quel que soit le th\u00e8me *\/\n.modal-ov, .modal-ov *, .ouzoud-toast-wrap, .ouzoud-toast-wrap * { box-sizing:border-box; }\n.modal-header h3 { font-size:17px; font-weight:800; margin:0; }\n.modal-header p { font-size:13px; color:var(--muted); margin:3px 0 0; }\n.modal-close { width:28px; height:28px; border-radius:50%; background:var(--bg); border:none; display:flex; align-items:center; justify-content:center; color:var(--muted); font-size:16px; transition:all .15s; }\n.modal-close:hover { background:var(--red-light); color:var(--red); }\n\n.page-title { display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; }\n.page-title h4 { font-size:18px; font-weight:800; color:var(--text); }\n\n\/* \u2500\u2500 Toast notifications \u2500\u2500 *\/\n.ouzoud-toast-wrap { position:fixed; bottom:28px; right:28px; z-index:1000000; display:flex; flex-direction:column; gap:10px; pointer-events:none; }\n.ouzoud-toast { display:flex; align-items:center; gap:10px; padding:13px 18px; border-radius:12px; font-size:13.5px; font-weight:600; color:#fff; box-shadow:0 8px 24px rgba(15,23,42,0.15); opacity:0; transform:translateY(12px); transition:all .25s cubic-bezier(0.4,0,0.2,1); pointer-events:auto; max-width:320px; }\n.ouzoud-toast.show { opacity:1; transform:translateY(0); }\n.ouzoud-toast.success { background:linear-gradient(135deg,#10B981,#059669); }\n.ouzoud-toast.error   { background:linear-gradient(135deg,var(--red),var(--red-dark)); }\n.ouzoud-toast.info    { background:linear-gradient(135deg,var(--blue),var(--blue-dark)); }\n.ouzoud-toast-icon    { font-size:16px; flex-shrink:0; }\n\n\/* \u2500\u2500 Workspace universel (Parent & Admin) : layout topbar \u2500\u2500 *\/\n.main-workspace { display:none; min-height:82vh; flex-direction:column; background:var(--bg); }\n.main-workspace.active { display:flex; }\n\n.main-topbar {\n  background:#fff;\n  border-bottom:1px solid var(--border);\n  padding:0 28px;\n  display:flex;\n  align-items:center;\n  justify-content:space-between;\n  flex-shrink:0;\n  flex-wrap:nowrap;\n  gap:12px;\n}\n.main-topbar-left { display:flex; align-items:center; gap:0; min-width:0; flex:1 1 auto; }\n.main-topbar-brand {\n  display:flex; align-items:center; gap:12px;\n  padding:14px 20px 14px 0;\n  border-right:1px solid var(--border);\n  margin-right:8px;\n}\n.main-topbar-brand .m-brand-name {\n  font-family:'Plus Jakarta Sans',sans-serif;\n  font-size:15px; font-weight:800; letter-spacing:-.3px;\n}\n.main-topbar-brand .m-brand-name.parent-brand { color:var(--blue-dark); }\n.main-topbar-brand .m-brand-name.admin-brand  { color:var(--purple); }\n.main-topbar-brand .m-brand-icon {\n  width:34px; height:34px; border-radius:10px;\n  display:flex; align-items:center; justify-content:center; font-size:16px; flex-shrink:0;\n}\n.main-topbar-brand .m-brand-icon.parent-icon { background:var(--blue-light); }\n.main-topbar-brand .m-brand-icon.admin-icon  { background:var(--purple-light); }\n.main-topbar-brand .main-topbar-user-name { padding-left:12px; border-left:1px solid var(--border); }\n\n\/* Navigation horizontale *\/\n.main-topbar-nav { display:flex; align-items:center; gap:2px; height:100%; position:relative; min-width:0; overflow-x:auto; -webkit-overflow-scrolling:touch; scrollbar-width:none; }\n.main-topbar-nav::-webkit-scrollbar { display:none; }\n\/* [FIX#16 v4.6] : indicateur de fade quand la nav devient scrollable horizontalement\n   (tablette\/t\u00e9l\u00e9phone) \u2014 m\u00eame principe que .tbl-wrap, activ\u00e9 via JS (classe .can-scroll). *\/\n.main-topbar-nav::after {\n  content:'';\n  position:absolute;\n  top:0; right:0; bottom:0;\n  width:22px;\n  background:linear-gradient(to right, transparent, rgba(255,255,255,0.95));\n  pointer-events:none;\n  opacity:0;\n  transition:opacity .2s;\n}\n.main-topbar-nav.can-scroll::after { opacity:1; }\n.main-nav-btn {\n  display:flex; align-items:center; gap:7px;\n  padding:10px 14px; border-radius:8px;\n  font-size:13.5px; font-weight:500;\n  color:var(--text-2); border:none; background:none;\n  transition:all .15s ease; white-space:nowrap;\n  height:44px;\n}\n.main-nav-btn:hover { background:#F1F5F9; color:var(--text); }\n.main-nav-btn.active { background:var(--blue-light); color:var(--blue-dark); font-weight:600; }\n.main-nav-btn.admin-nav.active { background:var(--purple-light); color:var(--purple); }\n.main-nav-btn svg { flex-shrink:0; }\n\n.main-topbar-right { display:flex; align-items:center; gap:12px; padding:10px 0; flex-shrink:0; }\n.main-topbar-user { display:flex; align-items:center; gap:8px; }\n.main-topbar-user-name { font-size:15px; font-weight:700; color:var(--text); }\n.main-topbar-date { font-size:13px; color:var(--muted); font-weight:600; background:#F1F5F9; padding:6px 14px; border-radius:20px; }\n#p-date-bar { font-family:'Cairo',sans-serif; direction:rtl; unicode-bidi:isolate; font-size:14px; }\n.main-logout-btn {\n  display:flex; align-items:center; gap:7px;\n  padding:9px 16px; border-radius:8px;\n  font-size:13px; font-weight:600;\n  color:var(--muted); border:1px solid var(--border); background:#fff;\n  transition:all .15s ease;\n}\n.main-logout-btn:hover { color:var(--red); border-color:var(--red); background:var(--red-light); }\n.main-logout-btn svg { flex-shrink:0; }\n\n.main-content-area { padding:24px; flex:1; overflow-y:auto; }\n\n@media(max-width:860px){\n  \/* [FIX#16 v4.6] : sur tablette\/t\u00e9l\u00e9phone, la marque + la navigation horizontale\n     se partageaient l'espace avec les infos utilisateur, ce qui rendait les onglets\n     trop \u00e9troits pour \u00eatre cliqu\u00e9s confortablement au doigt. On empile d\u00e9sormais\n     la barre sur 2 lignes pleine largeur : (marque + nav), puis (utilisateur + d\u00e9connexion). *\/\n  .main-topbar { padding:0; flex-wrap:wrap; gap:0; }\n  .main-topbar-left { flex-direction:column; align-items:flex-start; width:100%; gap:0; }\n  .main-topbar-brand { width:100%; border-right:none; padding:10px 16px; margin-right:0; box-sizing:border-box; }\n  .main-topbar-nav { width:100%; gap:2px; padding:0 10px 8px; overflow-x:auto; -webkit-overflow-scrolling:touch; }\n  .main-nav-btn { padding:8px 10px; font-size:12.5px; }\n  .main-topbar-right { width:100%; justify-content:space-between; padding:8px 16px; border-top:1px solid var(--border); box-sizing:border-box; }\n  .main-topbar-date { display:none; }\n  .main-topbar-user-name { display:inline-block; max-width:130px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; vertical-align:middle; }\n}\n.teacher-workspace { display:none; min-height:82vh; flex-direction:column; background:var(--bg); }\n.teacher-workspace.active { display:flex; }\n.teacher-topbar {\n  background:#fff;\n  border-bottom:1px solid var(--border);\n  padding:0 28px;\n  min-height:64px;\n  display:flex;\n  align-items:center;\n  justify-content:space-between;\n  flex-shrink:0;\n}\n.teacher-topbar-brand { display:flex; align-items:center; gap:14px; }\n.teacher-topbar-brand .t-brand-name {\n  font-family:'Plus Jakarta Sans',sans-serif;\n  font-size:17px;\n  font-weight:800;\n  color:#059669;\n  letter-spacing:-.3px;\n}\n.teacher-topbar-brand .t-teacher-name { font-size:15px; font-weight:700; color:var(--text); line-height:1.2; }\n.teacher-topbar-brand .t-teacher-name:not(:empty) { padding-left:12px; border-left:1px solid var(--border); }\n.teacher-topbar-brand .t-teacher-name-row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-top:1px; }\n.teacher-topbar-brand .t-teacher-classes { font-size:12px; color:var(--muted); font-weight:500; display:flex; flex-wrap:wrap; gap:4px; align-items:center; }\n.teacher-topbar-brand .t-teacher-classes .t-class-chip { background:#D1FAE5; color:#065F46; border-radius:999px; padding:1px 8px; font-size:11.5px; font-weight:600; white-space:nowrap; }\n.teacher-topbar-right { display:flex; align-items:center; gap:14px; }\n.teacher-topbar-date { font-size:13px; color:var(--muted); font-weight:600; background:#F1F5F9; padding:6px 14px; border-radius:20px; }\n.teacher-logout-btn {\n  display:flex; align-items:center; gap:8px;\n  padding:10px 18px; border-radius:8px;\n  font-size:13.5px; font-weight:600;\n  color:var(--muted);\n  border:1px solid var(--border);\n  background:#fff;\n  cursor:pointer;\n  transition:all .15s ease;\n}\n.teacher-logout-btn:hover { color:var(--red); border-color:var(--red); background:var(--red-light); }\n.teacher-content {\n  padding:24px;\n  flex:1;\n  overflow-y:auto;\n  display:grid;\n  grid-template-columns:360px 1fr;\n  gap:20px;\n  align-items:start;\n}\n@media(max-width:860px){\n  .teacher-content { grid-template-columns:1fr; }\n  .teacher-topbar { padding:0 16px; min-height:56px; }\n}\n\n\/* \u2500\u2500 Tablette (\u2264 860px) \u2500\u2500 *\/\n@media(max-width:860px){\n  .login-page{flex-direction:column;}\n  \/* [FIX#16 v4.6] : flex:0 0 560px (valeur desktop) devenait une hauteur fixe\n     de 560px une fois .login-page pass\u00e9 en colonne ci-dessus \u2014 ce qui cr\u00e9ait un\n     grand vide avant\/apr\u00e8s le formulaire selon la hauteur de l'\u00e9cran. *\/\n  .login-left{max-width:100%;padding:56px 24px 36px 24px;flex:1 1 auto;}\n  .login-right{display:none;}\n  .app-workspace{flex-direction:column;}\n  .sidebar{width:100%;border-right:none;border-bottom:1px solid var(--border);}\n  .g4{grid-template-columns:1fr 1fr;}\n  .g21{grid-template-columns:1fr;}\n  .g-devoirs{grid-template-columns:1fr;}\n  .page-title{flex-direction:column;align-items:flex-end;gap:12px;}\n  .page-title > div:first-child{align-self:flex-start;}\n  .page-title > div:last-child{display:flex;flex-direction:column;align-items:flex-end;gap:8px;width:auto;}\n  .page-title .btn{white-space:nowrap;}\n  .ouzoud-toast-wrap { bottom:16px; right:16px; left:16px; }\n  .ouzoud-toast { max-width:100%; }\n  \/* scroll horizontal avec indicateur de fade *\/\n  .tbl-wrap { overflow-x:auto; -webkit-overflow-scrolling:touch; border-radius:8px; }\n  .tbl { min-width:560px; }\n  .tbl th, .tbl td { padding:10px 12px; font-size:12.5px; }\n  [id$=\"-pagination\"] { flex-wrap:wrap !important; }\n  [id$=\"-pagination-btns\"] { flex-wrap:wrap !important; }\n}\n\n\/* \u2500\u2500 Mobile (\u2264 600px) : transformation en cartes \u2500\u2500 *\/\n@media(max-width:600px){\n  \/* [FIX#17 v4.6] : la r\u00e8gle ciblait \".content-area\", une classe absente du HTML\n     (le vrai conteneur est \".main-content-area\") \u2014 le padding r\u00e9duit ne s'appliquait\n     donc jamais sur t\u00e9l\u00e9phone, et s'ajoutait aux 24px de \".card\" pour resserrer\n     inutilement la largeur des cartes. On corrige le s\u00e9lecteur ET on resserre\n     aussi le padding interne des cartes pour gagner de la largeur utile. *\/\n  .main-content-area { padding:14px; }\n  .teacher-content { padding:14px; }\n  .card { padding:16px; }\n  .g4 { grid-template-columns:1fr; }\n\n  \/* Chaque ligne devient une carte empil\u00e9e *\/\n  .tbl-wrap { overflow-x:visible; border-radius:0; }\n  .tbl-wrap::after { display:none; }\n  .tbl,\n  .tbl tbody,\n  .tbl tr,\n  .tbl td { display:block; width:100%; box-sizing:border-box; }\n\n  \/* [FIX38] Mode carte : le min-width:560px du mode tableau (\u2264860px) rognait les cartes\n     (boutons \u00c9diter\/Suppr. coup\u00e9s \u00e0 droite) \u2014 on le neutralise sur t\u00e9l\u00e9phone. *\/\n  .tbl { min-width:0; }\n\n  \/* Masquer le thead \u2014 les labels viennent via data-label en CSS *\/\n  .tbl thead { display:none; }\n\n  \/* Carte par ligne *\/\n  .tbl tbody tr {\n    background:#fff;\n    border:1px solid var(--border);\n    border-radius:12px;\n    margin-bottom:10px;\n    padding:4px 0;\n    box-shadow:var(--s1);\n    overflow:hidden;\n  }\n  .tbl tbody tr:last-child { margin-bottom:0; }\n  .tbl tbody tr:hover td { background:transparent; }\n\n  \/* Cellule = ligne label \/ valeur *\/\n  .tbl td {\n    display:flex;\n    align-items:flex-start;\n    gap:8px;\n    padding:9px 14px;\n    border-bottom:1px solid var(--border);\n    font-size:13px;\n    line-height:1.4;\n  }\n  .tbl td:last-child { border-bottom:none; }\n\n  \/* Label affich\u00e9 avant la valeur via data-label *\/\n  .tbl td::before {\n    content: attr(data-label);\n    flex-shrink:0;\n    width:110px;\n    font-size:10.5px;\n    font-weight:700;\n    text-transform:uppercase;\n    letter-spacing:.04em;\n    color:var(--muted);\n    padding-top:1px;\n  }\n\n  \/* Colonne actions : pas de label, centrer les boutons *\/\n  .tbl td[data-label=\"Actions\"],\n  .tbl td[data-label=\"Action\"] {\n    justify-content:flex-end;\n    flex-wrap:wrap;\n    gap:6px;\n  }\n  .tbl td[data-label=\"Actions\"]::before,\n  .tbl td[data-label=\"Action\"]::before { display:none; }\n\n  \/* Cellule vide \u2192 masquer *\/\n  .tbl td:empty { display:none; }\n  \/* [BULK] Case de s\u00e9lection : ligne \u00ab S\u00e9lectionner \u00bb en t\u00eate de carte *\/\n  .tbl td.bulk-cell { width:100%; padding:9px 14px; text-align:left; }\n  .tbl td.bulk-cell::before { display:none; }\n  .bulk-lbl-txt { display:inline; font-size:12px; font-weight:600; color:var(--muted); }\n  .bulk-cb { width:20px; height:20px; }\n  .bulk-bar { gap:6px; padding:10px; }\n  .bulk-bar .bulk-count { flex:1 0 100%; margin-right:0; }\n  .bulk-bar .btn { flex:1 1 auto; justify-content:center; padding-left:8px; padding-right:8px; }\n  .bulk-bar .bulk-del-btn { flex:1 0 100%; height:40px; }\n\n  \/* [FIX38] Filtres des registres : listes d\u00e9roulantes pleine largeur (libell\u00e9s tronqu\u00e9s sinon) *\/\n  .reg-filters .reg-select { flex:1 1 100%; width:100%; }\n\n\n  \/* [AR] Espaces Parent, Admin et Enseignant : libell\u00e9 arabe sous le libell\u00e9 fran\u00e7ais (attribut data-ar) *\/\n  #ouzoud-parent-app .tbl td[data-ar]::before,\n  #ouzoud-admin-app .tbl td[data-ar]::before,\n  #ouzoud-teacher-app .tbl td[data-ar]::before {\n    content: attr(data-label) \"\\A\" attr(data-ar);\n    white-space:pre-line;\n    letter-spacing:0;\n    line-height:1.5;\n    font-family:'Inter','Cairo',sans-serif;\n  }\n\n  \/* \u00c9tats vides (colspan) : reset mode carte \u2192 ligne centr\u00e9e pleine largeur *\/\n  .tbl td[colspan] {\n    display:block !important;\n    width:100% !important;\n    text-align:center;\n    padding:20px 14px;\n    color:var(--muted);\n    font-style:italic;\n  }\n  .tbl td[colspan]::before { display:none; }\n\n  \/* Badge statut : pleine largeur dans la carte *\/\n  .tbl td .badge { display:inline-flex; }\n\n  \/* Pagination plus compacte *\/\n  [id$=\"-pagination\"] { flex-direction:column; align-items:flex-start !important; gap:8px !important; }\n  [id$=\"-pagination-btns\"] { flex-wrap:wrap !important; }\n\n  \/* Page title boutons en colonne *\/\n  .page-title > div:last-child { flex-direction:column; align-items:stretch !important; width:100% !important; }\n  .page-title > div:last-child .btn { width:100%; justify-content:center; }\n\n  \/* Modal plein \u00e9cran *\/\n  .modal-ov { align-items:flex-end; padding:0; }\n  .modal-box { border-radius:16px 16px 0 0; max-height:90vh; overflow-y:auto; max-width:100% !important; }\n\n  \/* [FIX#16 v4.6] : sur un t\u00e9l\u00e9phone \u00e9troit (\u2264375px), le texte des onglets de\n     connexion (\"Administration\", \"Enseignants\"...) pouvait d\u00e9passer visuellement\n     de son bouton. On resserre la taille et on ajoute une troncature de s\u00e9curit\u00e9. *\/\n  .lf-tabs { gap:3px; }\n  .lf-tab { padding:9px 4px; font-size:10.8px; overflow:hidden; text-overflow:ellipsis; }\n  .lf-tab svg { width:13px; height:13px; }\n\n  \/* [FIX#16 v4.6] : le bandeau de l'espace enseignant n'avait pas de retour \u00e0 la\n     ligne \u2014 sur un petit t\u00e9l\u00e9phone, la date et le bouton de d\u00e9connexion pouvaient\n     chevaucher le nom de la marque. On autorise le retour \u00e0 la ligne et on masque\n     la date (d\u00e9j\u00e0 masqu\u00e9e sur les autres bandeaux \u00e0 cette largeur). *\/\n  .teacher-topbar { flex-wrap:wrap; height:auto; padding:10px 16px; gap:8px; row-gap:8px; }\n  .teacher-topbar-date { display:none; }\n  .teacher-topbar-right { width:100%; justify-content:flex-end; }\n  .main-logout-btn, .teacher-logout-btn { padding:8px 12px; font-size:12.5px; }\n\n  \/* [FIX#18 v4.6] : sur t\u00e9l\u00e9phone, le formulaire \"Publier un nouveau Devoir\"\n     est repositionn\u00e9 apr\u00e8s \"Historique du Cahier de Textes\" (on consulte\n     d'abord, on publie ensuite). Les deux cartes gardent la m\u00eame largeur\n     (100%, comme avant), seul l'ordre d'affichage change. *\/\n  .teacher-content > .card { width:100%; margin:0 auto; }\n  .teacher-content > .card:first-child { order:2; }\n  .teacher-content > .card:last-child { order:1; }\n}\n\n\/* \u2550\u2550 [AR] Interface arabe : police Cairo appliqu\u00e9e \u00e0 la page de connexion et\n   \u00e0 l'espace parent (RTL). L'espace Admin\/Enseignant reste en fran\u00e7ais (LTR),\n   ces r\u00e8gles ne ciblent donc que les blocs marqu\u00e9s dir=\"rtl\". \u2550\u2550 *\/\n#ouzoud-login[dir=\"rtl\"], #ouzoud-login[dir=\"rtl\"] *,\n#ouzoud-parent-app[dir=\"rtl\"], #ouzoud-parent-app[dir=\"rtl\"] * {\n  font-family: 'Cairo', sans-serif !important;\n}\n#ouzoud-login[dir=\"rtl\"] .lf-tabs,\n#ouzoud-parent-app[dir=\"rtl\"] .main-topbar-nav,\n#ouzoud-parent-app[dir=\"rtl\"] .g4,\n#ouzoud-parent-app[dir=\"rtl\"] .g21 { direction: rtl; }\n#ouzoud-login[dir=\"rtl\"] svg,\n#ouzoud-parent-app[dir=\"rtl\"] svg { margin-right:0 !important; margin-left:6px; }\n#ouzoud-login[dir=\"rtl\"] .tbl th, #ouzoud-login[dir=\"rtl\"] .tbl td,\n#ouzoud-parent-app[dir=\"rtl\"] .tbl th, #ouzoud-parent-app[dir=\"rtl\"] .tbl td { text-align:right; }\n#ouzoud-parent-app[dir=\"rtl\"] .kpi, #ouzoud-parent-app[dir=\"rtl\"] .card-header { direction: rtl; }\n<\/style>\n<div class=\"ouzoud-scope\">\n\n  <div class=\"modal-ov\" id=\"m-csv-import\">\n    <div class=\"modal-box\" style=\"max-width:560px;\">\n      <div class=\"modal-header\">\n        <div>\n          <h3>Importer des Parents via CSV<\/h3>\n          <p>Ajout ou mise \u00e0 jour en masse depuis un fichier CSV<\/p>\n        <\/div>\n        <button class=\"modal-close\" onclick=\"closeOuzoudModal('m-csv-import')\">\u00d7<\/button>\n      <\/div>\n      <div style=\"background:var(--blue-light);border:1px solid rgba(59,130,246,0.2);border-radius:10px;padding:14px 16px;margin-bottom:18px;\">\n        <p style=\"font-size:13px;font-weight:600;color:var(--blue-dark);margin-bottom:6px;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px;\"><rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\" ry=\"1\"\/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"\/><path d=\"M12 11h4\"\/><path d=\"M12 16h4\"\/><path d=\"M8 11h.01\"\/><path d=\"M8 16h.01\"\/><\/svg>Format du fichier CSV requis<\/p>\n        <p style=\"font-size:12.5px;color:var(--text-2);line-height:1.7;\">\n          Colonnes : <code style=\"background:#fff;padding:1px 5px;border-radius:4px;font-size:11.5px;\">nom_complet<\/code>, <code style=\"background:#fff;padding:1px 5px;border-radius:4px;font-size:11.5px;\">code_parent<\/code> <span style=\"font-size:11px;color:var(--muted);\">(optionnel)<\/span>, <code style=\"background:#fff;padding:1px 5px;border-radius:4px;font-size:11.5px;\">email<\/code>, <code style=\"background:#fff;padding:1px 5px;border-radius:4px;font-size:11.5px;\">enfants<\/code><br>\n          S\u00e9parateur de colonnes : <b>virgule (,) ou point-virgule (;)<\/b><br>\n          Enfants (m\u00eame cellule) : <b>Prenom Nom (1APG-1)|Prenom2 Nom2 (3APG-3)<\/b><br>\n          Si <code style=\"background:#fff;padding:1px 5px;border-radius:4px;font-size:11.5px;\">code_parent<\/code> est omis, il est <b>g\u00e9n\u00e9r\u00e9 automatiquement<\/b> (OUZ-AAAA-XXXX).<br>\n          Si un code_parent existe d\u00e9j\u00e0, la fiche sera <b>mise \u00e0 jour<\/b> automatiquement.\n        <\/p>\n      <\/div>\n      <div style=\"margin-bottom:16px;\">\n        <button class=\"btn btn-outline btn-sm\" onclick=\"downloadCsvTemplate()\" style=\"width:100%;justify-content:center;\">\n          <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px;\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"\/><polyline points=\"7 10 12 15 17 10\"\/><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"\/><\/svg>T\u00e9l\u00e9charger le fichier mod\u00e8le CSV\n        <\/button>\n      <\/div>\n      <div class=\"form-group\">\n        <label class=\"form-label\">S\u00e9lectionner le fichier CSV *<\/label>\n        <input class=\"form-input\" id=\"csv-file-input\" type=\"file\" accept=\".csv\" style=\"padding:10px;\">\n      <\/div>\n      <div id=\"csv-import-result\" style=\"display:none;margin-bottom:14px;\"><\/div>\n      <div style=\"display:flex;gap:12px;margin-top:20px;\">\n        <button class=\"btn btn-outline\" style=\"flex:1;\" onclick=\"closeOuzoudModal('m-csv-import')\">Annuler<\/button>\n        <button class=\"btn btn-blue\" style=\"flex:1;\" id=\"btn-launch-import\" onclick=\"launchCsvImport()\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px;\"><path d=\"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z\"\/><path d=\"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z\"\/><path d=\"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0\"\/><path d=\"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5\"\/><\/svg>Lancer l'importation<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"modal-ov\" id=\"m-parent-form\">\n    <div class=\"modal-box\">\n      <div class=\"modal-header\">\n        <div>\n          <h3 id=\"mp-title\">Ajouter un Parent<\/h3>\n          <p>Gestion de la fiche d'acc\u00e8s parent<\/p>\n        <\/div>\n        <button class=\"modal-close\" onclick=\"closeOuzoudModal('m-parent-form')\">\u00d7<\/button>\n      <\/div>\n      <input type=\"hidden\" id=\"p-id\" value=\"0\">\n      <input type=\"hidden\" id=\"p-code-parent\" value=\"\">\n      <div class=\"form-group\"><label class=\"form-label\">Nom Complet *<\/label><input class=\"form-input\" id=\"p-nom\" type=\"text\" placeholder=\"Mme\/M. ...\"><\/div>\n      <div class=\"form-group\"><label class=\"form-label\">Email<\/label><input class=\"form-input\" id=\"p-email\" type=\"email\" placeholder=\"parent@email.ma\"><\/div>\n      <div class=\"form-group\">\n        <label class=\"form-label\">Enfants rattach\u00e9s * <span style=\"font-weight:400;color:var(--muted);\">(Format: Nom (Classe), s\u00e9par\u00e9s par virgule)<\/span><\/label>\n        <input class=\"form-input\" id=\"p-enfants\" type=\"text\" placeholder=\"Yassine Benali (3APG-1), Nour Benali (PS-1), Sami Benali (6APG-1)\">\n      <\/div>\n      <div style=\"display:flex;gap:12px;margin-top:20px;\">\n        <button class=\"btn btn-outline\" style=\"flex:1;\" onclick=\"closeOuzoudModal('m-parent-form')\">Annuler<\/button>\n        <button class=\"btn btn-blue\" style=\"flex:1;\" onclick=\"submitParentForm()\">Enregistrer<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- \u2550\u2550 Modal : Modifier \/ justifier une absence \u2550\u2550 -->\n  <div class=\"modal-ov\" id=\"m-abs-edit\">\n    <div class=\"modal-box\" style=\"max-width:460px;\">\n      <div class=\"modal-header\">\n        <div>\n          <h3>Modifier l'absence<\/h3>\n          <p id=\"ae-resume\">\u2014<\/p>\n        <\/div>\n        <button class=\"modal-close\" onclick=\"closeOuzoudModal('m-abs-edit')\">\u00d7<\/button>\n      <\/div>\n      <input type=\"hidden\" id=\"ae-id\" value=\"0\">\n      <div class=\"form-group\">\n        <label class=\"form-label\">Statut<\/label>\n        <select class=\"form-select\" id=\"ae-statut\">\n          <option value=\"unjustified\">Non justifi\u00e9e<\/option>\n          <option value=\"justified\">Justifi\u00e9e<\/option>\n        <\/select>\n      <\/div>\n      <div class=\"form-group\">\n        <label class=\"form-label\">Motif \/ Note interne<\/label>\n        <input class=\"form-input\" id=\"ae-motif\" type=\"text\" maxlength=\"100\" placeholder=\"Ex: Certificat m\u00e9dical d\u00e9pos\u00e9\" onkeydown=\"if(event.key==='Enter'){event.preventDefault();submitAbsenceEdit();}\">\n      <\/div>\n      <div style=\"display:flex;gap:12px;margin-top:20px;\">\n        <button class=\"btn btn-outline\" style=\"flex:1;\" onclick=\"closeOuzoudModal('m-abs-edit')\">Annuler<\/button>\n        <button class=\"btn btn-blue\" style=\"flex:1;\" id=\"ae-submit-btn\" onclick=\"submitAbsenceEdit()\">Enregistrer<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- \u2550\u2550 Modal : Modifier un devoir \u2550\u2550 -->\n  <div class=\"modal-ov\" id=\"m-dev-edit\">\n    <div class=\"modal-box\" style=\"max-width:520px;\">\n      <div class=\"modal-header\">\n        <div>\n          <h3>Modifier le devoir<\/h3>\n          <p id=\"de-resume\">\u2014<\/p>\n        <\/div>\n        <button class=\"modal-close\" onclick=\"closeOuzoudModal('m-dev-edit')\">\u00d7<\/button>\n      <\/div>\n      <input type=\"hidden\" id=\"de-id\" value=\"0\">\n      <div class=\"form-group\">\n        <label class=\"form-label\">Mati\u00e8re *<\/label>\n        <select class=\"form-select\" id=\"de-matiere\" onchange=\"toggleDevoirEditAutre()\"><\/select>\n      <\/div>\n      <div class=\"form-group\" id=\"de-autre-wrap\" style=\"display:none;\">\n        <label class=\"form-label\">Pr\u00e9cisez la mati\u00e8re *<\/label>\n        <input class=\"form-input\" id=\"de-matiere-autre\" type=\"text\" maxlength=\"60\" placeholder=\"Ex: Sciences, Anglais\u2026\">\n      <\/div>\n      <div class=\"form-group\">\n        <label class=\"form-label\">Titre *<\/label>\n        <input class=\"form-input\" id=\"de-titre\" type=\"text\" maxlength=\"255\" placeholder=\"Ex: Lire la le\u00e7on G12\">\n      <\/div>\n      <div class=\"form-group\">\n        <label class=\"form-label\">Consignes<\/label>\n        <textarea class=\"form-textarea\" id=\"de-desc\" rows=\"1\" style=\"min-height:0;\" placeholder=\"D\u00e9tails du travail \u00e0 faire...\"><\/textarea>\n      <\/div>\n      <div class=\"form-group\">\n        <label class=\"form-label\">Date de remise *<\/label>\n        <input class=\"form-input date-left\" id=\"de-date\" type=\"date\">\n      <\/div>\n      <div style=\"display:flex;gap:12px;margin-top:20px;\">\n        <button class=\"btn btn-outline\" style=\"flex:1;\" onclick=\"closeOuzoudModal('m-dev-edit')\">Annuler<\/button>\n        <button class=\"btn btn-blue\" style=\"flex:1;\" id=\"de-submit-btn\" onclick=\"submitDevoirEdit()\">Enregistrer<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- Modal : Modifier une annonce -->\n  <div class=\"modal-ov\" id=\"m-ann-edit\">\n    <div class=\"modal-box\" style=\"max-width:520px;\">\n      <div class=\"modal-header\">\n        <div>\n          <h3>Modifier l'annonce<\/h3>\n          <p id=\"ae-ann-resume\"><\/p>\n        <\/div>\n        <button class=\"modal-close\" onclick=\"closeOuzoudModal('m-ann-edit')\">\u00d7<\/button>\n      <\/div>\n      <input type=\"hidden\" id=\"ae-ann-id\" value=\"0\">\n      <div class=\"form-group\">\n        <label class=\"form-label\">Titre de l'annonce *<\/label>\n        <input class=\"form-input\" id=\"ae-ann-titre\" type=\"text\" maxlength=\"255\" placeholder=\"Objet officiel du message...\">\n      <\/div>\n      <div class=\"form-group\">\n        <label class=\"form-label\">Niveau d'urgence<\/label>\n        <select class=\"form-select\" id=\"ae-ann-priorite\"><option value=\"normale\">Normale (Bleu)<\/option><option value=\"haute\">Haute \/ Urgente (Rouge)<\/option><\/select>\n      <\/div>\n      <div class=\"form-group\">\n        <label class=\"form-label\">Corps du message<\/label>\n        <textarea class=\"form-textarea\" id=\"ae-ann-contenu\" rows=\"4\" style=\"min-height:0;\" placeholder=\"\u00c9crivez le contenu ici...\"><\/textarea>\n      <\/div>\n      <div style=\"display:flex;gap:12px;margin-top:20px;\">\n        <button class=\"btn btn-outline\" style=\"flex:1;\" onclick=\"closeOuzoudModal('m-ann-edit')\">Annuler<\/button>\n        <button class=\"btn btn-blue\" style=\"flex:1;\" id=\"ae-ann-submit-btn\" onclick=\"submitAnnonceEdit()\">Enregistrer<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- \u2550\u2550 Modal : Fiche Enseignant \u2550\u2550 -->\n  <div class=\"modal-ov\" id=\"m-ens-form\">\n    <div class=\"modal-box\" style=\"max-width:560px;\">\n      <div class=\"modal-header\">\n        <div>\n          <h3 id=\"me-title\">Ajouter un Enseignant<\/h3>\n          <p>Gestion de la fiche enseignant<\/p>\n        <\/div>\n        <button class=\"modal-close\" onclick=\"closeOuzoudModal('m-ens-form')\">\u00d7<\/button>\n      <\/div>\n      <input type=\"hidden\" id=\"e-id\" value=\"0\">\n      <div class=\"form-group\"><label class=\"form-label\">Nom Complet *<\/label><input class=\"form-input\" id=\"e-nom\" type=\"text\" placeholder=\"Ex : Mme Fatima Alaoui\"><\/div>\n      <div class=\"form-group\">\n        <label class=\"form-label\">Mati\u00e8re(s) *<\/label>\n        <div id=\"e-matieres-checkboxes\" style=\"display:flex;flex-wrap:wrap;gap:6px 12px;margin-top:6px;max-height:160px;overflow-y:auto;padding:8px;background:#F8FAFC;border:1px solid #E2E8F0;border-radius:8px;\"><\/div>\n        <small style=\"color:var(--muted);font-size:12px;margin-top:4px;display:block;\">Cochez une ou plusieurs mati\u00e8res<\/small>\n      <\/div>\n      <div class=\"form-group\">\n        <label class=\"form-label\">Cat\u00e9gories affich\u00e9es<\/label>\n        <div id=\"e-categories-checkboxes\" style=\"display:flex;flex-wrap:wrap;gap:10px;margin-top:6px;\">\n                      <label style=\"display:flex;align-items:center;gap:5px;font-size:13px;cursor:pointer;\">\n              <input type=\"checkbox\" class=\"e-cat-toggle\" value=\"Pr\u00e9scolaire\" onchange=\"_filterEnsClassesByCategory()\">\n              Pr\u00e9scolaire            <\/label>\n                      <label style=\"display:flex;align-items:center;gap:5px;font-size:13px;cursor:pointer;\">\n              <input type=\"checkbox\" class=\"e-cat-toggle\" value=\"Primaire\" onchange=\"_filterEnsClassesByCategory()\">\n              Primaire            <\/label>\n                  <\/div>\n        <small style=\"color:var(--muted);font-size:12px;margin-top:4px;display:block;\">D\u00e9cochez une cat\u00e9gorie pour masquer ses classes ci-dessous<\/small>\n      <\/div>\n      <div class=\"form-group\">\n        <label class=\"form-label\">Classe(s) assign\u00e9e(s) *<\/label>\n        <div id=\"e-classes-checkboxes\" style=\"display:flex;flex-wrap:wrap;gap:6px 12px;margin-top:6px;max-height:180px;overflow-y:auto;padding:8px;background:#F8FAFC;border:1px solid #E2E8F0;border-radius:8px;\"><\/div>\n        <small style=\"color:var(--muted);font-size:12px;margin-top:4px;display:block;\">Cochez une ou plusieurs classes<\/small>\n      <\/div>\n      <div style=\"display:flex;gap:12px;margin-top:20px;\">\n        <button class=\"btn btn-outline\" style=\"flex:1;\" onclick=\"closeOuzoudModal('m-ens-form')\">Annuler<\/button>\n        <button class=\"btn btn-lg\" style=\"flex:1;background:linear-gradient(135deg,var(--purple),#5B21B6);color:#fff;\" id=\"btn-save-ens\" onclick=\"submitEnsForm()\">Enregistrer<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- \u2550\u2550 Modal : Import CSV Enseignants \u2550\u2550 -->\n  <div class=\"modal-ov\" id=\"m-ens-csv-import\">\n    <div class=\"modal-box\" style=\"max-width:560px;\">\n      <div class=\"modal-header\">\n        <div>\n          <h3>Importer des Enseignants via CSV<\/h3>\n          <p>Ajout ou mise \u00e0 jour en masse depuis un fichier CSV<\/p>\n        <\/div>\n        <button class=\"modal-close\" onclick=\"closeOuzoudModal('m-ens-csv-import')\">\u00d7<\/button>\n      <\/div>\n      <div style=\"background:#EDE9FE;border:1px solid rgba(124,58,237,0.2);border-radius:10px;padding:14px 16px;margin-bottom:18px;\">\n        <p style=\"font-size:13px;font-weight:600;color:var(--purple);margin-bottom:6px;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px;\"><rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\" ry=\"1\"\/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"\/><path d=\"M12 11h4\"\/><path d=\"M12 16h4\"\/><path d=\"M8 11h.01\"\/><path d=\"M8 16h.01\"\/><\/svg>Format du fichier CSV requis<\/p>\n        <p style=\"font-size:12.5px;color:var(--text-2);line-height:1.7;\">\n          Colonnes : <code style=\"background:#fff;padding:1px 5px;border-radius:4px;font-size:11.5px;\">nom_complet<\/code>, <code style=\"background:#fff;padding:1px 5px;border-radius:4px;font-size:11.5px;\">code_enseignant<\/code> <span style=\"font-size:11px;color:var(--muted);\">(optionnel)<\/span>, <code style=\"background:#fff;padding:1px 5px;border-radius:4px;font-size:11.5px;\">matieres<\/code>, <code style=\"background:#fff;padding:1px 5px;border-radius:4px;font-size:11.5px;\">classes<\/code><br>\n          S\u00e9parateur de colonnes : <b>virgule (,) ou point-virgule (;)<\/b><br>\n          Classes (m\u00eame cellule) : <b>1APG-1|3APG-3|4APG-2<\/b><br>\n          Si <code style=\"background:#fff;padding:1px 5px;border-radius:4px;font-size:11.5px;\">code_enseignant<\/code> est omis, il est <b>g\u00e9n\u00e9r\u00e9 automatiquement<\/b> (ENS-AAAA-XXXX).<br>\n          Si un code existe d\u00e9j\u00e0, la fiche sera <b>mise \u00e0 jour<\/b> automatiquement.\n        <\/p>\n      <\/div>\n      <div style=\"margin-bottom:16px;\">\n        <button class=\"btn btn-outline btn-sm\" onclick=\"downloadEnsCsvTemplate()\" style=\"width:100%;justify-content:center;\">\n          <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px;\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"\/><polyline points=\"7 10 12 15 17 10\"\/><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"\/><\/svg>T\u00e9l\u00e9charger le fichier mod\u00e8le CSV\n        <\/button>\n      <\/div>\n      <div class=\"form-group\">\n        <label class=\"form-label\">S\u00e9lectionner le fichier CSV *<\/label>\n        <input class=\"form-input\" id=\"ens-csv-file-input\" type=\"file\" accept=\".csv\" style=\"padding:10px;\">\n      <\/div>\n      <div id=\"ens-csv-import-result\" style=\"display:none;margin-bottom:14px;\"><\/div>\n      <div style=\"display:flex;gap:12px;margin-top:20px;\">\n        <button class=\"btn btn-outline\" style=\"flex:1;\" onclick=\"closeOuzoudModal('m-ens-csv-import')\">Annuler<\/button>\n        <button class=\"btn btn-lg\" style=\"flex:1;background:linear-gradient(135deg,var(--purple),#5B21B6);color:#fff;\" id=\"btn-launch-ens-import\" onclick=\"launchEnsCsvImport()\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px;\"><path d=\"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z\"\/><path d=\"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z\"\/><path d=\"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0\"\/><path d=\"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5\"\/><\/svg>Lancer l'importation<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"login-page\" id=\"ouzoud-login\" dir=\"rtl\" lang=\"ar\">\n    <div class=\"login-left\">\n      <div class=\"bilingual-section\">\n        <div class=\"bilingual-title\">\n          <div class=\"ar\">\u0627\u0644\u0641\u0636\u0627\u0621 \u0627\u0644\u0631\u0642\u0645\u064a<\/div>\n        <\/div>\n      <\/div>\n      <div class=\"bilingual-section\">\n        <div class=\"bilingual-subtitle\">\n          <div class=\"ar\">\u0627\u062e\u062a\u0631 \u0646\u0648\u0639 \u062d\u0633\u0627\u0628\u0643 \u0644\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0627\u0644\u0641\u0636\u0627\u0621 \u0627\u0644\u0645\u062f\u0631\u0633\u064a \u0627\u0644\u062e\u0627\u0635 \u0628\u0627\u0644\u0645\u062a\u0627\u0628\u0639\u0629 \u0648\u0627\u0644\u062a\u0648\u0627\u0635\u0644.<\/div>\n        <\/div>\n      <\/div>\n      <div class=\"lf-group\">\n        <label class=\"lf-label\" for=\"login-profile\">\u0646\u0648\u0639 \u0627\u0644\u062d\u0633\u0627\u0628<\/label>\n        <select class=\"lf-input lf-select\" id=\"login-profile\" onchange=\"toggleLoginTab(this.value)\">\n          <option value=\"parent\" selected>\u0648\u0644\u064a \u0627\u0644\u0623\u0645\u0631<\/option>\n          <option value=\"teacher\">\u0623\u0633\u062a\u0627\u0630(\u0629)<\/option>\n          <option value=\"admin\">\u0627\u0644\u0625\u062f\u0627\u0631\u0629<\/option>\n        <\/select>\n      <\/div>\n\n      <div class=\"lf-group\">\n        <label class=\"lf-label\" id=\"login-secret-label\" for=\"login-secret-field\">\u0631\u0645\u0632 \u062f\u062e\u0648\u0644 \u0648\u0644\u064a \u0627\u0644\u0623\u0645\u0631<\/label>\n        <div class=\"lf-input-wrap\">\n          <input class=\"lf-input\" type=\"password\" id=\"login-secret-field\" placeholder=\"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\" autocomplete=\"new-password\" aria-label=\"\u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631\"\n                 onblur=\"validateParentCodeFormat(this);\" oninput=\"this.style.borderColor='';\"\n                 onkeydown=\"if(event.key==='Enter')executeLogin(currentProfileType)\">\n          <button type=\"button\" class=\"lf-eye\" onclick=\"toggleFieldVisibility('login-secret-field', this)\" tabindex=\"-1\" aria-label=\"\u0625\u0638\u0647\u0627\u0631 \/ \u0625\u062e\u0641\u0627\u0621 \u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631\">\n            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"\/><circle cx=\"12\" cy=\"12\" r=\"3\"\/><\/svg>\n          <\/button>\n        <\/div>\n      <\/div>\n      <button class=\"lf-btn\" id=\"login-submit-btn\" onclick=\"executeLogin(currentProfileType)\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;\"><path d=\"M5 12h14\"\/><path d=\"m12 5 7 7-7 7\"\/><\/svg>\u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0641\u0636\u0627\u0621 \u0648\u0644\u064a \u0627\u0644\u0623\u0645\u0631 \u2192<\/button>\n    <\/div>\n    \n    <div class=\"login-right\">\n      <div class=\"login-right-inner\">\n        <div class=\"bilingual-section\">\n          <div class=\"bilingual-title\">\n            <div class=\"ar\">\u0645\u062a\u0627\u0628\u0639\u0629 \u0645\u062f\u0631\u0633\u064a\u0629 \u0641\u064a \u0627\u0644\u0648\u0642\u062a \u0627\u0644\u0641\u0639\u0644\u064a<\/div>\n          <\/div>\n        <\/div>\n        <div class=\"bilingual-section\">\n          <div class=\"bilingual-subtitle\">\n            <div class=\"ar\">\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u0628\u0646\u0627\u0626\u0643\u0645 \u0641\u064a \u0645\u062a\u0646\u0627\u0648\u0644 \u064a\u062f\u0643\u0645 \u0628\u0646\u0642\u0631\u0629 \u0648\u0627\u062d\u062f\u0629\u060c \u0645\u0646 \u0623\u064a \u0645\u0643\u0627\u0646.<\/div>\n          <\/div>\n        <\/div>\n        \n        <div class=\"lr-feat\">\n          <div class=\"lr-feat-col lr-feat-ar\"><div class=\"lr-feat-icon\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"\/><line x1=\"16\" y1=\"2\" x2=\"16\" y2=\"6\"\/><line x1=\"8\" y1=\"2\" x2=\"8\" y2=\"6\"\/><line x1=\"3\" y1=\"10\" x2=\"21\" y2=\"10\"\/><path d=\"m17 14-5 5\"\/><path d=\"m17 19-5-5\"\/><\/svg><\/div><div class=\"lr-feat-body ar-block-inline\"><div class=\"ar-title\">\u0627\u0644\u063a\u064a\u0627\u0628\u0627\u062a \u0627\u0644\u0645\u0641\u0635\u0644\u0629<\/div><div class=\"ar-subtitle\">\u0645\u062a\u0627\u0628\u0639\u0629 \u062d\u0633\u0628 \u0646\u0635\u0641 \u0627\u0644\u064a\u0648\u0645 \u0645\u0639 \u0625\u0645\u0643\u0627\u0646\u064a\u0629 \u0627\u0644\u062a\u0628\u0631\u064a\u0631<\/div><\/div><\/div>\n        <\/div>\n        <div class=\"lr-feat\">\n          <div class=\"lr-feat-col lr-feat-ar\"><div class=\"lr-feat-icon\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 19.5A2.5 2.5 0 0 1 6.5 17H20\"\/><path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2Z\"\/><path d=\"M9 7h7\"\/><path d=\"M9 11h7\"\/><\/svg><\/div><div class=\"lr-feat-body ar-block-inline\"><div class=\"ar-title\">\u062f\u0641\u062a\u0631 \u0627\u0644\u0646\u0635\u0648\u0635<\/div><div class=\"ar-subtitle\">\u0627\u0644\u0648\u0627\u062c\u0628\u0627\u062a \u0648\u0627\u0644\u062f\u0631\u0648\u0633 \u0627\u0644\u0645\u0628\u0631\u0645\u062c\u0629 \u062d\u0633\u0628 \u0643\u0644 \u0642\u0633\u0645<\/div><\/div><\/div>\n        <\/div>\n        <div class=\"lr-feat\">\n          <div class=\"lr-feat-col lr-feat-ar\"><div class=\"lr-feat-icon\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m3 11 18-5v12L3 14v-3z\"\/><path d=\"M11.6 16.8a3 3 0 1 1-5.8-1.6\"\/><\/svg><\/div><div class=\"lr-feat-body ar-block-inline\"><div class=\"ar-title\">\u0641\u0636\u0627\u0621 \u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a<\/div><div class=\"ar-subtitle\">\u062c\u0645\u064a\u0639 \u0627\u0644\u0645\u0631\u0627\u0633\u0644\u0627\u062a \u0627\u0644\u0631\u0633\u0645\u064a\u0629 \u0627\u0644\u0635\u0627\u062f\u0631\u0629 \u0639\u0646 \u0627\u0644\u0625\u062f\u0627\u0631\u0629<\/div><\/div><\/div>\n        <\/div>\n        <div class=\"lr-feat\">\n          <div class=\"lr-feat-col lr-feat-ar\"><div class=\"lr-feat-icon\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"4\" width=\"20\" height=\"16\" rx=\"2\"\/><path d=\"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7\"\/><\/svg><\/div><div class=\"lr-feat-body ar-block-inline\"><div class=\"ar-title\">\u0625\u0634\u0639\u0627\u0631\u0627\u062a \u0639\u0628\u0631 \u0627\u0644\u0628\u0631\u064a\u062f \u0627\u0644\u0625\u0644\u0643\u062a\u0631\u0648\u0646\u064a<\/div><div class=\"ar-subtitle\">\u062a\u0646\u0628\u064a\u0647\u0627\u062a \u062a\u064f\u0631\u0633\u0644 \u0644\u0623\u0648\u0644\u064a\u0627\u0621 \u0627\u0644\u0623\u0645\u0648\u0631 \u0639\u0646\u062f \u0643\u0644 \u062a\u062d\u062f\u064a\u062b<\/div><\/div><\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- \u2550\u2550 Workspace Parent \u2550\u2550 -->\n  <div class=\"main-workspace\" id=\"ouzoud-parent-app\" dir=\"rtl\" lang=\"ar\">\n    <div class=\"main-topbar\">\n      <div class=\"main-topbar-left\">\n        <div class=\"main-topbar-brand\">\n          <div class=\"m-brand-icon parent-icon\">\n            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"var(--blue-dark)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2\"\/><circle cx=\"9\" cy=\"7\" r=\"4\"\/><path d=\"M23 21v-2a4 4 0 0 0-3-3.87\"\/><path d=\"M16 3.13a4 4 0 0 1 0 7.75\"\/><\/svg>\n          <\/div>\n          <div class=\"m-brand-name parent-brand\">\u0641\u0636\u0627\u0621 \u0648\u0644\u064a \u0627\u0644\u0623\u0645\u0631<\/div>\n          <span class=\"main-topbar-user-name\" id=\"p-user-name\">...<\/span>\n        <\/div>\n        <nav class=\"main-topbar-nav\">\n          <button class=\"main-nav-btn active\" id=\"p-nav-dashboard\" onclick=\"switchMainPane('p-dashboard','parent',this)\">\n            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"7\" height=\"7\"\/><rect x=\"14\" y=\"3\" width=\"7\" height=\"7\"\/><rect x=\"14\" y=\"14\" width=\"7\" height=\"7\"\/><rect x=\"3\" y=\"14\" width=\"7\" height=\"7\"\/><\/svg>\n            <span class=\"nav-txt\">\u0627\u0644\u0645\u0644\u062e\u0635<\/span>\n          <\/button>\n          <button class=\"main-nav-btn\" id=\"p-nav-absences\" onclick=\"switchMainPane('p-absences','parent',this)\">\n            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"\/><polyline points=\"14 2 14 8 20 8\"\/><line x1=\"9\" y1=\"15\" x2=\"15\" y2=\"15\"\/><\/svg>\n            <span class=\"nav-txt\">\u0627\u0644\u063a\u064a\u0627\u0628\u0627\u062a<\/span>\n          <\/button>\n          <button class=\"main-nav-btn\" id=\"p-nav-devoirs\" onclick=\"switchMainPane('p-devoirs','parent',this)\">\n            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z\"\/><path d=\"M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z\"\/><\/svg>\n            <span class=\"nav-txt\">\u062f\u0641\u062a\u0631 \u0627\u0644\u0646\u0635\u0648\u0635<\/span>\n          <\/button>\n          <button class=\"main-nav-btn\" id=\"p-nav-annonces\" onclick=\"switchMainPane('p-annonces','parent',this)\">\n            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 17H2a3 3 0 0 0 3-3V9a7 7 0 0 1 14 0v5a3 3 0 0 0 3 3zm-8.27 4a2 2 0 0 1-3.46 0\"\/><\/svg>\n            <span class=\"nav-txt\">\u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a<\/span>\n          <\/button>\n        <\/nav>\n      <\/div>\n      <div class=\"main-topbar-right\">\n        <div class=\"main-topbar-date\" id=\"p-date-bar\">...<\/div>\n        <button class=\"main-logout-btn\" onclick=\"triggerLogout()\">\n          <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"\/><polyline points=\"16 17 21 12 16 7\"\/><line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\"\/><\/svg>\n          <span class=\"nav-txt\">\u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062e\u0631\u0648\u062c<\/span>\n        <\/button>\n      <\/div>\n    <\/div>\n    <div class=\"main-content-area\">\n      <div class=\"page-pane active\" id=\"p-dashboard\">\n        <div class=\"g4\">\n          <div class=\"kpi\"><div class=\"kpi-icon blue\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 10v6M2 10l10-5 10 5-10 5z\"\/><path d=\"M6 12v5c3 3 9 3 12 0v-5\"\/><\/svg><\/div><div><div class=\"kpi-val\" id=\"dash-kids-count\">0<\/div><div class=\"kpi-label\">\u0627\u0644\u062a\u0644\u0627\u0645\u064a\u0630 \u0627\u0644\u0645\u0633\u062c\u0644\u0648\u0646<\/div><\/div><\/div>\n          <div class=\"kpi\"><div class=\"kpi-icon red\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z\"\/><path d=\"M12 9v4\"\/><path d=\"M12 17h.01\"\/><\/svg><\/div><div><div class=\"kpi-val\" id=\"dash-abs-count\">0<\/div><div class=\"kpi-label\">\u0627\u0644\u063a\u064a\u0627\u0628\u0627\u062a<\/div><\/div><\/div>\n          <div class=\"kpi\"><div class=\"kpi-icon orange\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z\"\/><path d=\"M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z\"\/><\/svg><\/div><div><div class=\"kpi-val\" id=\"dash-devoirs-count\">0<\/div><div class=\"kpi-label\">\u0627\u0644\u0648\u0627\u062c\u0628\u0627\u062a \u0627\u0644\u062d\u0627\u0644\u064a\u0629<\/div><\/div><\/div>\n          <div class=\"kpi\"><div class=\"kpi-icon blue\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m3 11 18-5v12L3 14v-3z\"\/><path d=\"M11.6 16.8a3 3 0 1 1-5.8-1.6\"\/><\/svg><\/div><div><div class=\"kpi-val\" id=\"dash-annonces-count\">0<\/div><div class=\"kpi-label\">\u0625\u0639\u0644\u0627\u0646\u0627\u062a \u0627\u0644\u0645\u062f\u0631\u0633\u0629<\/div><\/div><\/div>\n        <\/div>\n        <div class=\"g21\">\n          <div class=\"card\">\n            <div class=\"card-header\"><h4>\u0622\u062e\u0631 \u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a<\/h4><p>\u0645\u0631\u0627\u0633\u0644\u0627\u062a \u0627\u0644\u0625\u062f\u0627\u0631\u0629<\/p><\/div>\n            <div id=\"p-dash-ann-list\"><\/div>\n          <\/div>\n          <div class=\"card\">\n            <div class=\"card-header\"><h4>\u0623\u0628\u0646\u0627\u0624\u0643\u0645<\/h4><p>\u0627\u0644\u0623\u0642\u0633\u0627\u0645 \u0627\u0644\u0645\u0631\u062a\u0628\u0637\u0629 \u0628\u0647\u0645<\/p><\/div>\n            <div class=\"tbl-wrap\"><table class=\"tbl\"><thead><tr><th><span class=\"th-txt\">\u0627\u0644\u0627\u0633\u0645 \u0627\u0644\u0643\u0627\u0645\u0644<\/span><\/th><th><span class=\"th-txt\">\u0627\u0644\u0642\u0633\u0645<\/span><\/th><\/tr><\/thead><tbody id=\"p-dash-kids-table\"><\/tbody><\/table><\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"page-pane\" id=\"p-absences\">\n        <div class=\"card\">\n          <div class=\"card-header\"><h4>\u0633\u062c\u0644 \u0627\u0644\u063a\u064a\u0627\u0628\u0627\u062a<\/h4><p>\u0645\u062a\u0627\u0628\u0639\u0629 \u062a\u0641\u0635\u064a\u0644\u064a\u0629 \u0644\u0643\u0644 \u063a\u064a\u0627\u0628 \u0644\u0631\u0624\u064a\u0629 \u0623\u0648\u0636\u062d<\/p><\/div>\n          <div id=\"p-absences-pagination\" style=\"display:none;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:nowrap;gap:8px;overflow-x:auto;\">\n            <div id=\"p-absences-pagination-info\" style=\"font-size:13px;color:var(--muted);\"><\/div>\n            <div id=\"p-absences-pagination-btns\" style=\"display:flex;gap:6px;flex-wrap:nowrap;\"><\/div>\n          <\/div>\n          <div class=\"tbl-wrap\"><table class=\"tbl\"><thead><tr><th><span class=\"th-txt\">\u0627\u0644\u062a\u0644\u0645\u064a\u0630<\/span><\/th><th><span class=\"th-txt\">\u0627\u0644\u0642\u0633\u0645<\/span><\/th><th><span class=\"th-txt\">\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u063a\u064a\u0627\u0628<\/span><\/th><th><span class=\"th-txt\">\u0627\u0644\u0641\u062a\u0631\u0629<\/span><\/th><th><span class=\"th-txt\">\u0633\u0628\u0628 \u0627\u0644\u063a\u064a\u0627\u0628<\/span><\/th><th><span class=\"th-txt\">\u0627\u0644\u062d\u0627\u0644\u0629<\/span><\/th><\/tr><\/thead><tbody id=\"p-table-absences\"><\/tbody><\/table><\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"page-pane\" id=\"p-devoirs\">\n        <div class=\"card\">\n          <div class=\"card-header\"><h4>\u062f\u0641\u062a\u0631 \u0627\u0644\u0646\u0635\u0648\u0635<\/h4><p>\u0642\u0627\u0626\u0645\u0629 \u0632\u0645\u0646\u064a\u0629 \u0628\u0627\u0644\u0623\u0639\u0645\u0627\u0644 \u0627\u0644\u0645\u0628\u0631\u0645\u062c\u0629<\/p><\/div>\n          <div id=\"p-devoirs-pagination\" style=\"display:none;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:nowrap;gap:8px;overflow-x:auto;\">\n            <div id=\"p-devoirs-pagination-info\" style=\"font-size:13px;color:var(--muted);\"><\/div>\n            <div id=\"p-devoirs-pagination-btns\" style=\"display:flex;gap:6px;flex-wrap:nowrap;\"><\/div>\n          <\/div>\n          <div class=\"tbl-wrap\"><table class=\"tbl\">\n            <thead><tr>\n              <th style=\"width:15%;\"><span class=\"th-txt\">\u0627\u0644\u0645\u0627\u062f\u0629<\/span><\/th>\n              <th style=\"width:20%;\"><span class=\"th-txt\">\u0627\u0644\u0639\u0645\u0644 \u0627\u0644\u0645\u0637\u0644\u0648\u0628<\/span><\/th>\n              <th style=\"width:40%;\"><span class=\"th-txt\">\u0627\u0644\u062a\u0639\u0644\u064a\u0645\u0627\u062a \u0648\u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644<\/span><\/th>\n              <th style=\"width:10%;\"><span class=\"th-txt\">\u0627\u0644\u0648\u062b\u064a\u0642\u0629<\/span><\/th>\n              <th style=\"width:15%;\"><span class=\"th-txt\">\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0633\u0644\u064a\u0645<\/span><\/th>\n            <\/tr><\/thead>\n            <tbody id=\"p-list-devoirs-table\"><\/tbody>\n          <\/table><\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"page-pane\" id=\"p-annonces\">\n        <div class=\"card\">\n          <div class=\"card-header\"><h4>\u0644\u0648\u062d\u0629 \u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a<\/h4><p>\u0627\u0644\u0633\u062c\u0644 \u0627\u0644\u0643\u0627\u0645\u0644 \u0644\u0644\u0645\u0646\u0634\u0648\u0631\u0627\u062a<\/p><\/div>\n          <div id=\"p-annonces-pagination\" style=\"display:none;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:nowrap;gap:8px;overflow-x:auto;\">\n            <div id=\"p-annonces-pagination-info\" style=\"font-size:13px;color:var(--muted);\"><\/div>\n            <div id=\"p-annonces-pagination-btns\" style=\"display:flex;gap:6px;flex-wrap:nowrap;\"><\/div>\n          <\/div>\n          <div id=\"p-list-annonces\"><\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n  <!-- \u2550\u2550 \/Workspace Parent \u2550\u2550 -->\n\n  <!-- \u2550\u2550 Workspace Admin \u2550\u2550 -->\n  <div class=\"main-workspace\" id=\"ouzoud-admin-app\">\n    <div class=\"main-topbar\">\n      <div class=\"main-topbar-left\">\n        <div class=\"main-topbar-brand\">\n          <div class=\"m-brand-icon admin-icon\">\n            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"var(--purple)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\" ry=\"2\"\/><path d=\"M7 11V7a5 5 0 0 1 10 0v4\"\/><\/svg>\n          <\/div>\n          <div class=\"m-brand-name admin-brand\">ADMINISTRATION<\/div>\n        <\/div>\n        <nav class=\"main-topbar-nav\">\n          <button class=\"main-nav-btn admin-nav active\" id=\"a-nav-parents\" onclick=\"switchMainPane('a-parents','admin',this)\">\n            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2\"\/><circle cx=\"9\" cy=\"7\" r=\"4\"\/><path d=\"M23 21v-2a4 4 0 0 0-3-3.87\"\/><path d=\"M16 3.13a4 4 0 0 1 0 7.75\"\/><\/svg>\n            <span class=\"nav-txt\">Parents<\/span>\n          <\/button>\n          <button class=\"main-nav-btn admin-nav\" id=\"a-nav-enseignants\" onclick=\"switchMainPane('a-enseignants','admin',this)\">\n            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z\"\/><path d=\"M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z\"\/><\/svg>\n            <span class=\"nav-txt\">Enseignants<\/span>\n          <\/button>\n          <button class=\"main-nav-btn admin-nav\" id=\"a-nav-absences\" onclick=\"switchMainPane('a-absences','admin',this)\">\n            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"\/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"\/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"\/><\/svg>\n            <span class=\"nav-txt\">Absences<\/span>\n          <\/button>\n          <button class=\"main-nav-btn admin-nav\" id=\"a-nav-devoirs\" onclick=\"switchMainPane('a-devoirs','admin',this)\">\n            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 19.5A2.5 2.5 0 0 1 6.5 17H20\"\/><path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z\"\/><\/svg>\n            <span class=\"nav-txt\">Devoirs<\/span>\n          <\/button>\n          <button class=\"main-nav-btn admin-nav\" id=\"a-nav-annonces\" onclick=\"switchMainPane('a-annonces','admin',this)\">\n            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 17H2a3 3 0 0 0 3-3V9a7 7 0 0 1 14 0v5a3 3 0 0 0 3 3zm-8.27 4a2 2 0 0 1-3.46 0\"\/><\/svg>\n            <span class=\"nav-txt\">Annonces<\/span>\n          <\/button>\n        <\/nav>\n      <\/div>\n      <div class=\"main-topbar-right\">\n        <div class=\"main-topbar-date\" id=\"a-date-bar\">...<\/div>\n        <button class=\"main-logout-btn\" onclick=\"triggerLogout()\">\n          <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"\/><polyline points=\"16 17 21 12 16 7\"\/><line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\"\/><\/svg>\n          <span class=\"nav-txt\">D\u00e9connexion<\/span>\n        <\/button>\n      <\/div>\n    <\/div>\n    <div class=\"main-content-area\">\n      <div class=\"page-pane active\" id=\"a-parents\">\n        <div class=\"page-title\">\n          <div><h4>Base de Donn\u00e9es Parents<\/h4><\/div>\n          <div style=\"display:flex;gap:10px;\">\n            <button class=\"btn btn-outline btn-lg\" onclick=\"exportParentsCsv()\" style=\"color:var(--blue-dark);border-color:var(--blue);\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px;\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"\/><polyline points=\"7 10 12 15 17 10\"\/><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"\/><\/svg>T\u00e9l\u00e9charger CSV<\/button>\n            <button class=\"btn btn-outline btn-lg\" onclick=\"openCsvImportModal()\" style=\"color:var(--blue-dark);border-color:var(--blue);\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px;\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"\/><polyline points=\"17 8 12 3 7 8\"\/><line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"\/><\/svg>Importer CSV<\/button>\n            <button class=\"btn btn-blue btn-lg\" onclick=\"openParentModal(0)\">+ Ajouter un Parent<\/button>\n          <\/div>\n        <\/div>\n        <div class=\"card\">\n          <div style=\"margin-bottom:16px;\">\n            <div style=\"position:relative;max-width:420px;\">\n              <span style=\"position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--muted);pointer-events:none;display:flex;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"\/><path d=\"m21 21-4.3-4.3\"\/><\/svg><\/span>\n              <input class=\"form-input\" id=\"parents-search-input\" type=\"text\" placeholder=\"Rechercher par nom, code parent ou email...\" oninput=\"filterParentsTable(this.value)\" style=\"padding-left:42px;height:44px;font-size:14px;\">\n            <\/div>\n          <\/div>\n          <div id=\"parents-pagination\" style=\"display:none;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:nowrap;gap:8px;overflow-x:auto;\">\n            <div id=\"parents-pagination-info\" style=\"font-size:13px;color:var(--muted);\"><\/div>\n            <div id=\"parents-pagination-btns\" style=\"display:flex;gap:6px;flex-wrap:nowrap;\"><\/div>\n          <\/div>\n          <div class=\"tbl-wrap\"><table class=\"tbl\"><colgroup><col style=\"width:18%\"><col style=\"width:18%\"><col style=\"width:22%\"><col style=\"width:20%\"><col style=\"width:22%\"><\/colgroup><thead><tr><th><span class=\"th-txt\">Parent<\/span><\/th><th><span class=\"th-txt\">Code Parent<\/span><\/th><th><span class=\"th-txt\">Email<\/span><\/th><th><span class=\"th-txt\">Enfants Rattach\u00e9s<\/span><\/th><th><span class=\"th-txt\">Actions<\/span><\/th><\/tr><\/thead><tbody id=\"a-table-parents\"><\/tbody><\/table><\/div>\n          <div id=\"parents-search-empty\" style=\"display:none;text-align:center;padding:28px;color:var(--muted);font-size:14px;\">Aucun parent ne correspond \u00e0 la recherche.<\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"page-pane\" id=\"a-absences\">\n        <div class=\"g-devoirs\">\n          <!-- Colonne gauche : formulaire -->\n          <div class=\"card\">\n            <div class=\"card-header\"><h4>D\u00e9clarer une Absence<\/h4><\/div>\n            <div class=\"form-stack-full\">\n              <div class=\"form-group\">\n                <label class=\"form-label\">Recherche par \u00e9l\u00e8ve *<\/label>\n                <div class=\"eleve-search-wrap\">\n                  <span class=\"eleve-search-icon\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"\/><path d=\"m21 21-4.3-4.3\"\/><\/svg><\/span>\n                  <input class=\"form-input\" id=\"abs-eleve-search\" type=\"text\" placeholder=\"Tapez le nom d'un \u00e9l\u00e8ve\u2026\" autocomplete=\"off\"\n                         oninput=\"filterAbsenceEleveSearch()\" onfocus=\"filterAbsenceEleveSearch()\" onkeydown=\"absEleveSearchKeydown(event)\">\n                  <div class=\"eleve-search-results\" id=\"abs-eleve-search-results\"><\/div>\n                <\/div>\n              <\/div>\n              <div style=\"display:none;\" aria-hidden=\"true\">\n              <div class=\"form-group\">\n                <label class=\"form-label\">Niveau *<\/label>\n                <select class=\"form-select\" id=\"abs-niveau\" onchange=\"updateAbsenceClasseDropdown()\">\n                  <option value=\"\" disabled selected>-- Choisir le niveau --<\/option>\n                                      <optgroup label=\"Pr\u00e9scolaire\">\n                                              <option value=\"PS\">PS<\/option>\n                                              <option value=\"MS\">MS<\/option>\n                                              <option value=\"GS\">GS<\/option>\n                                          <\/optgroup>\n                                      <optgroup label=\"Primaire\">\n                                              <option value=\"1APG\">1APG<\/option>\n                                              <option value=\"2APG\">2APG<\/option>\n                                              <option value=\"3APG\">3APG<\/option>\n                                              <option value=\"4APG\">4APG<\/option>\n                                              <option value=\"5APG\">5APG<\/option>\n                                              <option value=\"6APG\">6APG<\/option>\n                                          <\/optgroup>\n                                  <\/select>\n              <\/div>\n              <div class=\"form-group\">\n                <label class=\"form-label\">Classe *<\/label>\n                <select class=\"form-select\" id=\"abs-classe\" onchange=\"updateAbsencesStudentsDropdown()\" disabled>\n                  <option value=\"\" disabled selected>S\u00e9lectionnez d'abord un niveau<\/option>\n                <\/select>\n              <\/div>\n\n              <div class=\"form-group\" id=\"abs-nom-single-wrap\">\n                <label class=\"form-label\">Nom de l'\u00c9l\u00e8ve *<\/label>\n                <select class=\"form-select\" id=\"abs-nom\" disabled>\n                  <option value=\"\" disabled selected>S\u00e9lectionnez d'abord une classe<\/option>\n                <\/select>\n              <\/div>\n\n              <\/div>\n              <div class=\"form-group abs-mode-toggle-group\" id=\"abs-same-class-wrap\" style=\"display:none;\">\n                <label class=\"abs-mode-toggle\">\n                  <input type=\"checkbox\" id=\"abs-multi-toggle\" onchange=\"onAbsSameClassToggle()\">\n                  <span id=\"abs-same-class-label\">S\u00e9lectionner d'autres \u00e9l\u00e8ves de la m\u00eame classe<\/span>\n                <\/label>\n              <\/div>\n\n              <div class=\"form-group\" id=\"abs-nom-multi-wrap\" style=\"display:none;\">\n                <div class=\"abs-multi-list\" id=\"abs-multi-list\"><\/div>\n              <\/div>\n\n              <div class=\"form-group\">\n                <label class=\"form-label\" id=\"abs-date-label\">Date<\/label>\n                <input class=\"form-input date-left\" id=\"abs-date\" type=\"date\" onchange=\"updateAbsenceRangeSummary()\">\n              <\/div>\n              <div class=\"form-group abs-mode-toggle-group\">\n                <label class=\"abs-mode-toggle\">\n                  <input type=\"checkbox\" id=\"abs-range-toggle\" onchange=\"toggleAbsenceRange()\">\n                  <span>Absence sur plusieurs jours (plage de dates)<\/span>\n                <\/label>\n              <\/div>\n              <div id=\"abs-range-wrap\" style=\"display:none;\">\n                <div class=\"form-group\">\n                  <label class=\"form-label\">Au (inclus) *<\/label>\n                  <input class=\"form-input date-left\" id=\"abs-date-fin\" type=\"date\" onchange=\"updateAbsenceRangeSummary()\">\n                <\/div>\n                <div class=\"form-group abs-mode-toggle-group\">\n                  <label class=\"abs-mode-toggle\">\n                    <input type=\"checkbox\" id=\"abs-range-noweekend\" checked onchange=\"updateAbsenceRangeSummary()\">\n                    <span>Exclure le samedi et le dimanche<\/span>\n                  <\/label>\n                  <span class=\"eleve-search-hint\" id=\"abs-range-summary\"><\/span>\n                <\/div>\n              <\/div>\n              <div class=\"form-group\">\n                <label class=\"form-label\">P\u00e9riode<\/label>\n                <select class=\"form-select\" id=\"abs-periode\">\n                  <option value=\"matin\">Matin<\/option>\n                  <option value=\"apr\u00e8s-midi\">Apr\u00e8s-midi<\/option>\n                  <option value=\"journ\u00e9e\">Journ\u00e9e compl\u00e8te<\/option>\n                <\/select>\n              <\/div>\n              <div class=\"form-group\">\n                <label class=\"form-label\">Statut<\/label>\n                <select class=\"form-select\" id=\"abs-statut\">\n                  <option value=\"unjustified\">Non justifi\u00e9e<\/option>\n                  <option value=\"justified\">Justifi\u00e9e<\/option>\n                <\/select>\n              <\/div>\n              <div class=\"form-group\">\n                <label class=\"form-label\">Motif \/ Note interne<\/label>\n                <input class=\"form-input\" id=\"abs-motif\" type=\"text\" maxlength=\"100\" placeholder=\"Ex: Certificat m\u00e9dical d\u00e9pos\u00e9\">\n              <\/div>\n            <\/div>\n            <button class=\"btn btn-blue\" style=\"width:100%;margin-top:6px;\" id=\"abs-submit-btn\" onclick=\"publishAbsence()\">Enregistrer l'absence<\/button>\n          <\/div>\n\n          <!-- Colonne droite : historique -->\n          <div class=\"card\">\n            <div class=\"card-header\"><h4>Registre des Absences<\/h4><p>Historique des absences enregistr\u00e9es<\/p><\/div>\n            <div class=\"reg-filters\" id=\"abs-filters\">\n              <div class=\"reg-search\"><span class=\"reg-ico\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"\/><path d=\"m21 21-4.3-4.3\"\/><\/svg><\/span><input class=\"form-input\" id=\"abs-f-search\" type=\"text\" placeholder=\"Rechercher un \u00e9l\u00e8ve ou un motif...\" oninput=\"onRegistreFilterChange('abs')\" autocomplete=\"off\"><\/div>\n              <select class=\"form-select reg-select\" id=\"abs-f-classe\" onchange=\"onRegistreFilterChange('abs')\" title=\"Classe\"><option value=\"\">Toutes les classes<\/option><\/select>\n              <select class=\"form-select reg-select\" id=\"abs-f-statut\" onchange=\"onRegistreFilterChange('abs')\" title=\"Statut\"><option value=\"\">Tous les statuts<\/option><option value=\"justified\">Justifi\u00e9e<\/option><option value=\"unjustified\">Non justifi\u00e9e<\/option><\/select>\n              <select class=\"form-select reg-select\" id=\"abs-f-periode\" onchange=\"onRegistreFilterChange('abs')\" title=\"P\u00e9riode\"><option value=\"\">Toutes p\u00e9riodes<\/option><option value=\"matin\">Matin<\/option><option value=\"apres-midi\">Apr\u00e8s-midi<\/option><option value=\"journee\">Journ\u00e9e compl\u00e8te<\/option><\/select>\n              <button type=\"button\" class=\"btn btn-outline btn-sm\" id=\"abs-f-reset\" onclick=\"resetRegistreFilters('abs')\" style=\"display:none;\">R\u00e9initialiser<\/button>\n            <\/div>\n            <div class=\"reg-count\" id=\"abs-f-count\" aria-live=\"polite\"><\/div>\n            <div id=\"absences-pagination\" style=\"display:none;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:nowrap;gap:8px;overflow-x:auto;\">\n              <div id=\"absences-pagination-info\" style=\"font-size:13px;color:var(--muted);\"><\/div>\n              <div id=\"absences-pagination-btns\" style=\"display:flex;gap:6px;flex-wrap:nowrap;\"><\/div>\n            <\/div>\n            <div class=\"tbl-wrap\"><table class=\"tbl\"><colgroup><col style=\"width:18%\"><col style=\"width:12%\"><col style=\"width:17%\"><col style=\"width:13%\"><col style=\"width:18%\"><col style=\"width:22%\"><\/colgroup><thead><tr><th><span class=\"th-txt\">\u00c9l\u00e8ve<\/span><\/th><th><span class=\"th-txt\">Classe<\/span><\/th><th><span class=\"th-txt\">Date<\/span><\/th><th><span class=\"th-txt\">P\u00e9riode<\/span><\/th><th><span class=\"th-txt\">Statut<\/span><\/th><th><span class=\"th-txt\">Action<\/span><\/th><\/tr><\/thead><tbody id=\"a-table-absences\"><\/tbody><\/table><\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"page-pane\" id=\"a-devoirs\">\n        <div class=\"g-devoirs\">\n          <!-- Colonne gauche : formulaire -->\n          <div class=\"card\">\n            <div class=\"card-header\"><h4>Publier un nouveau Devoir<\/h4><\/div>\n            <div class=\"form-stack-full\">\n              <div class=\"form-group\">\n                <label class=\"form-label\">Classe *<\/label>\n                <div class=\"eleve-search-wrap\">\n                  <span class=\"eleve-search-icon\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"\/><path d=\"m21 21-4.3-4.3\"\/><\/svg><\/span>\n                  <input class=\"form-input\" id=\"a-dev-classe-search\" type=\"text\" placeholder=\"Tapez le nom de la classe\u2026\" autocomplete=\"off\"\n                         oninput=\"filterClasseSearch('a-dev')\" onfocus=\"filterClasseSearch('a-dev')\" onkeydown=\"classeSearchKeydown(event,'a-dev')\">\n                  <div class=\"eleve-search-results\" id=\"a-dev-classe-search-results\"><\/div>\n                <\/div>\n              <\/div>\n              <div style=\"display:none;\" aria-hidden=\"true\">\n              <div class=\"form-group\">\n                <label class=\"form-label\">Niveau *<\/label>\n                <select class=\"form-select\" id=\"a-dev-niveau\" onchange=\"updateADevoirClasseDropdown()\">\n                  <option value=\"\" disabled selected>-- Choisir le niveau --<\/option>\n                                      <optgroup label=\"Pr\u00e9scolaire\">\n                                              <option value=\"PS\">PS<\/option>\n                                              <option value=\"MS\">MS<\/option>\n                                              <option value=\"GS\">GS<\/option>\n                                          <\/optgroup>\n                                      <optgroup label=\"Primaire\">\n                                              <option value=\"1APG\">1APG<\/option>\n                                              <option value=\"2APG\">2APG<\/option>\n                                              <option value=\"3APG\">3APG<\/option>\n                                              <option value=\"4APG\">4APG<\/option>\n                                              <option value=\"5APG\">5APG<\/option>\n                                              <option value=\"6APG\">6APG<\/option>\n                                          <\/optgroup>\n                                  <\/select>\n              <\/div>\n              <div class=\"form-group\">\n                <label class=\"form-label\">Classe *<\/label>\n                <select class=\"form-select\" id=\"a-dev-classe\" disabled>\n                  <option value=\"\" disabled selected>S\u00e9lectionnez d'abord un niveau<\/option>\n                <\/select>\n              <\/div>\n              <\/div>\n              <div class=\"form-group\">\n                <label class=\"form-label\">Mati\u00e8re *<\/label>\n                <select class=\"form-select\" id=\"a-dev-matiere\">\n                  <option value=\"\" disabled selected>-- Choisir la mati\u00e8re --<\/option>\n                <\/select>\n              <\/div>\n            <\/div>\n            <div class=\"form-group\" id=\"a-dev-autre-wrap\" style=\"display:none;\">\n              <label class=\"form-label\">Pr\u00e9cisez la mati\u00e8re *<\/label>\n              <input class=\"form-input\" id=\"a-dev-matiere-autre\" type=\"text\" placeholder=\"Ex: Sciences, Anglais\u2026\">\n            <\/div>\n            <div class=\"form-group\" id=\"a-dev-all-wrap\" style=\"display:none;\">\n              <label class=\"form-label\" style=\"display:flex;align-items:center;gap:8px;cursor:pointer;\">\n                <input type=\"checkbox\" id=\"a-dev-all-classes\" style=\"width:16px;height:16px;accent-color:var(--blue);flex-shrink:0;\" onchange=\"toggleADevAllClasses()\">\n                <span id=\"a-dev-all-label\">Envoyer \u00e0 toutes les classes de ce niveau<\/span>\n              <\/label>\n            <\/div>\n            <div class=\"form-group\">\n              <label class=\"form-label\">Titre du devoir *<\/label>\n              <input class=\"form-input\" id=\"a-dev-titre\" type=\"text\" value=\"Devoir maison\" placeholder=\"Ex: Lire la le\u00e7on G12\">\n            <\/div>\n            <div class=\"form-group\">\n              <label class=\"form-label\">Fichier joint <span style=\"font-weight:400;color:var(--muted);\">(Image, TXT ou PDF - Max 2 Mo)<\/span><\/label>\n              <input class=\"form-input\" id=\"a-dev-fichier\" type=\"file\" accept=\".jpg,.jpeg,.png,.txt,.pdf\" style=\"padding:10px;\">\n            <\/div>\n            <div class=\"form-group\">\n              <label class=\"form-label\">Description \/ Consignes<\/label>\n              <textarea class=\"form-textarea\" id=\"a-dev-desc\" rows=\"1\" style=\"min-height:0;\" placeholder=\"D\u00e9tails du travail \u00e0 faire...\"><\/textarea>\n            <\/div>\n            <div class=\"form-group\">\n              <label class=\"form-label\">Date de remise requise<\/label>\n              <input class=\"form-input date-left\" id=\"a-dev-date\" type=\"date\">\n            <\/div>\n            <button class=\"btn btn-blue\" style=\"width:100%;margin-top:6px;background:linear-gradient(135deg,#10B981,#059669);\" onclick=\"publishADevoir()\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px;\"><path d=\"m22 2-7 20-4-9-9-4Z\"\/><path d=\"M22 2 11 13\"\/><\/svg>Diffuser le devoir<\/button>\n          <\/div>\n\n          <!-- Colonne droite : historique -->\n          <div class=\"card\">\n            <div class=\"card-header\"><h4>Historique du Cahier de Textes<\/h4><p>Devoirs publi\u00e9s par l'administration et les enseignants<\/p><\/div>\n            <div class=\"reg-filters\" id=\"dev-filters\">\n              <div class=\"reg-search\"><span class=\"reg-ico\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"\/><path d=\"m21 21-4.3-4.3\"\/><\/svg><\/span><input class=\"form-input\" id=\"dev-f-search\" type=\"text\" placeholder=\"Rechercher un devoir (titre, mati\u00e8re, classe, enseignant)...\" oninput=\"onRegistreFilterChange('dev')\" autocomplete=\"off\"><\/div>\n              <button type=\"button\" class=\"btn btn-outline btn-sm\" id=\"dev-f-reset\" onclick=\"resetRegistreFilters('dev')\" style=\"display:none;\">R\u00e9initialiser<\/button>\n            <\/div>\n            <div class=\"reg-count\" id=\"dev-f-count\" aria-live=\"polite\"><\/div>\n            <div id=\"devoirs-pagination\" style=\"display:none;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:nowrap;gap:8px;overflow-x:auto;\">\n              <div id=\"devoirs-pagination-info\" style=\"font-size:13px;color:var(--muted);\"><\/div>\n              <div id=\"devoirs-pagination-btns\" style=\"display:flex;gap:6px;flex-wrap:nowrap;\"><\/div>\n            <\/div>\n            <div class=\"tbl-wrap\">\n              <table class=\"tbl\">\n                <colgroup><col style=\"width:8%\"><col style=\"width:12%\"><col style=\"width:20%\"><col style=\"width:19%\"><col style=\"width:14%\"><col style=\"width:12%\"><col style=\"width:15%\"><\/colgroup>\n                <thead><tr><th><span class=\"th-txt\">Classe<\/span><\/th><th><span class=\"th-txt\">Mati\u00e8re<\/span><\/th><th><span class=\"th-txt\">Titre<\/span><\/th><th><span class=\"th-txt\">Publi\u00e9 par<\/span><\/th><th><span class=\"th-txt\">Date Butoir<\/span><\/th><th><span class=\"th-txt\">Document<\/span><\/th><th><span class=\"th-txt\">Action<\/span><\/th><\/tr><\/thead>\n                <tbody id=\"a-table-devoirs\"><\/tbody>\n              <\/table>\n            <\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"page-pane\" id=\"a-annonces\">\n        <div class=\"g-devoirs\">\n          <!-- Colonne gauche : formulaire -->\n          <div class=\"card\">\n            <div class=\"card-header\"><h4>R\u00e9diger un communiqu\u00e9<\/h4><\/div>\n            <div class=\"form-group\"><label class=\"form-label\">Titre de l'annonce *<\/label><input class=\"form-input\" id=\"ann-titre\" type=\"text\" placeholder=\"Objet officiel du message...\"><\/div>\n            <div class=\"form-group\"><label class=\"form-label\">Niveau d'urgence<\/label><select class=\"form-select\" id=\"ann-priorite\"><option value=\"normale\">Normale (Bleu)<\/option><option value=\"haute\">Haute \/ Urgente (Rouge)<\/option><\/select><\/div>\n            <div class=\"form-group\">\n              <label class=\"form-label\">Fichier joint <span style=\"font-weight:400;color:var(--muted);\">(Image, TXT ou PDF - Max 2 Mo)<\/span><\/label>\n              <input class=\"form-input\" id=\"ann-fichier\" type=\"file\" accept=\".jpg,.jpeg,.png,.txt,.pdf\" style=\"padding:10px;\">\n            <\/div>\n            <div class=\"form-group\"><label class=\"form-label\">Corps du message<\/label><textarea class=\"form-textarea\" id=\"ann-contenu\" rows=\"2\" style=\"min-height:0;\" placeholder=\"\u00c9crivez le contenu ici...\"><\/textarea><\/div>\n            <button class=\"btn btn-blue\" style=\"width:100%;margin-top:6px;\" onclick=\"publishAnnonce()\">Lancer la publication<\/button>\n          <\/div>\n\n          <!-- Colonne droite : historique -->\n          <div class=\"card\">\n            <div class=\"card-header\"><h4>Registre des Annonces Diffus\u00e9es<\/h4><p>Annonces publi\u00e9es par l'administration<\/p><\/div>\n            <div id=\"annonces-pagination\" style=\"display:none;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:nowrap;gap:8px;overflow-x:auto;\">\n              <div id=\"annonces-pagination-info\" style=\"font-size:13px;color:var(--muted);\"><\/div>\n              <div id=\"annonces-pagination-btns\" style=\"display:flex;gap:6px;flex-wrap:nowrap;\"><\/div>\n            <\/div>\n            <div class=\"tbl-wrap\"><table class=\"tbl\"><colgroup><col style=\"width:38%\"><col style=\"width:14%\"><col style=\"width:24%\"><col style=\"width:24%\"><\/colgroup><thead><tr><th><span class=\"th-txt\">Titre<\/span><\/th><th><span class=\"th-txt\">Priorit\u00e9<\/span><\/th><th><span class=\"th-txt\">Document<\/span><\/th><th><span class=\"th-txt\">Action<\/span><\/th><\/tr><\/thead><tbody id=\"a-table-annonces\"><\/tbody><\/table><\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n\n      <!-- \u2500\u2500 Onglet Enseignants \u2500\u2500 -->\n      <div class=\"page-pane\" id=\"a-enseignants\">\n        <div class=\"page-title\">\n          <div><h4>Corps Enseignant<\/h4><\/div>\n          <div style=\"display:flex;gap:10px;\">\n            <button class=\"btn btn-outline btn-lg\" onclick=\"exportEnseignantsCsv()\" style=\"color:var(--purple);border-color:var(--purple);\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px;\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"\/><polyline points=\"7 10 12 15 17 10\"\/><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"\/><\/svg>T\u00e9l\u00e9charger CSV<\/button>\n            <button class=\"btn btn-outline btn-lg\" onclick=\"openEnsImportModal()\" style=\"color:var(--purple);border-color:var(--purple);\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px;\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"\/><polyline points=\"17 8 12 3 7 8\"\/><line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"\/><\/svg>Importer CSV<\/button>\n            <button class=\"btn btn-lg\" style=\"background:linear-gradient(135deg,var(--purple),#5B21B6);color:#fff;box-shadow:0 4px 14px rgba(124,58,237,.25);\" onclick=\"openEnsModal(0)\">+ Ajouter un Enseignant<\/button>\n          <\/div>\n        <\/div>\n        <div class=\"card\">\n          <div style=\"margin-bottom:16px;\">\n            <div style=\"position:relative;max-width:420px;\">\n              <span style=\"position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--muted);pointer-events:none;display:flex;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"\/><path d=\"m21 21-4.3-4.3\"\/><\/svg><\/span>\n              <input class=\"form-input\" id=\"ens-search-input\" type=\"text\" placeholder=\"Rechercher par nom, code, mati\u00e8re ou classe...\" oninput=\"filterEnseignantsTable(this.value)\" style=\"padding-left:42px;height:44px;font-size:14px;\">\n            <\/div>\n          <\/div>\n          <div id=\"ens-pagination\" style=\"display:none;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:nowrap;gap:8px;overflow-x:auto;\">\n            <div id=\"ens-pagination-info\" style=\"font-size:13px;color:var(--muted);\"><\/div>\n            <div id=\"ens-pagination-btns\" style=\"display:flex;gap:6px;flex-wrap:nowrap;\"><\/div>\n          <\/div>\n          <div class=\"tbl-wrap\">\n            <table class=\"tbl\">\n              <colgroup><col style=\"width:16%\"><col style=\"width:14%\"><col style=\"width:25%\"><col style=\"width:27%\"><col style=\"width:18%\"><\/colgroup>\n              <thead><tr><th><span class=\"th-txt\">Enseignant<\/span><\/th><th><span class=\"th-txt\">Code<\/span><\/th><th><span class=\"th-txt\">Mati\u00e8re(s)<\/span><\/th><th><span class=\"th-txt\">Classe(s) assign\u00e9e(s)<\/span><\/th><th><span class=\"th-txt\">Actions<\/span><\/th><\/tr><\/thead>\n              <tbody id=\"a-table-enseignants\"><\/tbody>\n            <\/table>\n          <\/div>\n          <div id=\"ens-search-empty\" style=\"display:none;text-align:center;padding:28px;color:var(--muted);font-size:14px;\">Aucun enseignant ne correspond \u00e0 la recherche.<\/div>\n        <\/div>\n      <\/div>\n\n    <\/div>\n  <\/div>\n  <!-- \u2550\u2550 \/Workspace Admin \u2550\u2550 -->\n  <!-- \u2550\u2550 Workspace d\u00e9di\u00e9 Enseignant \u2550\u2550 -->\n  <div class=\"teacher-workspace\" id=\"ouzoud-teacher-app\">\n    <div class=\"teacher-topbar\">\n      <div class=\"teacher-topbar-brand\">\n        <div style=\"width:36px;height:36px;border-radius:10px;background:#10B981;border:1px solid #E5E7EB;display:flex;align-items:center;justify-content:center;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#fff\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z\"\/><path d=\"M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z\"\/><\/svg><\/div>\n        <div>\n          <div class=\"t-teacher-name-row\">\n            <div class=\"t-brand-name\">ESPACE ENSEIGNANT<\/div>\n            <div class=\"t-teacher-name\" id=\"teacher-name-bar\"><\/div>\n            <div class=\"t-teacher-classes\" id=\"teacher-classes-bar\"><\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n      <div class=\"teacher-topbar-right\">\n        <div class=\"teacher-topbar-date\" id=\"teacher-date-bar\">...<\/div>\n        <button class=\"teacher-logout-btn\" onclick=\"triggerLogout()\">\n          <span><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;\"><path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"\/><polyline points=\"16 17 21 12 16 7\"\/><line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\"\/><\/svg><\/span> <span class=\"nav-txt\">D\u00e9connexion<\/span>\n        <\/button>\n      <\/div>\n    <\/div>\n\n    <div class=\"teacher-content\">\n      <!-- Colonne gauche : formulaire -->\n      <div class=\"card\">\n        <div class=\"card-header\"><h4>Publier un nouveau Devoir<\/h4><\/div>\n        <div class=\"form-stack-full\">\n          <div class=\"form-group\">\n            <label class=\"form-label\">Classe *<\/label>\n            <div class=\"eleve-search-wrap\">\n              <span class=\"eleve-search-icon\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"\/><path d=\"m21 21-4.3-4.3\"\/><\/svg><\/span>\n              <input class=\"form-input\" id=\"t-dev-classe-search\" type=\"text\" placeholder=\"Tapez le nom de la classe\u2026\" autocomplete=\"off\"\n                     oninput=\"filterClasseSearch('t-dev')\" onfocus=\"filterClasseSearch('t-dev')\" onkeydown=\"classeSearchKeydown(event,'t-dev')\">\n              <div class=\"eleve-search-results\" id=\"t-dev-classe-search-results\"><\/div>\n            <\/div>\n          <\/div>\n          <div style=\"display:none;\" aria-hidden=\"true\">\n          <div class=\"form-group\">\n            <label class=\"form-label\">Niveau *<\/label>\n            <select class=\"form-select\" id=\"t-dev-niveau\" onchange=\"updateTDevoirClasseDropdown()\">\n              <option value=\"\" disabled selected>-- Choisir le niveau --<\/option>\n            <\/select>\n          <\/div>\n          <div class=\"form-group\">\n            <label class=\"form-label\">Classe *<\/label>\n            <select class=\"form-select\" id=\"t-dev-classe\" disabled>\n              <option value=\"\" disabled selected>S\u00e9lectionnez d'abord un niveau<\/option>\n            <\/select>\n          <\/div>\n          <\/div>\n          <div class=\"form-group\">\n            <label class=\"form-label\">Mati\u00e8re *<\/label>\n            <select class=\"form-select\" id=\"t-dev-matiere\">\n              <option value=\"\" disabled selected>-- Choisir la mati\u00e8re --<\/option>\n            <\/select>\n          <\/div>\n        <\/div>\n        <div class=\"form-group\" id=\"t-dev-autre-wrap\" style=\"display:none;\">\n          <label class=\"form-label\">Pr\u00e9cisez la mati\u00e8re *<\/label>\n          <input class=\"form-input\" id=\"t-dev-matiere-autre\" type=\"text\" placeholder=\"Ex: Sciences, Anglais\u2026\">\n        <\/div>\n        <div class=\"form-group\">\n          <label class=\"form-label\">Titre du devoir *<\/label>\n          <input class=\"form-input\" id=\"t-dev-titre\" type=\"text\" value=\"Devoir maison\" placeholder=\"Ex: Lire la le\u00e7on G12\">\n        <\/div>\n        <div class=\"form-group\">\n          <label class=\"form-label\">Fichier joint <span style=\"font-weight:400;color:var(--muted);\">(Image, TXT ou PDF - Max 2 Mo)<\/span><\/label>\n          <input class=\"form-input\" id=\"t-dev-fichier\" type=\"file\" accept=\".jpg,.jpeg,.png,.txt,.pdf\" style=\"padding:10px;\">\n        <\/div>\n        <div class=\"form-group\">\n          <label class=\"form-label\">Description \/ Consignes<\/label>\n          <textarea class=\"form-textarea\" id=\"t-dev-desc\" rows=\"1\" style=\"min-height:0;\" placeholder=\"D\u00e9tails du travail \u00e0 faire...\"><\/textarea>\n        <\/div>\n        <div class=\"form-group\">\n          <label class=\"form-label\">Date de remise requise<\/label>\n          <input class=\"form-input date-left\" id=\"t-dev-date\" type=\"date\">\n        <\/div>\n        <button class=\"btn btn-blue\" style=\"width:100%;margin-top:6px;background:linear-gradient(135deg,#10B981,#059669);\" onclick=\"publishTDevoir()\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px;\"><path d=\"m22 2-7 20-4-9-9-4Z\"\/><path d=\"M22 2 11 13\"\/><\/svg>Diffuser le devoir<\/button>\n      <\/div>\n\n      <!-- Colonne droite : historique -->\n      <div class=\"card\">\n        <div class=\"card-header\"><h4>Historique du Cahier de Textes<\/h4><p>Devoirs publi\u00e9s par l'administration et les enseignants<\/p><\/div>\n        <div id=\"t-devoirs-pagination\" style=\"display:none;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:nowrap;gap:8px;overflow-x:auto;\">\n          <div id=\"t-devoirs-pagination-info\" style=\"font-size:13px;color:var(--muted);\"><\/div>\n          <div id=\"t-devoirs-pagination-btns\" style=\"display:flex;gap:6px;flex-wrap:nowrap;\"><\/div>\n        <\/div>\n        <div class=\"tbl-wrap\">\n          <table class=\"tbl\">\n            <colgroup><col style=\"width:8%\"><col style=\"width:12%\"><col style=\"width:20%\"><col style=\"width:19%\"><col style=\"width:14%\"><col style=\"width:12%\"><col style=\"width:15%\"><\/colgroup>\n            <thead><tr><th><span class=\"th-txt\">Classe<\/span><\/th><th><span class=\"th-txt\">Mati\u00e8re<\/span><\/th><th><span class=\"th-txt\">Titre<\/span><\/th><th><span class=\"th-txt\">Publi\u00e9 par<\/span><\/th><th><span class=\"th-txt\">Date Butoir<\/span><\/th><th><span class=\"th-txt\">Document<\/span><\/th><th><span class=\"th-txt\">Action<\/span><\/th><\/tr><\/thead>\n            <tbody id=\"t-table-devoirs\"><\/tbody>\n          <\/table>\n        <\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n  <!-- \u2550\u2550 \/Workspace Enseignant \u2550\u2550 -->\n\n  <div class=\"ouzoud-toast-wrap\" id=\"ouzoud-toast-wrap\"><\/div>\n\n<\/div>\n\n<script>\n\/\/ [FIX#19] : les fen\u00eatres modales (Ajout Parent, Ajout Enseignant, Import CSV) et les\n\/\/ notifications (toasts) doivent toujours s'afficher au premier plan, par-dessus l'en-t\u00eate\n\/\/ et le menu du th\u00e8me WordPress. Comme elles \u00e9taient imbriqu\u00e9es dans .ouzoud-scope (un\n\/\/ conteneur avec overflow:hidden), certains th\u00e8mes\/page builders cr\u00e9ent malgr\u00e9 tout un\n\/\/ contexte d'empilement (stacking context) qui les fait appara\u00eetre SOUS l'en-t\u00eate du site,\n\/\/ m\u00eame avec un z-index tr\u00e8s \u00e9lev\u00e9. On les d\u00e9place donc \u00e0 la racine du <body>, hors de\n\/\/ .ouzoud-scope, ce qui garantit qu'elles s'affichent bien au-dessus de toute la page.\n(function ouzoudMoveOverlaysToBody() {\n    ['m-csv-import', 'm-parent-form', 'm-abs-edit', 'm-dev-edit', 'm-ann-edit', 'm-ens-form', 'm-ens-csv-import', 'ouzoud-toast-wrap'].forEach(function(id) {\n        var el = document.getElementById(id);\n        if (el && el.parentNode !== document.body) {\n            document.body.appendChild(el);\n        }\n    });\n})();\n\n\/\/ \u2500\u2500 \u00c9chappement HTML \u2014 protection XSS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction escHtml(str) {\n    if (str === null || str === undefined) return '';\n    return String(str)\n        .replace(\/&\/g, '&amp;')\n        .replace(\/<\/g, '&lt;')\n        .replace(\/>\/g, '&gt;')\n        .replace(\/\"\/g, '&quot;')\n        .replace(\/'\/g, '&#39;');\n}\n\/\/ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\/\/ [FIX#4 v4.3] : le nonce ne doit plus appara\u00eetre dans les URLs de fichiers (logs serveur,\n\/\/ Referer, historique navigateur). On obtient d'abord un token \u00e9ph\u00e9m\u00e8re (valable 60s) via POST,\n\/\/ puis on charge le fichier via GET avec ce token \u2014 le nonce reste dans le body POST uniquement.\n\/\/ Toutes les fonctions qui affichent des liens vers des fichiers sont d\u00e9sormais asynchrones.\nasync function buildSecureFileUrl(rawUrl) {\n    if (!rawUrl) return '';\n    let p = new URLSearchParams();\n    p.append('action', 'ouzoud_ajax_get_file_token');\n    p.append('nonce',  OuzoudAjax.nonce);\n    p.append('file',   rawUrl);\n    try {\n        const res = await fetch(OuzoudAjax.url, { method: 'POST', body: p });\n        const json = await res.json();\n        if (json.success && json.data && json.data.token) {\n            return OuzoudAjax.url\n                + '?action=ouzoud_serve_file'\n                + '&token=' + encodeURIComponent(json.data.token);\n        }\n    } catch(e) { \/* silencieux \u2014 le lien sera vide *\/ }\n    return '';\n}\n\nlet ouzoudSessionData = null;\nlet currentProfileType = 'parent';\n\n\/\/ [FIX#8 v4.2] : renouveler le nonce toutes les 10h. En cas d'\u00e9chec,\n\/\/ on notifie l'utilisateur plut\u00f4t que d'avaler l'erreur silencieusement \u2014\n\/\/ sans nonce valide, toutes les actions AJAX \u00e9choueraient sans explication.\n(function ouzoudScheduleNonceRenewal() {\n    setInterval(function() {\n        let p = new URLSearchParams();\n        p.append('action', 'ouzoud_ajax_refresh_nonce');\n        fetch(OuzoudAjax.url, { method: 'POST', body: p })\n            .then(r => r.json())\n            .then(res => {\n                if (res.success && res.data && res.data.nonce) {\n                    OuzoudAjax.nonce = res.data.nonce;\n                } else {\n                    \/\/ Nonce non renouvel\u00e9 (session expir\u00e9e c\u00f4t\u00e9 serveur) \u2192 d\u00e9connexion propre\n                    showToast('Votre session a expir\u00e9. Veuillez vous reconnecter.', 'info', 6000);\n                    setTimeout(() => triggerLogout(), 2000);\n                }\n            })\n            .catch(() => {\n                showToast('Connexion interrompue. V\u00e9rifiez votre r\u00e9seau \u2014 la page n\u00e9cessite un rechargement.', 'error', 8000);\n            });\n    }, 10 * 60 * 60 * 1000); \/\/ 10 heures\n})();\n\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction showToast(message, type = 'success', duration = 3200) {\n    const icons = { success: '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;\"><path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"\/><path d=\"m9 11 3 3L22 4\"\/><\/svg>', error: '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;\"><circle cx=\"12\" cy=\"12\" r=\"10\"\/><path d=\"m15 9-6 6\"\/><path d=\"m9 9 6 6\"\/><\/svg>', info: '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z\"\/><path d=\"M12 9v4\"\/><path d=\"M12 17h.01\"\/><\/svg>' };\n    const wrap = document.getElementById('ouzoud-toast-wrap');\n    const toast = document.createElement('div');\n    toast.className = `ouzoud-toast ${type}`;\n    toast.innerHTML = `<span class=\"ouzoud-toast-icon\">${icons[type] || '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;\"><path d=\"M7.9 20A9 9 0 1 0 4 16.1L2 22Z\"\/><\/svg>'}<\/span><span>${escHtml(message)}<\/span>`;\n    wrap.appendChild(toast);\n    requestAnimationFrame(() => { requestAnimationFrame(() => { toast.classList.add('show'); }); });\n    setTimeout(() => {\n        toast.classList.remove('show');\n        setTimeout(() => toast.remove(), 280);\n    }, duration);\n}\n\/\/ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction formatToFrenchDate(inputDate) {\n    if (!inputDate) return '-';\n    let parts = inputDate.split('-');\n    if (parts.length === 3) {\n        return `${parts[2]}\/${parts[1]}\/${parts[0]}`;\n    }\n    return inputDate;\n}\n\nfunction getTomorrowSQLDate() {\n    let tomorrow = new Date();\n    tomorrow.setDate(tomorrow.getDate() + 1);\n    let tomDay = String(tomorrow.getDate()).padStart(2, '0');\n    let tomMonth = String(tomorrow.getMonth() + 1).padStart(2, '0');\n    let tomYear = tomorrow.getFullYear();\n    return `${tomYear}-${tomMonth}-${tomDay}`;\n}\n\n\/\/ [FIX#1] : date d'aujourd'hui en format SQL pour r\u00e9initialisation du formulaire d'absence\nfunction getTodaySQLDate() {\n    let today = new Date();\n    return `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`;\n}\n\n\/\/ [FIX#9] : validation du format OUZ-YYYY-XXXX \u2014 d\u00e9clench\u00e9e \u00e0 la perte de focus\nfunction validateParentCodeFormat(input) {\n    if (currentProfileType !== 'parent') { input.style.borderColor = ''; return; }\n    const val = input.value.trim().toUpperCase();\n    const valid = \/^OUZ-\\d{4}-\\d{4}$\/.test(val);\n    if (val.length > 0 && !valid) {\n        input.style.borderColor = 'var(--amber)';\n    } else {\n        input.style.borderColor = valid ? 'var(--blue)' : '';\n    }\n}\n\/\/ R\u00e9initialise un champ password et remet l'\u0153il en mode masqu\u00e9\nfunction clearPasswordField(fieldId) {\n    const f = document.getElementById(fieldId);\n    if (!f) return;\n    f.value = '';\n    f.style.borderColor = '';\n    if (f.type === 'text') {\n        f.type = 'password';\n        const btn = f.parentElement && f.parentElement.querySelector('.lf-eye');\n        if (btn) btn.innerHTML = eyeOpenSVG();\n    }\n}\nfunction clearParentField() { clearPasswordField('login-secret-field'); }\nfunction clearAdminField()  { clearPasswordField('login-secret-field'); }\n\/\/ SVG \u0153il ouvert (mot de passe masqu\u00e9) et \u0153il barr\u00e9 (mot de passe visible)\nfunction eyeOpenSVG() {\n    return '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"\/><circle cx=\"12\" cy=\"12\" r=\"3\"\/><\/svg>';\n}\nfunction eyeClosedSVG() {\n    return '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94\"\/><path d=\"M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19\"\/><line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\"\/><\/svg>';\n}\nfunction _buildMatiereOptions(selectEl) {\n    const matieres = (ouzoudSessionData && Array.isArray(ouzoudSessionData.matieres_config) && ouzoudSessionData.matieres_config.length > 0)\n        ? ouzoudSessionData.matieres_config\n        : ['Arabe', 'Fran\u00e7ais', 'Maths'];\n    selectEl.innerHTML = '<option value=\"\" disabled selected>-- Choisir la mati\u00e8re --<\/option>';\n    matieres.forEach(m => {\n        const opt = document.createElement('option');\n        opt.value = m; opt.textContent = m;\n        selectEl.appendChild(opt);\n    });\n}\nfunction populateAllMatiereSelects() {\n    const admin = document.getElementById('a-dev-matiere');\n    if (admin) _buildMatiereOptions(admin);\n    const teacher = document.getElementById('t-dev-matiere');\n    if (teacher) _buildMatiereOptions(teacher);\n}\n\nfunction toggleFieldVisibility(fieldId, btn) {\n    const f = document.getElementById(fieldId);\n    if (f.type === 'password') {\n        f.type = 'text';\n        btn.innerHTML = eyeClosedSVG();\n    } else {\n        f.type = 'password';\n        btn.innerHTML = eyeOpenSVG();\n    }\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", function() {\n    let d = new Date();\n    let day = String(d.getDate()).padStart(2, '0');\n    let month = String(d.getMonth() + 1).padStart(2, '0');\n    let year = d.getFullYear();\n\n    \/\/ Indicateur de scroll horizontal sur les tbl-wrap\n    function updateScrollIndicators() {\n      document.querySelectorAll('.tbl-wrap').forEach(w => {\n        w.classList.toggle('can-scroll', w.scrollWidth > w.clientWidth + 2);\n      });\n      \/\/ [FIX#16 v4.6] : m\u00eame indicateur pour la navigation horizontale du bandeau\n      \/\/ (utile sur tablette\/t\u00e9l\u00e9phone quand les onglets d\u00e9passent la largeur visible).\n      document.querySelectorAll('.main-topbar-nav').forEach(w => {\n        w.classList.toggle('can-scroll', w.scrollWidth > w.clientWidth + 2);\n      });\n    }\n    updateScrollIndicators();\n    window.addEventListener('resize', updateScrollIndicators);\n    document.addEventListener('click', updateScrollIndicators);\n\n    let todaySQL = `${year}-${month}-${day}`;\n    if(document.getElementById('abs-date')) document.getElementById('abs-date').value = todaySQL;\n    if(document.getElementById('abs-classe')) document.getElementById('abs-classe').disabled = true;\n\n    setTimeout(function() {\n        clearParentField();\n        clearAdminField();\n    }, 50);\n});\n\nconst LOGIN_UI = {\n    labels: {'parent': '\u0631\u0645\u0632 \u062f\u062e\u0648\u0644 \u0648\u0644\u064a \u0627\u0644\u0623\u0645\u0631', 'teacher': '\u0631\u0645\u0632 \u0627\u0644\u0623\u0633\u062a\u0627\u0630(\u0629)', 'admin': '\u0645\u0641\u062a\u0627\u062d \u0627\u0644\u0625\u062f\u0627\u0631\u0629'},\n    buttons: {'parent': '\u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0641\u0636\u0627\u0621 \u0648\u0644\u064a \u0627\u0644\u0623\u0645\u0631', 'teacher': '\u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0641\u0636\u0627\u0621 \u0627\u0644\u0623\u0633\u062a\u0627\u0630', 'admin': '\u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062f\u062e\u0648\u0644 \u0627\u0644\u0625\u062f\u0627\u0631\u064a'},\n    arrowAfter: false\n};\nconst LOGIN_ARROW = '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;\"><path d=\"M5 12h14\"\/><path d=\"m12 5 7 7-7 7\"\/><\/svg>';\nfunction toggleLoginTab(type) {\n    currentProfileType = type;\n    const sel = document.getElementById('login-profile');\n    if (sel && sel.value !== type) sel.value = type;\n    const lbl = document.getElementById('login-secret-label');\n    if (lbl) lbl.textContent = LOGIN_UI.labels[type];\n    const btn = document.getElementById('login-submit-btn');\n    if (btn) {\n        btn.className = 'lf-btn' + (type === 'admin' ? ' admin' : type === 'teacher' ? ' teacher' : '');\n        btn.innerHTML = LOGIN_UI.arrowAfter\n            ? LOGIN_UI.buttons[type] + ' ' + LOGIN_ARROW\n            : LOGIN_ARROW + LOGIN_UI.buttons[type] + ' \\u2192';\n    }\n    clearParentField();\n}\n\nfunction clearTeacherField() { clearPasswordField('login-secret-field'); }\n\nfunction executeLogin(type) {\n    let p = new URLSearchParams();\n    p.append('action', 'ouzoud_ajax_login');\n    p.append('nonce', OuzoudAjax.nonce);\n    p.append('login_type', type);\n\n    if(type === 'admin') {\n        p.append('password', document.getElementById('login-secret-field').value);\n    } else if(type === 'teacher') {\n        const codeT = document.getElementById('login-secret-field').value.trim().toUpperCase();\n        if (!\/^ENS-\\d{4}-\\d{4}$\/.test(codeT)) {\n            showToast('Cl\u00e9 enseignant invalide.', 'error');\n            return;\n        }\n        p.append('code_enseignant', codeT);\n    } else {\n        const codeVal = document.getElementById('login-secret-field').value.trim().toUpperCase();\n        \/\/ [FIX#9] : valider le format avant envoi \u2014 \u00e9vite un aller-retour inutile\n        if (!\/^OUZ-\\d{4}-\\d{4}$\/.test(codeVal)) {\n            showToast('Cl\u00e9 parent invalide.', 'error');\n            return;\n        }\n        p.append('code_parent', codeVal);\n    }\n\n    fetch(OuzoudAjax.url, { method: 'POST', body: p })\n    .then(r => r.json())\n    .then(res => {\n        if(res.success) {\n            ouzoudSessionData = res.data;\n            ouzoudSessionData._loginType = type;\n            \/\/ Effacer imm\u00e9diatement les champs du formulaire apr\u00e8s connexion\n            clearAdminField();\n            clearParentField();\n            clearTeacherField();\n            setupApplicationWorkspace();\n        } else {\n            showToast(res.data || \"Identifiant ou cl\u00e9 incorrecte.\", 'error');\n            if(type === 'admin') clearAdminField();\n            else if(type === 'teacher') clearTeacherField();\n            else clearParentField();\n        }\n    })\n    \/\/ [FIX#11] : gestion des erreurs r\u00e9seau sur la connexion\n    .catch(() => {\n        showToast(\"Erreur r\u00e9seau. V\u00e9rifiez votre connexion et r\u00e9essayez.\", 'error');\n    });\n}\n\nfunction setupApplicationWorkspace() {\n    document.getElementById('ouzoud-login').style.display = 'none';\n\n    let role = ouzoudSessionData.role;\n    let isA = (role === 'admin');\n    let isT = (role === 'teacher');\n    const now = new Date();\n    const dateStr = now.toLocaleDateString('fr-FR', {weekday:'long', day:'numeric', month:'long', year:'numeric'});\n    \/\/ Date en arabe (police Cairo) uniquement pour l'espace parents\n    const dateStrAr = now.toLocaleDateString('ar-MA-u-nu-latn', {weekday:'long', day:'numeric', month:'long', year:'numeric'}).replace(\/[\u060c,]\\s*\/g, ' ');\n\n    if (isT) {\n        document.getElementById('ouzoud-teacher-app').classList.add('active');\n        document.getElementById('teacher-date-bar').innerText = dateStr;\n        const nameBar = document.getElementById('teacher-name-bar');\n        if (nameBar) {\n            const nom = ouzoudSessionData.name || '';\n            nameBar.innerText = nom;\n        }\n        const classesBar = document.getElementById('teacher-classes-bar');\n        if (classesBar) {\n            const classes = (ouzoudSessionData && Array.isArray(ouzoudSessionData.classes) && ouzoudSessionData.classes.length > 0)\n                ? ouzoudSessionData.classes\n                : [];\n            if (classes.length > 0) {\n                classesBar.innerHTML = classes.map(c => `<span class=\"t-class-chip\">${escHtml(c.trim())}<\/span>`).join('');\n            } else {\n                classesBar.innerHTML = '';\n            }\n        }\n        updateTDevoirMatiereDropdown();\n        updateTDevoirClasseDropdown();\n        document.getElementById('t-dev-date').value = getTomorrowSQLDate();\n        loadApplicationWorkspace();\n    } else if (isA) {\n        document.getElementById('ouzoud-admin-app').classList.add('active');\n        document.getElementById('a-date-bar').innerText = dateStr;\n        updateADevoirClasseDropdown();\n        document.getElementById('a-dev-date').value = getTomorrowSQLDate();\n        loadApplicationWorkspace();\n    } else {\n        document.getElementById('ouzoud-parent-app').classList.add('active');\n        document.getElementById('p-date-bar').innerText = dateStrAr;\n        document.getElementById('p-user-name').innerText = ouzoudSessionData.name || '';\n        loadApplicationWorkspace();\n    }\n}\n\nfunction switchMainPane(targetId, role, btn) {\n    \/\/ D\u00e9sactiver tous les panes dans le bon workspace\n    const workspace = role === 'admin' ? document.getElementById('ouzoud-admin-app') : document.getElementById('ouzoud-parent-app');\n    workspace.querySelectorAll('.page-pane').forEach(p => p.classList.remove('active'));\n    workspace.querySelectorAll('.main-nav-btn').forEach(b => b.classList.remove('active'));\n    document.getElementById(targetId).classList.add('active');\n    btn.classList.add('active');\n}\n\nfunction navigateOuzoud(targetId, btn) {\n    \/\/ Conserv\u00e9 pour compatibilit\u00e9 avec d'\u00e9ventuels appels r\u00e9siduels\n    document.querySelectorAll('.page-pane').forEach(p => p.classList.remove('active'));\n    document.querySelectorAll('.main-nav-btn').forEach(l => l.classList.remove('active'));\n    const el = document.getElementById(targetId);\n    if (el) el.classList.add('active');\n    if (btn) btn.classList.add('active');\n}\n\nfunction loadApplicationWorkspace() {\n    let p = new URLSearchParams();\n    p.append('action', 'ouzoud_ajax_refresh_data');\n    p.append('nonce', OuzoudAjax.nonce);\n\n    fetch(OuzoudAjax.url, { method: 'POST', body: p })\n    .then(r => r.json())\n    .then(res => {\n        if(res.success) {\n            \/\/ [FIX#14 v4.5] : isoler les erreurs d'affichage des erreurs r\u00e9seau.\n            \/\/ Avant ce correctif, une exception JS lev\u00e9e pendant le rendu (ex: \u00e9l\u00e9ment\n            \/\/ DOM manquant) \u00e9tait captur\u00e9e par le .catch() r\u00e9seau ci-dessous et affichait\n            \/\/ \u00e0 tort \"Erreur r\u00e9seau\" alors que les donn\u00e9es avaient bien \u00e9t\u00e9 re\u00e7ues.\n            try {\n                let savedMeta = {\n                    _loginType: ouzoudSessionData ? ouzoudSessionData._loginType : null\n                };\n                ouzoudSessionData = res.data;\n                Object.assign(ouzoudSessionData, savedMeta);\n                let r = ouzoudSessionData.role;\n                if(r === 'admin') renderAdminPanels();\n                else if(r === 'teacher') renderTeacherPanels();\n                else renderParentPanels();\n            } catch (renderErr) {\n                console.error('[Ouzoud] Erreur lors de l\\'affichage des donn\u00e9es re\u00e7ues :', renderErr);\n                showToast(\"Les donn\u00e9es ont \u00e9t\u00e9 re\u00e7ues mais n'ont pas pu \u00eatre affich\u00e9es. Veuillez recharger la page.\", 'error');\n            }\n        } else {\n            \/\/ Session expir\u00e9e (ou acc\u00e8s refus\u00e9) \u2014 retour \u00e0 l'\u00e9cran de connexion.\n            \/\/ [DEMANDE] : afficher le message r\u00e9el du serveur quand il est fourni\n            \/\/ (ex: refus \"Vous devez vous connecter au site en tant qu'Admin\")\n            \/\/ plut\u00f4t qu'un texte g\u00e9n\u00e9rique qui masquerait la vraie raison.\n            triggerLogout();\n            showToast(res.data || 'Session expir\u00e9e. Veuillez vous reconnecter.', 'info');\n        }\n    })\n    \/\/ [FIX#11] : gestion des erreurs r\u00e9seau \u2014 \u00e9vite un \u00e9cran blanc silencieux\n    .catch((err) => {\n        console.error('[Ouzoud] Erreur r\u00e9seau lors du rafra\u00eechissement des donn\u00e9es :', err);\n        showToast(\"Erreur r\u00e9seau. Les donn\u00e9es n'ont pas pu \u00eatre charg\u00e9es.\", 'error');\n    });\n}\n\n\/\/ [AR] Traduction arabe de la p\u00e9riode d'absence (valeurs stock\u00e9es : matin \/ apr\u00e8s-midi \/ journ\u00e9e)\nfunction _arPeriode(v) {\n    const k = String(v || '').toLowerCase().trim();\n    if (k === 'matin') return '\u0635\u0628\u0627\u062d\u0627';\n    if (k === 'apr\u00e8s-midi' || k === 'apres-midi') return '\u0628\u0639\u062f \u0627\u0644\u0632\u0648\u0627\u0644';\n    if (k === 'journ\u00e9e' || k === 'journee' || k === 'journ\u00e9e compl\u00e8te') return '\u0627\u0644\u064a\u0648\u0645 \u0643\u0627\u0645\u0644\u0627';\n    return v || '';\n}\n\nfunction renderParentPanels() {\n    document.getElementById('dash-kids-count').innerText = ouzoudSessionData.enfants.length;\n    document.getElementById('dash-abs-count').innerText = ouzoudSessionData.absences.length;\n    document.getElementById('dash-devoirs-count').innerText = ouzoudSessionData.devoirs.length;\n    document.getElementById('dash-annonces-count').innerText = ouzoudSessionData.annonces.length;\n\n    let kHtml = '';\n    ouzoudSessionData.enfants.forEach(k => { kHtml += `<tr><td data-label=\"\u0627\u0644\u0627\u0633\u0645 \u0627\u0644\u0643\u0627\u0645\u0644\"><b>${escHtml(k.nom_enfant)}<\/b><\/td><td data-label=\"\u0627\u0644\u0642\u0633\u0645\"><span class=\"chip chip-blue\">${escHtml(k.classe)}<\/span><\/td><\/tr>`; });\n    document.getElementById('p-dash-kids-table').innerHTML = kHtml;\n\n    let dAnnHtml = '';\n    ouzoudSessionData.annonces.slice(0, 2).forEach(a => {\n        let fileButton = '';\n        if (a.fichier_joint) {\n            \/\/ [FIX#4 v4.3] : le token est r\u00e9solu au clic (async) \u2014 pas dans l'URL statique\n            fileButton = `<div style=\"margin-top:8px;\"><button class=\"btn btn-outline btn-sm ouzoud-file-btn\" data-fileurl=\"${escHtml(a.fichier_joint)}\" style=\"height:26px; padding:2px 8px; font-size:11px;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"\/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"\/><path d=\"M10 9H8\"\/><path d=\"M16 13H8\"\/><path d=\"M16 17H8\"\/><\/svg>\u0641\u062a\u062d \u0627\u0644\u0648\u062b\u064a\u0642\u0629<\/button><\/div>`;\n        }\n        dAnnHtml += `<div class=\"ann-card ${escHtml(a.priorite==='haute'?'urgent':'')}\"><h5>${escHtml(a.titre)}<\/h5><p style=\"font-size:12.5px;color:var(--text-2);margin-top:4px;\">${escHtml(a.contenu)}<\/p>${fileButton}<\/div>`;\n    });\n    document.getElementById('p-dash-ann-list').innerHTML = dAnnHtml || '<p style=\"font-size:13px;color:var(--muted);\">\u0644\u0627 \u062a\u0648\u062c\u062f \u0625\u0639\u0644\u0627\u0646\u0627\u062a \u062d\u062f\u064a\u062b\u0629.<\/p>';\n\n    \/\/ R\u00e9initialiser les pages et d\u00e9clencher le rendu pagin\u00e9\n    _pAbsCurrentPage = 1;\n    _pDevCurrentPage = 1;\n    _pAnnCurrentPage = 1;\n    _pAbsRenderPage(1);\n    _pDevRenderPage(1);\n    _pAnnRenderPage(1);\n}\n\n\/\/ \u2500\u2500 Pagination absences (espace parent) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet _pAbsCurrentPage = 1;\nfunction _pAbsRenderPage(page) {\n    const list = ouzoudSessionData ? (ouzoudSessionData.absences || []) : [];\n    const total = list.length;\n    const totalPages = Math.max(1, Math.ceil(total \/ PARENTS_PER_PAGE));\n    _pAbsCurrentPage = Math.min(Math.max(1, page), totalPages);\n    const slice = list.slice((_pAbsCurrentPage-1)*PARENTS_PER_PAGE, _pAbsCurrentPage*PARENTS_PER_PAGE);\n    let html = slice.map(a => {\n        let chip = a.statut==='justified' ? '<div><span class=\"chip chip-orange\">\u0645\u0628\u0631\u0631\u0629<\/span><\/div>' : '<div><span class=\"chip chip-red\">\u063a\u064a\u0631 \u0645\u0628\u0631\u0631\u0629<\/span><\/div>';\n        let perAr = _arPeriode(a.demi_journee);\n        return `<tr><td data-label=\"\u0627\u0644\u062a\u0644\u0645\u064a\u0630\"><b>${escHtml(a.nom_eleve)}<\/b><\/td><td data-label=\"\u0627\u0644\u0642\u0633\u0645\"><span class=\"chip chip-blue\">${escHtml(a.classe)}<\/span><\/td><td data-label=\"\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u063a\u064a\u0627\u0628\" class=\"date-cell\">${escHtml(formatToFrenchDate(a.date_absence))}<\/td><td data-label=\"\u0627\u0644\u0641\u062a\u0631\u0629\"><div>${escHtml(perAr)}<\/div><\/td><td data-label=\"\u0633\u0628\u0628 \u0627\u0644\u063a\u064a\u0627\u0628\"><i>${escHtml(a.motif||'-')}<\/i><\/td><td data-label=\"\u0627\u0644\u062d\u0627\u0644\u0629\">${chip}<\/td><\/tr>`;\n    }).join('');\n    document.getElementById('p-table-absences').innerHTML = html || '<tr><td colspan=\"6\" style=\"text-align:center;color:var(--muted);\">\u0644\u0645 \u064a\u062a\u0645 \u062a\u0633\u062c\u064a\u0644 \u0623\u064a \u063a\u064a\u0627\u0628.<\/td><\/tr>';\n    _renderPaginationBar('p-absences', total, totalPages, _pAbsCurrentPage, '_pAbsRenderPage');\n    if (typeof updateScrollIndicators === 'function') updateScrollIndicators();\n}\n\n\/\/ \u2500\u2500 Pagination devoirs (espace parent) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet _pDevCurrentPage = 1;\nfunction _pDevRenderPage(page) {\n    const list = ouzoudSessionData ? (ouzoudSessionData.devoirs || []) : [];\n    const total = list.length;\n    const totalPages = Math.max(1, Math.ceil(total \/ PARENTS_PER_PAGE));\n    _pDevCurrentPage = Math.min(Math.max(1, page), totalPages);\n    const slice = list.slice((_pDevCurrentPage-1)*PARENTS_PER_PAGE, _pDevCurrentPage*PARENTS_PER_PAGE);\n    let html = slice.map(d => {\n        let fileLink = '-';\n        if (d.fichier_joint) {\n            \/\/ [FIX#4 v4.3] : token r\u00e9solu au clic\n            fileLink = `<button class=\"btn btn-outline btn-sm ouzoud-file-btn\" data-fileurl=\"${escHtml(d.fichier_joint)}\" style=\"height:26px;padding:2px 7px;font-size:10px;white-space:nowrap;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:3px;\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"\/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"\/><path d=\"M10 9H8\"\/><path d=\"M16 13H8\"\/><path d=\"M16 17H8\"\/><\/svg>\u0641\u062a\u062d<\/button>`;\n            fileLink = '<div>' + fileLink + '<\/div>';\n        }\n        return `<tr>\n            <td data-label=\"\u0627\u0644\u0645\u0627\u062f\u0629\"><span class=\"chip chip-blue\" style=\"font-weight:700;\">${escHtml(d.matiere)}<\/span><\/td>\n            <td data-label=\"\u0627\u0644\u0639\u0645\u0644 \u0627\u0644\u0645\u0637\u0644\u0648\u0628\"><b>${escHtml(d.titre)}<\/b> <span class=\"chip chip-orange\" style=\"font-size:10px;padding:2px 6px;\">${escHtml(d.classe)}<\/span><\/td>\n            <td data-label=\"\u0627\u0644\u062a\u0639\u0644\u064a\u0645\u0627\u062a \u0648\u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644\" style=\"color:var(--text-2); max-width:400px; word-wrap:break-word;\">${d.description ? escHtml(d.description) : '<span style=\"color:var(--muted);\">\u0644\u0627 \u062a\u0648\u062c\u062f \u062a\u0639\u0644\u064a\u0645\u0627\u062a \u0645\u062d\u062f\u062f\u0629.<\/span>'}<\/td>\n            <td data-label=\"\u0627\u0644\u0648\u062b\u064a\u0642\u0629\">${fileLink}<\/td>\n            <td data-label=\"\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0633\u0644\u064a\u0645\" class=\"date-cell\"><b>${escHtml(formatToFrenchDate(d.date_butoir))}<\/b><\/td>\n        <\/tr>`;\n    }).join('');\n    document.getElementById('p-list-devoirs-table').innerHTML = html || '<tr><td colspan=\"5\" style=\"text-align:center;color:var(--muted);padding:20px;\">\u0644\u0627 \u064a\u0648\u062c\u062f \u0623\u064a \u0648\u0627\u062c\u0628 \u0645\u0628\u0631\u0645\u062c \u062d\u0627\u0644\u064a\u0627.<\/td><\/tr>';\n    _renderPaginationBar('p-devoirs', total, totalPages, _pDevCurrentPage, '_pDevRenderPage');\n    if (typeof updateScrollIndicators === 'function') updateScrollIndicators();\n}\n\n\/\/ \u2500\u2500 Pagination annonces (espace parent) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet _pAnnCurrentPage = 1;\nfunction _pAnnRenderPage(page) {\n    const list = ouzoudSessionData ? (ouzoudSessionData.annonces || []) : [];\n    const total = list.length;\n    const totalPages = Math.max(1, Math.ceil(total \/ PARENTS_PER_PAGE));\n    _pAnnCurrentPage = Math.min(Math.max(1, page), totalPages);\n    const slice = list.slice((_pAnnCurrentPage-1)*PARENTS_PER_PAGE, _pAnnCurrentPage*PARENTS_PER_PAGE);\n    let html = slice.map(a => {\n        let prioriteChip = a.priorite === 'haute' ? 'chip-red' : 'chip-blue';\n        let prioriteLabel = a.priorite === 'haute' ? '\u0639\u0627\u062c\u0644' : '\u0639\u0627\u062f\u064a';\n        let fileLinkBlock = '';\n        if (a.fichier_joint) {\n            \/\/ [FIX#4 v4.3] : token r\u00e9solu au clic\n            fileLinkBlock = `\n            <div style=\"margin-top:12px; padding-top:10px; border-top:1px dashed var(--border);\">\n                <button class=\"btn btn-outline btn-sm ouzoud-file-btn\" data-fileurl=\"${escHtml(a.fichier_joint)}\" style=\"height:30px; font-size:12px;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"\/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"\/><path d=\"M10 9H8\"\/><path d=\"M16 13H8\"\/><path d=\"M16 17H8\"\/><\/svg>\u0641\u062a\u062d \u0627\u0644\u0648\u062b\u064a\u0642\u0629 \u0627\u0644\u0645\u0631\u0641\u0642\u0629<\/button>\n            <\/div>`;\n        }\n        return `\n        <div class=\"ann-card ${escHtml(a.priorite === 'haute' ? 'urgent' : '')}\">\n            <div style=\"display:flex; justify-content:space-between; margin-bottom:8px; align-items:center;\">\n                <b style=\"font-size:15px; color:var(--text);\">${escHtml(a.titre)}<\/b>\n                <div style=\"text-align:center;\"><span class=\"chip ${prioriteChip}\">${escHtml(prioriteLabel)}<\/span><\/div>\n            <\/div>\n            <p style=\"font-size:13.5px; color:var(--text-2); line-height:1.5; white-space:pre-line;\">${escHtml(a.contenu)}<\/p>\n            ${fileLinkBlock}\n        <\/div>`;\n    }).join('');\n    document.getElementById('p-list-annonces').innerHTML = html || '<p style=\"color:var(--muted); text-align:center; padding:20px;\">\u0644\u0627 \u062a\u0648\u062c\u062f \u0625\u0639\u0644\u0627\u0646\u0627\u062a \u0645\u062a\u0627\u062d\u0629 \u062d\u0627\u0644\u064a\u0627.<\/p>';\n    _renderPaginationBar('p-annonces', total, totalPages, _pAnnCurrentPage, '_pAnnRenderPage');\n    if (typeof updateScrollIndicators === 'function') updateScrollIndicators();\n}\n\n\/\/ \u2500\u2500 Constante et helper partag\u00e9s pour toutes les paginations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst PARENTS_PER_PAGE = 10;\n\nfunction _renderPaginationBar(id, total, totalPages, currentPage, callbackName) {\n    const wrap = document.getElementById(id+'-pagination');\n    const info = document.getElementById(id+'-pagination-info');\n    const btns = document.getElementById(id+'-pagination-btns');\n    if (total > PARENTS_PER_PAGE) {\n        wrap.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:nowrap;gap:8px;overflow-x:auto;';\n        btns.style.cssText = 'display:flex;gap:6px;flex-wrap:nowrap;flex-shrink:0;';\n        const from = total === 0 ? 0 : (currentPage-1)*PARENTS_PER_PAGE + 1;\n        const to   = Math.min(currentPage*PARENTS_PER_PAGE, total);\n        info.innerHTML = `${from}\u2013${to} sur ${total}` + ``;\n        let b = `<button class=\"btn btn-outline btn-sm\" onclick=\"${callbackName}(${currentPage-1})\" ${currentPage===1?'disabled':''} style=\"padding:6px 10px;flex-shrink:0;\">\u2039<\/button>`;\n        for (let i = 1; i <= totalPages; i++) {\n            b += `<button class=\"btn btn-sm\" onclick=\"${callbackName}(${i})\" style=\"padding:6px 12px;flex-shrink:0;${i===currentPage?'background:var(--blue);color:#fff;':'background:var(--bg);border:1px solid var(--border);color:var(--text);'}\">${i}<\/button>`;\n        }\n        b += `<button class=\"btn btn-outline btn-sm\" onclick=\"${callbackName}(${currentPage+1})\" ${currentPage===totalPages?'disabled':''} style=\"padding:6px 10px;flex-shrink:0;\">\u203a<\/button>`;\n        btns.innerHTML = b;\n    } else {\n        wrap.style.display = 'none';\n    }\n}\n\n\n\/\/ \u2500\u2500 [FIX32] Filtres et recherche dans les registres (absences \/ devoirs) \u2500\u2500\u2500\u2500\u2500\nfunction _normSearch(str) {\n    return String(str == null ? '' : str).normalize('NFD').replace(\/[\u0300-\u036f]\/g, '').toLowerCase().trim();\n}\nfunction _regVal(id) { const el = document.getElementById(id); return el ? el.value : ''; }\n\nfunction _absFiltersActive() {\n    return !!(_regVal('abs-f-search').trim() || _regVal('abs-f-classe') || _regVal('abs-f-statut') || _regVal('abs-f-periode'));\n}\nfunction _devFiltersActive() {\n    return !!_regVal('dev-f-search').trim();\n}\n\nfunction _absFilteredList() {\n    const all = (ouzoudSessionData && ouzoudSessionData.absences) ? ouzoudSessionData.absences : [];\n    if (!_absFiltersActive()) return all;\n    const q = _normSearch(_regVal('abs-f-search'));\n    const classe = _regVal('abs-f-classe');\n    const statut = _regVal('abs-f-statut');\n    const periode = _regVal('abs-f-periode');\n    return all.filter(a => {\n        if (q && !_normSearch(a.nom_eleve + ' ' + (a.motif || '') + ' ' + (a.classe || '')).includes(q)) return false;\n        if (classe && a.classe !== classe) return false;\n        if (statut && a.statut !== statut) return false;\n        if (periode && !_normSearch(a.demi_journee).startsWith(periode === 'apres-midi' ? 'apres' : periode.slice(0, 5))) return false;\n        return true;\n    });\n}\nfunction _devFilteredList() {\n    const all = (ouzoudSessionData && ouzoudSessionData.devoirs) ? ouzoudSessionData.devoirs : [];\n    if (!_devFiltersActive()) return all;\n    const q = _normSearch(_regVal('dev-f-search'));\n    return all.filter(d => {\n        \/\/ le code enseignant \u00ab (ENS-AAAA-XXXX) \u00bb n'est jamais affich\u00e9 : on ne cherche pas dedans\n        const par = String(d.publie_par || 'Administration').replace(\/\\s*\\(ENS-\\d{4}-\\d{4}\\)\\s*$\/i, '');\n        return _normSearch([d.titre, d.matiere, d.classe, d.description, par].join(' ')).includes(q);\n    });\n}\n\n\/\/ Met \u00e0 jour le compteur \u00ab N r\u00e9sultat(s) sur M \u00bb et le bouton R\u00e9initialiser\nfunction _updateRegistreMeta(kind, shown, total, active) {\n    const cnt = document.getElementById(kind + '-f-count');\n    const rst = document.getElementById(kind + '-f-reset');\n    if (rst) rst.style.display = active ? '' : 'none';\n    if (cnt) cnt.textContent = active ? (shown + ' r\u00e9sultat' + (shown > 1 ? 's' : '') + ' sur ' + total) : '';\n}\n\n\/\/ Liste des classes pr\u00e9sentes dans le registre (conserve la s\u00e9lection si elle existe encore)\nfunction _absRefreshClasseOptions() {\n    const sel = document.getElementById('abs-f-classe');\n    if (!sel) return;\n    const prev = sel.value;\n    const all = (ouzoudSessionData && ouzoudSessionData.absences) ? ouzoudSessionData.absences : [];\n    const classes = Array.from(new Set(all.map(a => a.classe).filter(Boolean)))\n        .sort((x, y) => String(x).localeCompare(String(y), 'fr', { numeric: true }));\n    sel.innerHTML = '<option value=\"\">Toutes les classes<\/option>' +\n        classes.map(c => `<option value=\"${escHtml(c)}\">${escHtml(c)}<\/option>`).join('');\n    sel.value = classes.includes(prev) ? prev : '';\n}\n\nlet _regFilterTimer = null;\nfunction onRegistreFilterChange(kind) {\n    clearTimeout(_regFilterTimer);\n    _regFilterTimer = setTimeout(() => {\n        if (kind === 'abs') _absRenderPage(1); else _devRenderPage(1);\n    }, 120);\n}\nfunction resetRegistreFilters(kind) {\n    const ids = kind === 'abs'\n        ? ['abs-f-search', 'abs-f-classe', 'abs-f-statut', 'abs-f-periode']\n        : ['dev-f-search'];\n    ids.forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; });\n    if (kind === 'abs') _absRenderPage(1); else _devRenderPage(1);\n}\n\n\/\/ \u2500\u2500 Pagination absences \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet _absCurrentPage = 1;\nfunction _absRenderPage(page) {\n    const list = _absFilteredList();\n    const total = list ? list.length : 0;\n    const totalPages = Math.max(1, Math.ceil(total \/ PARENTS_PER_PAGE));\n    _absCurrentPage = Math.min(Math.max(1, page), totalPages);\n    const slice = list ? list.slice((_absCurrentPage-1)*PARENTS_PER_PAGE, _absCurrentPage*PARENTS_PER_PAGE) : [];\n\n    let html = slice.map(a => {\n        const isJ = a.statut === 'justified';\n        const chip = isJ ? '<div><span class=\"chip chip-orange\">Justifi\u00e9e<\/span><\/div>' : '<div><span class=\"chip chip-red\">Non justifi\u00e9e<\/span><\/div>';\n        const motif = a.motif ? `<div style=\"font-size:11.5px;color:var(--muted);margin-top:4px;\">${escHtml(a.motif)}<\/div>` : '';\n        const editBtn = `<button class=\"btn btn-outline btn-sm\" title=\"Modifier le statut \/ le motif\" onclick=\"openAbsenceEdit(${parseInt(a.id)})\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"\/><path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z\"\/><\/svg><\/button> `;\n        return `<tr>${bulkCell('absence', a.id)}<td data-label=\"\u00c9l\u00e8ve\"><b>${escHtml(a.nom_eleve)}<\/b><\/td><td data-label=\"Classe\"><span class=\"chip chip-blue\">${escHtml(a.classe)}<\/span><\/td><td data-label=\"Date\" class=\"date-cell\">${escHtml(formatToFrenchDate(a.date_absence))}<\/td><td data-label=\"P\u00e9riode\">${escHtml(a.demi_journee)}<\/td><td data-label=\"Statut\">${chip}${motif}<\/td><td data-label=\"Action\" style=\"white-space:nowrap;\">${editBtn}<button class=\"btn btn-outline btn-sm\" style=\"color:var(--red);\" onclick=\"deleteItemRecord('absence', ${parseInt(a.id)})\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"\/><path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\"\/><path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"\/><line x1=\"10\" y1=\"11\" x2=\"10\" y2=\"17\"\/><line x1=\"14\" y1=\"11\" x2=\"14\" y2=\"17\"\/><\/svg><\/button><\/td><\/tr>`;\n    }).join('');\n    const _absActive = _absFiltersActive();\n    document.getElementById('a-table-absences').innerHTML = html || ('<tr><td colspan=\"7\" style=\"text-align:center;color:var(--muted);\">' + (_absActive ? 'Aucune absence ne correspond aux filtres.' : 'Historique vide.') + '<\/td><\/tr>');\n    _updateRegistreMeta('abs', total, (ouzoudSessionData && ouzoudSessionData.absences) ? ouzoudSessionData.absences.length : 0, _absActive);\n    _renderPaginationBar('absences', total, totalPages, _absCurrentPage, '_absRenderPage');\n    bulkSync('absence');\n    if (typeof updateScrollIndicators === 'function') updateScrollIndicators();\n}\n\/\/ \u2500\u2500 Pagination annonces \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet _annCurrentPage = 1;\nfunction _annRenderPage(page) {\n    const list = ouzoudSessionData ? ouzoudSessionData.annonces : [];\n    const total = list ? list.length : 0;\n    const totalPages = Math.max(1, Math.ceil(total \/ PARENTS_PER_PAGE));\n    _annCurrentPage = Math.min(Math.max(1, page), totalPages);\n    const slice = list ? list.slice((_annCurrentPage-1)*PARENTS_PER_PAGE, _annCurrentPage*PARENTS_PER_PAGE) : [];\n\n    let html = slice.map(a => {\n        let prioriteChip = a.priorite === 'haute' ? 'chip-red' : 'chip-blue';\n        let prioriteLabel = a.priorite === 'haute' ? 'Urgente' : 'Normale';\n        let prioriteAr = a.priorite === 'haute' ? 'Urgent' : 'Normale';\n        return `<tr>${bulkCell('annonce', a.id)}<td data-label=\"Titre\"><b>${escHtml(a.titre)}<\/b><\/td><td data-label=\"Priorit\u00e9\"><span class=\"chip ${prioriteChip}\">${escHtml(prioriteLabel)}<\/span><\/td><td data-label=\"Document\">${a.fichier_joint ? `<button class=\"btn btn-outline btn-sm ouzoud-file-btn\" data-fileurl=\"${escHtml(a.fichier_joint)}\" style=\"height:26px;padding:2px 7px;font-size:10px;white-space:nowrap;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:3px;\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"\/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"\/><path d=\"M10 9H8\"\/><path d=\"M16 13H8\"\/><path d=\"M16 17H8\"\/><\/svg>Ouvrir<\/button>` : '<span style=\"color:var(--muted);\">\u2014<\/span>'}<\/td><td data-label=\"Action\" style=\"white-space:nowrap;min-width:92px;\"><button class=\"btn btn-outline btn-sm\" title=\"Modifier l'annonce\" onclick=\"openAnnonceEdit(${parseInt(a.id)})\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"\/><path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z\"\/><\/svg><\/button> <button class=\"btn btn-outline btn-sm\" style=\"color:var(--red);\" onclick=\"deleteItemRecord('annonce', ${parseInt(a.id)})\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"\/><path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\"\/><path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"\/><line x1=\"10\" y1=\"11\" x2=\"10\" y2=\"17\"\/><line x1=\"14\" y1=\"11\" x2=\"14\" y2=\"17\"\/><\/svg><\/button><\/td><\/tr>`;\n    }).join('');\n    document.getElementById('a-table-annonces').innerHTML = html || '<tr><td colspan=\"5\" style=\"text-align:center;color:var(--muted);\">Aucune annonce.<\/td><\/tr>';\n    _renderPaginationBar('annonces', total, totalPages, _annCurrentPage, '_annRenderPage');\n    bulkSync('annonce');\n    if (typeof updateScrollIndicators === 'function') updateScrollIndicators();\n}\n\n\/\/ \u2500\u2500 Pagination devoirs (admin) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet _devCurrentPage = 1;\nfunction _devRenderPage(page) {\n    const list = _devFilteredList();\n    const total = list ? list.length : 0;\n    const totalPages = Math.max(1, Math.ceil(total \/ PARENTS_PER_PAGE));\n    _devCurrentPage = Math.min(Math.max(1, page), totalPages);\n    const slice = list ? list.slice((_devCurrentPage-1)*PARENTS_PER_PAGE, _devCurrentPage*PARENTS_PER_PAGE) : [];\n\n    let html = slice.map(d => {\n        let fileBtn = d.fichier_joint\n            ? `<button class=\"btn btn-outline btn-sm ouzoud-file-btn\" data-fileurl=\"${escHtml(d.fichier_joint)}\" style=\"height:26px;padding:2px 7px;font-size:10px;white-space:nowrap;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:3px;\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"\/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"\/><path d=\"M10 9H8\"\/><path d=\"M16 13H8\"\/><path d=\"M16 17H8\"\/><\/svg>Ouvrir<\/button>`\n            : '<span style=\"color:var(--muted);\">\u2014<\/span>';\n        const publiePar = d.publie_par && d.publie_par !== '' ? d.publie_par : 'Administration';\n        const isAdmin = publiePar === 'Administration';\n        \/\/ On n'affiche que le nom de l'enseignant : le code (ex. \"(ENS-2026-2177)\") reste\n        \/\/ en base pour le filtrage\/la suppression mais n'est jamais montr\u00e9 \u00e0 l'\u00e9cran.\n        const publieParAffiche = publiePar.replace(\/\\s*\\(ENS-\\d{4}-\\d{4}\\)\\s*$\/i, '').trim() || publiePar;\n        const badgeStyle = isAdmin\n            ? 'background:#FEF3C7;color:#92400E;'\n            : 'background:#D1FAE5;color:#065F46;';\n        return `<tr>\n            ${bulkCell('devoir', d.id)}\n            <td data-label=\"Classe\"><span class=\"chip chip-blue\">${escHtml(d.classe)}<\/span><\/td>\n            <td data-label=\"Mati\u00e8re\">${escHtml(d.matiere)}<\/td>\n            <td data-label=\"Titre\"><b>${escHtml(d.titre)}<\/b><\/td>\n            <td data-label=\"Publi\u00e9 par\"><span style=\"font-size:11.5px;padding:2px 8px;border-radius:20px;font-weight:600;${badgeStyle}\">${escHtml(publieParAffiche)}<\/span><\/td>\n            <td data-label=\"Date Butoir\" class=\"date-cell\">${escHtml(formatToFrenchDate(d.date_butoir))}<\/td>\n            <td data-label=\"Document\">${fileBtn}<\/td>\n            <td data-label=\"Action\" style=\"white-space:nowrap;min-width:92px;\">\n                <button class=\"btn btn-outline btn-sm\" title=\"Modifier le devoir\" onclick=\"openDevoirEdit(${parseInt(d.id)})\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"\/><path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z\"\/><\/svg><\/button>\n                <button class=\"btn btn-outline btn-sm\" style=\"color:var(--red);\" onclick=\"deleteItemRecord('devoir', ${parseInt(d.id)})\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"\/><path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\"\/><path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"\/><line x1=\"10\" y1=\"11\" x2=\"10\" y2=\"17\"\/><line x1=\"14\" y1=\"11\" x2=\"14\" y2=\"17\"\/><\/svg><\/button>\n            <\/td>\n        <\/tr>`;\n    }).join('');\n    const _devActive = _devFiltersActive();\n    document.getElementById('a-table-devoirs').innerHTML = html || ('<tr><td colspan=\"8\" style=\"text-align:center;color:var(--muted);padding:20px;\">' + (_devActive ? 'Aucun devoir ne correspond \u00e0 la recherche.' : 'Aucun devoir publi\u00e9 pour le moment.') + '<\/td><\/tr>');\n    _updateRegistreMeta('dev', total, (ouzoudSessionData && ouzoudSessionData.devoirs) ? ouzoudSessionData.devoirs.length : 0, _devActive);\n    _renderPaginationBar('devoirs', total, totalPages, _devCurrentPage, '_devRenderPage');\n    bulkSync('devoir');\n    if (typeof updateScrollIndicators === 'function') updateScrollIndicators();\n}\n\nlet _parentCurrentPage = 1;\nlet _parentFilteredList = null; \/\/ null = pas de filtre actif\n\nfunction _parentsBuildRows(list) {\n    if (!list || list.length === 0) return '';\n    return list.map(p => `<tr>${bulkCell('parent', p.id)}<td data-label=\"Parent\"><b>${escHtml(p.nom_complet)}<\/b><\/td><td data-label=\"Code Parent\"><span style=\"font-family:monospace;font-size:12px;font-weight:600;color:var(--blue-dark);background:var(--blue-light);padding:3px 8px;border-radius:6px;\">${escHtml(p.code_parent)}<\/span><\/td><td data-label=\"Email\">${escHtml(p.email||'-')}<\/td><td data-label=\"Enfants Rattach\u00e9s\">${p.enfants ? escHtml(p.enfants.replace(\/\\|\/g, ', ')) : '<i style=\"color:var(--muted)\">Aucun<\/i>'}<\/td><td data-label=\"Actions\"><button class=\"btn btn-outline btn-sm\" onclick=\"openParentModal(${parseInt(p.id)})\">\u00c9diter<\/button> <button class=\"btn btn-outline btn-sm\" style=\"color:var(--red);\" onclick=\"deleteItemRecord('parent', ${parseInt(p.id)})\">Suppr.<\/button><\/td><\/tr>`).join('');\n}\n\nfunction _parentsRenderPage(page) {\n    const list = _parentFilteredList !== null ? _parentFilteredList : (ouzoudSessionData ? ouzoudSessionData.parents : []);\n    const total = list ? list.length : 0;\n    const totalPages = Math.max(1, Math.ceil(total \/ PARENTS_PER_PAGE));\n    _parentCurrentPage = Math.min(Math.max(1, page), totalPages);\n\n    const start = (_parentCurrentPage - 1) * PARENTS_PER_PAGE;\n    const slice = list ? list.slice(start, start + PARENTS_PER_PAGE) : [];\n\n    const tbody = document.getElementById('a-table-parents');\n    const emptyMsg = document.getElementById('parents-search-empty');\n\n    tbody.innerHTML = _parentsBuildRows(slice) || '<tr><td colspan=\"6\" style=\"text-align:center;color:var(--muted);\">Aucun parent enregistr\u00e9.<\/td><\/tr>';\n    emptyMsg.style.display = (_parentFilteredList !== null && total === 0) ? 'block' : 'none';\n    _renderPaginationBar('parents', total, totalPages, _parentCurrentPage, '_parentsRenderPage');\n    bulkSync('parent');\n    if (typeof updateScrollIndicators === 'function') updateScrollIndicators();\n}\n\nfunction renderAdminPanels() {\n    populateAllMatiereSelects();\n    \/\/ R\u00e9initialiser la recherche \u00e0 chaque rechargement\n    const searchInput = document.getElementById('parents-search-input');\n    if (searchInput) { searchInput.value = ''; }\n    const emptyMsg = document.getElementById('parents-search-empty');\n    if (emptyMsg) { emptyMsg.style.display = 'none'; }\n\n    _parentFilteredList = null;\n    _parentCurrentPage = 1;\n    _parentsRenderPage(1);\n\n    _absCurrentPage = 1;\n    _annCurrentPage = 1;\n    _devCurrentPage = 1;\n    _absRefreshClasseOptions();\n    _absRenderPage(1);\n    _annRenderPage(1);\n    _devRenderPage(1);\n\n    _ensFilteredList = null;\n    _ensCurrentPage  = 1;\n    _ensRenderPage(1);\n\n    const ensSearch = document.getElementById('ens-search-input');\n    if (ensSearch) ensSearch.value = '';\n    const ensEmpty = document.getElementById('ens-search-empty');\n    if (ensEmpty) ensEmpty.style.display = 'none';\n\n    updateAbsenceClasseDropdown();\n    updateAbsencesStudentsDropdown();\n    restoreLastAbsenceClass();\n    updateADevoirClasseDropdown();\n\n    \/\/ Date d'absence par d\u00e9faut = aujourd'hui\n    const absDateEl = document.getElementById('abs-date');\n    if (absDateEl && !absDateEl.value) absDateEl.value = getTodaySQLDate();\n\n    \/\/ Date par d\u00e9faut = demain\n    const aDate = document.getElementById('a-dev-date');\n    if (aDate && !aDate.value) aDate.value = getTomorrowSQLDate();\n\n    if (typeof updateScrollIndicators === 'function') updateScrollIndicators();\n}\n\n\/\/ \u2500\u2500 Enseignants : pagination, rendu, modal, CSV \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet _ensFilteredList = null;\nlet _ensCurrentPage  = 1;\nconst ENS_PER_PAGE   = 20;\n\nfunction _ensBuildRows(list) {\n    return list.map(e => `<tr>\n        ${bulkCell('enseignant', e.id)}\n        <td data-label=\"Enseignant\"><b>${escHtml(e.nom_complet)}<\/b><\/td>\n        <td data-label=\"Code\"><span style=\"font-family:monospace;font-size:12px;font-weight:600;color:var(--purple);background:#EDE9FE;padding:3px 8px;border-radius:6px;\">${escHtml(e.code_enseignant)}<\/span><\/td>\n        <td data-label=\"Mati\u00e8re(s)\">${escHtml(e.matieres||'\u2014')}<\/td>\n        <td data-label=\"Classe(s)\" style=\"font-size:12px;color:var(--muted);\">${escHtml(e.classes||'\u2014')}<\/td>\n        <td data-label=\"Actions\"><button class=\"btn btn-outline btn-sm\" onclick=\"openEnsModal(${parseInt(e.id)})\">\u00c9diter<\/button> <button class=\"btn btn-outline btn-sm\" style=\"color:var(--red);\" onclick=\"deleteItemRecord('enseignant',${parseInt(e.id)})\">Suppr.<\/button><\/td>\n    <\/tr>`).join('');\n}\n\nfunction _ensRenderPage(page) {\n    const all   = _ensFilteredList !== null ? _ensFilteredList : (ouzoudSessionData ? (ouzoudSessionData.enseignants||[]) : []);\n    const total = all.length;\n    const totalPages = Math.max(1, Math.ceil(total \/ ENS_PER_PAGE));\n    _ensCurrentPage  = Math.min(Math.max(1, page), totalPages);\n    const start = (_ensCurrentPage - 1) * ENS_PER_PAGE;\n    const slice = all.slice(start, start + ENS_PER_PAGE);\n    const tbody = document.getElementById('a-table-enseignants');\n    if (tbody) tbody.innerHTML = slice.length ? _ensBuildRows(slice) : '<tr><td colspan=\"6\" style=\"text-align:center;color:var(--muted);padding:24px;\">Aucun enseignant enregistr\u00e9.<\/td><\/tr>';\n    _renderPaginationBar('ens', total, totalPages, _ensCurrentPage, '_ensRenderPage');\n    bulkSync('enseignant');\n}\n\nfunction filterEnseignantsTable(query) {\n    function norm(s) { return (s||'').normalize('NFD').replace(\/[\\u0300-\\u036f]\/g,'').toLowerCase(); }\n    query = norm(query.trim());\n    const emptyMsg = document.getElementById('ens-search-empty');\n    if (!query) {\n        _ensFilteredList = null;\n        if (emptyMsg) emptyMsg.style.display = 'none';\n    } else {\n        const all = ouzoudSessionData ? (ouzoudSessionData.enseignants||[]) : [];\n        _ensFilteredList = all.filter(e => norm([e.nom_complet,e.code_enseignant,e.matieres,e.classes].filter(Boolean).join(' ')).includes(query));\n        if (emptyMsg) emptyMsg.style.display = (_ensFilteredList.length === 0) ? 'block' : 'none';\n    }\n    _ensRenderPage(1);\n}\n\n\/\/ \u2500\u2500 Modal enseignant \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/\/ Table niveau -> cat\u00e9gorie, d\u00e9riv\u00e9e de ouzoudSessionData.niveaux_categories\n\/\/ (source unique de v\u00e9rit\u00e9 envoy\u00e9e par le serveur, cf. ouzoud_get_niveaux_categories()).\nfunction _ensNiveauToCategorieMap() {\n    const cats = (ouzoudSessionData && ouzoudSessionData.niveaux_categories) ? ouzoudSessionData.niveaux_categories : {};\n    const map = {};\n    for (const cat in cats) { (cats[cat] || []).forEach(n => { map[n] = cat; }); }\n    return map;\n}\n\n\/\/ Coche\/d\u00e9coche toutes les cases \"cat\u00e9gorie\".\n\/\/ isChecked = true  -> tout est affich\u00e9 (utilis\u00e9 en modification, pour retrouver les classes d\u00e9j\u00e0 coch\u00e9es)\n\/\/ isChecked = false -> tout est masqu\u00e9 (utilis\u00e9 \u00e0 l'ajout : aucune cat\u00e9gorie pr\u00e9-s\u00e9lectionn\u00e9e)\nfunction _resetEnsCategoryCheckboxes(isChecked) {\n    document.querySelectorAll('#e-categories-checkboxes .e-cat-toggle').forEach(cb => { cb.checked = isChecked; });\n}\n\n\/\/ Affiche\/masque les groupes de classes selon les cat\u00e9gories coch\u00e9es.\n\/\/ Ne modifie jamais les cases \"classe\" elles-m\u00eames : une classe coch\u00e9e puis\n\/\/ masqu\u00e9e reste coch\u00e9e (et donc bien prise en compte \u00e0 l'enregistrement).\nfunction _filterEnsClassesByCategory() {\n    const wrap = document.getElementById('e-classes-checkboxes');\n    if (!wrap) return;\n    const checkedCats = Array.from(document.querySelectorAll('#e-categories-checkboxes .e-cat-toggle:checked')).map(cb => cb.value);\n    wrap.querySelectorAll('label[data-categorie]').forEach(lbl => {\n        lbl.style.display = checkedCats.includes(lbl.dataset.categorie) ? 'flex' : 'none';\n    });\n}\n\nfunction _buildEnsClassesCheckboxes(selectedClasses) {\n    const wrap = document.getElementById('e-classes-checkboxes');\n    if (!wrap) return;\n    const config = (ouzoudSessionData && ouzoudSessionData.classes_config) ? ouzoudSessionData.classes_config : {};\n    const niveauCat = _ensNiveauToCategorieMap();\n    const sel = (selectedClasses || []).map(c => c.trim());\n    wrap.innerHTML = '';\n    for (const niveau in config) {\n        const cat = niveauCat[niveau] || '';\n        (config[niveau] || []).forEach(classe => {\n            const checked = sel.includes(classe) ? ' checked' : '';\n            wrap.innerHTML += `<label data-categorie=\"${escHtml(cat)}\" style=\"display:flex;align-items:center;gap:5px;font-size:13px;cursor:pointer;white-space:nowrap;min-width:80px;\">\n                <input type=\"checkbox\" value=\"${escHtml(classe)}\" style=\"accent-color:var(--purple);\"${checked}> ${escHtml(classe)}\n            <\/label>`;\n        });\n    }\n    _filterEnsClassesByCategory();\n}\n\nfunction _buildEnsMatieresCheckboxes(selectedMatieres) {\n    const wrap = document.getElementById('e-matieres-checkboxes');\n    if (!wrap) return;\n    const matieres = (ouzoudSessionData && Array.isArray(ouzoudSessionData.matieres_config) && ouzoudSessionData.matieres_config.length > 0)\n        ? ouzoudSessionData.matieres_config : [];\n    const sel = (selectedMatieres || []).map(m => m.trim());\n    wrap.innerHTML = '';\n    if (!matieres.length) {\n        wrap.innerHTML = '<span style=\"font-size:12px;color:var(--muted);\">Aucune mati\u00e8re configur\u00e9e. Ajoutez-en dans R\u00e9glages Espace Parents.<\/span>';\n        return;\n    }\n    matieres.forEach(m => {\n        const checked = sel.includes(m) ? ' checked' : '';\n        wrap.innerHTML += `<label style=\"display:flex;align-items:center;gap:5px;font-size:13px;cursor:pointer;white-space:nowrap;min-width:80px;\">\n            <input type=\"checkbox\" value=\"${escHtml(m)}\" style=\"accent-color:var(--purple);\"${checked}> ${escHtml(m)}\n        <\/label>`;\n    });\n}\n\nfunction openEnsModal(id) {\n    if (id === 0) {\n        _resetEnsCategoryCheckboxes(false); \/\/ Ajout : aucune cat\u00e9gorie coch\u00e9e par d\u00e9faut\n        document.getElementById('e-id').value = 0;\n        document.getElementById('e-nom').value = '';\n        document.getElementById('me-title').innerText = 'Ajouter un Enseignant';\n        _buildEnsMatieresCheckboxes([]);\n        _buildEnsClassesCheckboxes([]);\n    } else {\n        _resetEnsCategoryCheckboxes(true); \/\/ Modification : tout affich\u00e9 pour retrouver les classes d\u00e9j\u00e0 coch\u00e9es\n        const e = (ouzoudSessionData && ouzoudSessionData.enseignants||[]).find(x => x.id == id);\n        if (!e) return;\n        document.getElementById('e-id').value = e.id;\n        document.getElementById('e-nom').value = e.nom_complet;\n        document.getElementById('me-title').innerText = 'Modifier : ' + e.nom_complet;\n        const matieres = (e.matieres||'').split(',').map(m => m.trim());\n        _buildEnsMatieresCheckboxes(matieres);\n        const classes = (e.classes||'').split(',').map(c => c.trim());\n        _buildEnsClassesCheckboxes(classes);\n    }\n    const btn = document.getElementById('btn-save-ens');\n    if (btn) { btn.disabled = false; btn.textContent = 'Enregistrer'; }\n    document.getElementById('m-ens-form').style.display = 'flex';\n}\n\nfunction submitEnsForm() {\n    const id         = parseInt(document.getElementById('e-id').value) || 0;\n    const nom        = document.getElementById('e-nom').value.trim();\n    const matWrap    = document.getElementById('e-matieres-checkboxes');\n    const matieresArr= matWrap ? Array.from(matWrap.querySelectorAll('input[type=checkbox]:checked')).map(cb => cb.value) : [];\n    const matieres   = matieresArr.join(', ');\n    const wrap     = document.getElementById('e-classes-checkboxes');\n    const classes  = wrap ? Array.from(wrap.querySelectorAll('input[type=checkbox]:checked')).map(cb => cb.value) : [];\n\n    if (!nom)              { showToast('Le nom complet est obligatoire.', 'error'); return; }\n    if (!matieresArr.length){ showToast('Cochez au moins une mati\u00e8re.', 'error'); return; }\n    if (!classes.length)   { showToast('Cochez au moins une classe.', 'error'); return; }\n\n    const btn = document.getElementById('btn-save-ens');\n    if (btn) { btn.disabled = true; btn.textContent = 'Enregistrement\u2026'; }\n\n    let p = new URLSearchParams();\n    p.append('action', 'ouzoud_ajax_save_data');\n    p.append('action_type', 'save_enseignant');\n    p.append('nonce', OuzoudAjax.nonce);\n    p.append('id', id);\n    p.append('nom', nom);\n    p.append('matieres', matieres);\n    p.append('classes', classes.join(', '));\n\n    fetch(OuzoudAjax.url, { method: 'POST', body: p }).then(r => r.text()).then(text => {\n        let res; try { res = JSON.parse(text); } catch(ex) { res = { success: false, data: 'Erreur. Vous devez \u00eatre administrateur.' }; }\n        if (res.success) {\n            const code = res.data.code_enseignant;\n            if (res.data.is_new) {\n                showToast('Enseignant cr\u00e9\u00e9. Code : ' + code, 'success', 6000);\n                setTimeout(() => showToast('Code : ' + code + ' \u2014 \u00c0 communiquer \u00e0 l\\'enseignant.', 'success', 10000), 200);\n            } else {\n                showToast('Fiche enseignant mise \u00e0 jour.', 'success');\n            }\n            closeOuzoudModal('m-ens-form');\n            loadApplicationWorkspace();\n        } else {\n            showToast(res.data || '\u00c9chec de l\\'enregistrement.', 'error');\n            if (btn) { btn.disabled = false; btn.textContent = 'Enregistrer'; }\n        }\n    }).catch(() => {\n        showToast('Erreur r\u00e9seau.', 'error');\n        if (btn) { btn.disabled = false; btn.textContent = 'Enregistrer'; }\n    });\n}\n\n\/\/ \u2500\u2500 Suppression via deleteItemRecord (message de confirmation) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/\/ Ajout du cas 'enseignant' dans deleteItemRecord \u2014 inject\u00e9 dynamiquement\n(function patchDeleteItemRecord() {\n    const orig = window.deleteItemRecord;\n    window.deleteItemRecord = function(target, id) {\n        if (target === 'enseignant' && ouzoudSessionData && ouzoudSessionData.enseignants) {\n            const e = ouzoudSessionData.enseignants.find(x => x.id == id);\n            const msg = e\n                ? `Supprimer d\u00e9finitivement l'enseignant \"${_safeConfirmStr(e.nom_complet)}\" (${_safeConfirmStr(e.code_enseignant)}) ?\\n\\nCette action est irr\u00e9versible.`\n                : 'Confirmer la suppression d\u00e9finitive ?';\n            if (!confirm(msg)) return;\n            let p = new URLSearchParams();\n            p.append('action','ouzoud_ajax_delete_data'); p.append('nonce',OuzoudAjax.nonce);\n            p.append('target','enseignant'); p.append('id',id);\n            fetch(OuzoudAjax.url,{method:'POST',body:p}).then(r=>r.text()).then(text=>{\n                let res; try{res=JSON.parse(text);}catch(ex){res={success:false,data:'Erreur r\u00e9seau.'};}\n                if(res.success){showToast('Enseignant supprim\u00e9.','info');loadApplicationWorkspace();}\n                else{showToast(res.data||'\u00c9chec de la suppression.','error');}\n            }).catch(()=>showToast('Erreur r\u00e9seau.','error'));\n        } else {\n            orig(target, id);\n        }\n    };\n})();\n\n\/\/ \u2500\u2500 Export CSV \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction exportEnseignantsCsv() {\n    let p = new URLSearchParams();\n    p.append('action','ouzoud_ajax_refresh_data'); p.append('nonce',OuzoudAjax.nonce);\n    fetch(OuzoudAjax.url,{method:'POST',body:p}).then(r=>r.json()).then(res=>{\n        if (res.success) {\n            const saved = { _loginType: ouzoudSessionData ? ouzoudSessionData._loginType : null };\n            ouzoudSessionData = res.data;\n            Object.assign(ouzoudSessionData, saved);\n        }\n        _doExportEnseignantsCsv();\n    }).catch(()=>showToast('Erreur r\u00e9seau. Export annul\u00e9.','error'));\n}\n\nfunction _doExportEnseignantsCsv() {\n    const list = ouzoudSessionData && ouzoudSessionData.enseignants;\n    if (!list || list.length === 0) { showToast('Aucun enseignant \u00e0 exporter.','info'); return; }\n    const sep = ';';\n    const bom = '\\uFEFF';\n    \/\/ Les classes sont stock\u00e9es en base s\u00e9par\u00e9es par virgule (\"1APG-1, 4APG-2\") mais le\n    \/\/ format d'import CSV attendu (voir mod\u00e8le) utilise le tiret vertical (\"1APG-1|4APG-2\") :\n    \/\/ on convertit donc le s\u00e9parateur ici pour que le fichier export\u00e9 soit r\u00e9importable tel quel.\n    const classesToPipe = (str) => (str || '').split(',').map(c => c.trim()).filter(Boolean).join('|');\n    let csv = bom + ['nom_complet','code_enseignant','matieres','classes'].join(sep) + '\\n';\n    list.forEach(e => {\n        csv += `\"${csvSafeField(e.nom_complet)}\"${sep}\"${csvSafeField(e.code_enseignant)}\"${sep}\"${csvSafeField(e.matieres)}\"${sep}\"${csvSafeField(classesToPipe(e.classes))}\"\\n`;\n    });\n    const blob = new Blob([csv],{type:'text\/csv;charset=utf-8;'});\n    const url  = URL.createObjectURL(blob);\n    const a    = document.createElement('a');\n    const today = new Date();\n    const ds = `${today.getFullYear()}${String(today.getMonth()+1).padStart(2,'0')}${String(today.getDate()).padStart(2,'0')}`;\n    a.href = url; a.download = `enseignants_ecole_ouzoud_${ds}.csv`;\n    document.body.appendChild(a); a.click(); document.body.removeChild(a);\n    URL.revokeObjectURL(url);\n    showToast(`${list.length} enseignant(s) export\u00e9(s) en CSV.`, 'success');\n}\n\n\/\/ \u2500\u2500 Import CSV enseignants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction openEnsImportModal() {\n    document.getElementById('ens-csv-file-input').value = '';\n    const rd = document.getElementById('ens-csv-import-result');\n    rd.style.display = 'none'; rd.innerHTML = '';\n    const btn = document.getElementById('btn-launch-ens-import');\n    btn.disabled = false; btn.innerHTML = '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><path d=\"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z\"\/><path d=\"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z\"\/><path d=\"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0\"\/><path d=\"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5\"\/><\/svg>Lancer l\\'importation';\n    btn.onclick = launchEnsCsvImport;\n    document.getElementById('m-ens-csv-import').style.display = 'flex';\n}\n\nfunction downloadEnsCsvTemplate() {\n    const year = new Date().getFullYear();\n    let csv = 'nom_complet,code_enseignant,matieres,classes\\n';\n    csv += `Mme F. Alaoui,ENS-${year}-1234,\"Maths, Fran\u00e7ais\",\"1APG-1|1APG-2|2APG-1\"\\n`;\n    csv += `M. K. Benali,,\"Arabe\",\"4APG-1|4APG-2\"\\n`;\n    csv += `Mme N. Tazi,,Fran\u00e7ais,\"3APG-1\"\\n`;\n    const blob = new Blob([csv],{type:'text\/csv;charset=utf-8;'});\n    const url  = URL.createObjectURL(blob);\n    const a    = document.createElement('a');\n    a.href = url; a.download = 'modele_import_enseignants.csv';\n    document.body.appendChild(a); a.click(); document.body.removeChild(a);\n    URL.revokeObjectURL(url);\n}\n\nfunction launchEnsCsvImport() {\n    const fi = document.getElementById('ens-csv-file-input');\n    if (!fi.files || fi.files.length === 0) { showToast('Veuillez s\u00e9lectionner un fichier CSV.','error'); return; }\n    const btn = document.getElementById('btn-launch-ens-import');\n    btn.disabled = true; btn.textContent = '\u23f3 Importation en cours...';\n    const fd = new FormData();\n    fd.append('action','ouzoud_ajax_import_ens_csv');\n    fd.append('nonce',OuzoudAjax.nonce);\n    fd.append('csv_file',fi.files[0]);\n    fetch(OuzoudAjax.url,{method:'POST',body:fd})\n    .then(r=>r.text()).then(text=>{\n        let res; try{res=JSON.parse(text);}catch(e){res={success:false,data:'\u00c9chec de l\\'importation. Vous devez \u00eatre administrateur.'};}\n        const rd = document.getElementById('ens-csv-import-result');\n        rd.style.display = 'block';\n        if (res.success) {\n            const d = res.data;\n            let errorsHtml = '';\n            if (d.errors && d.errors.length > 0) {\n                errorsHtml = `<div style=\"margin-top:10px;padding:10px 12px;background:#FEF3C7;border:1px solid #FCD34D;border-radius:8px;\">\n                    <p style=\"font-weight:700;color:#92400E;margin-bottom:6px;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z\"\/><path d=\"M12 9v4\"\/><path d=\"M12 17h.01\"\/><\/svg>${d.errors.length} ligne(s) avec erreur(s) :<\/p>\n                    <ul style=\"font-size:12px;color:#78350F;padding-left:16px;line-height:1.7;\">${d.errors.map(e=>`<li>${escHtml(e)}<\/li>`).join('')}<\/ul>\n                <\/div>`;\n            }\n            rd.innerHTML = `<div style=\"padding:14px;background:#F5F3FF;border:1px solid #C4B5FD;border-radius:10px;\">\n                <p style=\"font-weight:700;color:#4C1D95;margin-bottom:8px;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"\/><path d=\"m9 11 3 3L22 4\"\/><\/svg>Importation r\u00e9ussie !<\/p>\n                <div style=\"display:flex;gap:16px;flex-wrap:wrap;\">\n                    <span style=\"font-size:13px;color:#6D28D9;\"><b>${d.inserted}<\/b> enseignant(s) ajout\u00e9(s)<\/span>\n                    <span style=\"font-size:13px;color:#0369A1;\"><b>${d.updated}<\/b> fiche(s) mise(s) \u00e0 jour<\/span>\n                    ${d.skipped>0?`<span style=\"font-size:13px;color:var(--muted);\"><b>${d.skipped}<\/b> ligne(s) ignor\u00e9e(s)<\/span>`:''}\n                <\/div>\n            <\/div>${errorsHtml}`;\n            btn.disabled = false; btn.innerHTML = '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"\/><path d=\"m9 11 3 3L22 4\"\/><\/svg>Termin\u00e9';\n            btn.onclick = function(){ closeOuzoudModal('m-ens-csv-import'); };\n            loadApplicationWorkspace();\n        } else {\n            rd.innerHTML = `<div style=\"padding:14px;background:var(--red-light);border:1px solid #FCA5A5;border-radius:10px;\"><p style=\"font-weight:700;color:var(--red-dark);\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><circle cx=\"12\" cy=\"12\" r=\"10\"\/><path d=\"m15 9-6 6\"\/><path d=\"m9 9 6 6\"\/><\/svg>Erreur : ${escHtml(res.data||'\u00c9chec.')}<\/p><\/div>`;\n            btn.disabled = false; btn.innerHTML = '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><path d=\"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z\"\/><path d=\"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z\"\/><path d=\"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0\"\/><path d=\"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5\"\/><\/svg>Lancer l\\'importation';\n        }\n    }).catch(()=>{\n        btn.disabled=false; btn.innerHTML='<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><path d=\"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z\"\/><path d=\"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z\"\/><path d=\"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0\"\/><path d=\"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5\"\/><\/svg>Lancer l\\'importation';\n        showToast('Erreur r\u00e9seau lors de l\\'importation.','error');\n    });\n}\n\/\/ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\n\/\/ \u2500\u2500 Workspace Admin : cascade niveau \u2192 classe (devoirs) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/\/ \u2500\u2500 Recherche rapide de classe (formulaires \"Publier un devoir\" admin & enseignant) \u2500\u2500\nlet _aDevSelectedClasse = '';\nlet _tDevSelectedClasse = '';\nlet _classeSearchActiveIdx = {};\nlet _classeSearchMatches = {};\n\n\/\/ Niveaux des cat\u00e9gories activ\u00e9es dans R\u00e9glages > Espace Parents (utilis\u00e9 par le formulaire Devoir admin)\nconst OUZOUD_ACTIVE_NIVEAUX = [\"PS\",\"MS\",\"GS\",\"1APG\",\"2APG\",\"3APG\",\"4APG\",\"5APG\",\"6APG\"];\n\nfunction getAllClassesList(onlyActive) {\n    const config = (ouzoudSessionData && ouzoudSessionData.classes_config) ? ouzoudSessionData.classes_config : {};\n    const list = [];\n    for (const niveau in config) {\n        if (onlyActive && !OUZOUD_ACTIVE_NIVEAUX.includes(niveau)) continue;\n        (config[niveau] || []).forEach(classe => list.push({ niveau: niveau, classe: classe }));\n    }\n    return list;\n}\n\nfunction filterClasseSearch(prefix) {\n    const input = document.getElementById(prefix + '-classe-search');\n    const box = document.getElementById(prefix + '-classe-search-results');\n    if (!input || !box) return;\n    const q = input.value.trim().toLowerCase();\n\n    \/\/ [SEC] : dans l'espace enseignant (prefix 't-dev'), limiter la recherche\n    \/\/ aux classes assign\u00e9es \u00e0 cet enseignant uniquement.\n    let all = getAllClassesList(prefix === 'a-dev');\n    if (prefix === 't-dev' && ouzoudSessionData && Array.isArray(ouzoudSessionData.classes) && ouzoudSessionData.classes.length > 0) {\n        const teacherClasses = ouzoudSessionData.classes.map(c => c.trim());\n        all = all.filter(c => teacherClasses.includes(c.classe));\n    }\n\n    \/\/ [Admin] texte modifi\u00e9 apr\u00e8s une s\u00e9lection : la classe choisie n'est plus valable\n    if (prefix === 'a-dev' && _aDevSelectedClasse && input.value.trim() !== _aDevSelectedClasse) _aDevClearClasse(false);\n    if (prefix === 't-dev' && _tDevSelectedClasse && input.value.trim() !== _tDevSelectedClasse) _tDevClearClasse(false);\n\n    if (!q) { box.classList.remove('open'); box.innerHTML = ''; _classeSearchMatches[prefix] = []; _classeSearchActiveIdx[prefix] = -1; return; }\n\n    _classeSearchMatches[prefix] = all.filter(c => (c.classe || '').toLowerCase().includes(q)).slice(0, 30);\n    _classeSearchActiveIdx[prefix] = -1;\n    const matches = _classeSearchMatches[prefix];\n\n    if (matches.length === 0) {\n        box.innerHTML = '<div class=\"eleve-search-empty\">Aucune classe trouv\u00e9e.<\/div>';\n    } else {\n        box.innerHTML = matches.map((c, i) =>\n            `<div class=\"eleve-search-item\" data-idx=\"${i}\" onclick=\"selectClasseFromSearch('${prefix}', ${i})\">\n                <span class=\"esi-name\">${escHtml(c.classe)}<\/span>\n                <span class=\"esi-classe\">${escHtml(c.niveau)}<\/span>\n            <\/div>`\n        ).join('');\n    }\n    box.classList.add('open');\n}\n\nfunction classeSearchKeydown(evt, prefix) {\n    const box = document.getElementById(prefix + '-classe-search-results');\n    const matches = _classeSearchMatches[prefix] || [];\n    if (!box || !box.classList.contains('open') || matches.length === 0) return;\n    const items = box.querySelectorAll('.eleve-search-item');\n    let idx = _classeSearchActiveIdx[prefix] ?? -1;\n    if (evt.key === 'ArrowDown') {\n        evt.preventDefault();\n        idx = Math.min(idx + 1, matches.length - 1);\n        _classeSearchActiveIdx[prefix] = idx;\n        items.forEach((it, i) => it.classList.toggle('active', i === idx));\n        if (items[idx]) items[idx].scrollIntoView({ block: 'nearest' });\n    } else if (evt.key === 'ArrowUp') {\n        evt.preventDefault();\n        idx = Math.max(idx - 1, 0);\n        _classeSearchActiveIdx[prefix] = idx;\n        items.forEach((it, i) => it.classList.toggle('active', i === idx));\n        if (items[idx]) items[idx].scrollIntoView({ block: 'nearest' });\n    } else if (evt.key === 'Enter') {\n        evt.preventDefault();\n        if (idx >= 0) selectClasseFromSearch(prefix, idx);\n        else if (matches.length === 1) selectClasseFromSearch(prefix, 0);\n    } else if (evt.key === 'Escape') {\n        box.classList.remove('open');\n    }\n}\n\nfunction selectClasseFromSearch(prefix, idx) {\n    const match = (_classeSearchMatches[prefix] || [])[idx];\n    if (!match) return;\n\n    const niveauSel = document.getElementById(prefix + '-niveau');\n    const classeSel = document.getElementById(prefix + '-classe');\n\n    niveauSel.value = match.niveau;\n    if (prefix === 'a-dev') updateADevoirClasseDropdown();\n    else if (prefix === 't-dev') updateTDevoirClasseDropdown();\n    classeSel.value = match.classe;\n\n    document.getElementById(prefix + '-classe-search').value = match.classe;\n    document.getElementById(prefix + '-classe-search-results').classList.remove('open');\n    if (prefix === 't-dev') {\n        _tDevSelectedClasse = match.classe;\n        const allCbT = document.getElementById('t-dev-all-classes');\n        if (allCbT) { allCbT.checked = false; toggleTDevAllClasses(); }\n        const allWrapT = document.getElementById('t-dev-all-wrap');\n        if (allWrapT) allWrapT.style.display = '';\n        const allLblT = document.getElementById('t-dev-all-label');\n        if (allLblT) allLblT.textContent = 'Envoyer \u00e0 toutes les classes de ce niveau (' + match.niveau + ')';\n    }\n    if (prefix === 'a-dev') {\n        _aDevSelectedClasse = match.classe;\n        const allCb = document.getElementById('a-dev-all-classes');\n        if (allCb) { allCb.checked = false; toggleADevAllClasses(); }\n        const allWrap = document.getElementById('a-dev-all-wrap');\n        if (allWrap) allWrap.style.display = '';\n        const allLbl = document.getElementById('a-dev-all-label');\n        if (allLbl) allLbl.textContent = 'Envoyer \u00e0 toutes les classes de ce niveau (' + match.niveau + ')';\n    }\n    showToast(`Classe ${match.classe} s\u00e9lectionn\u00e9e \u2014 ${match.niveau}.`, 'success');\n}\n\n\/\/ Ferme les listes de suggestions de classe au clic en dehors du champ\ndocument.addEventListener('click', function(evt) {\n    ['a-dev', 't-dev'].forEach(prefix => {\n        const wrap = document.getElementById(prefix + '-classe-search');\n        const box = document.getElementById(prefix + '-classe-search-results');\n        if (!wrap || !box) return;\n        if (evt.target !== wrap && !box.contains(evt.target)) box.classList.remove('open');\n    });\n});\n\n\/\/ Annule la classe choisie via la recherche (champ modifi\u00e9 ou formulaire r\u00e9initialis\u00e9)\nfunction _aDevClearClasse(clearInput) {\n    _aDevSelectedClasse = '';\n    const nv = document.getElementById('a-dev-niveau');\n    const cl = document.getElementById('a-dev-classe');\n    if (nv) nv.value = '';\n    if (cl) { cl.innerHTML = '<option value=\"\" disabled selected>S\u00e9lectionnez d\\'abord un niveau<\/option>'; cl.disabled = true; }\n    const cb = document.getElementById('a-dev-all-classes');\n    if (cb) cb.checked = false;\n    const w = document.getElementById('a-dev-all-wrap');\n    if (w) w.style.display = 'none';\n    if (clearInput) { const inp = document.getElementById('a-dev-classe-search'); if (inp) inp.value = ''; }\n}\n\nfunction _tDevClearClasse(clearInput) {\n    _tDevSelectedClasse = '';\n    const nv = document.getElementById('t-dev-niveau');\n    const cl = document.getElementById('t-dev-classe');\n    if (nv) nv.value = '';\n    if (cl) { cl.innerHTML = '<option value=\"\" disabled selected>S\u00e9lectionnez d\\'abord un niveau<\/option>'; cl.disabled = true; }\n    const cb = document.getElementById('t-dev-all-classes');\n    if (cb) cb.checked = false;\n    const w = document.getElementById('t-dev-all-wrap');\n    if (w) w.style.display = 'none';\n    if (clearInput) { const inp = document.getElementById('t-dev-classe-search'); if (inp) inp.value = ''; }\n}\n\nfunction updateADevoirClasseDropdown() {\n    const niveauSel = document.getElementById('a-dev-niveau');\n    const classeSel = document.getElementById('a-dev-classe');\n    if (!niveauSel || !classeSel) return;\n    const selectedNiveau = niveauSel.value;\n    const prevClasse = classeSel.value;\n    classeSel.innerHTML = '';\n    if (!selectedNiveau) {\n        classeSel.innerHTML = '<option value=\"\" disabled selected>S\u00e9lectionnez d\\'abord un niveau<\/option>';\n        classeSel.disabled = true;\n        return;\n    }\n    const config = (ouzoudSessionData && ouzoudSessionData.classes_config) ? ouzoudSessionData.classes_config : {};\n    const classes = config[selectedNiveau] || [];\n    if (classes.length === 0) {\n        classeSel.innerHTML = '<option value=\"\" disabled selected>Aucune classe configur\u00e9e<\/option>';\n        classeSel.disabled = true;\n    } else {\n        classeSel.innerHTML = '<option value=\"\" disabled selected>-- Choisir la classe --<\/option>';\n        classes.forEach(c => {\n            let opt = document.createElement('option');\n            opt.value = c; opt.textContent = c;\n            classeSel.appendChild(opt);\n        });\n        classeSel.disabled = false;\n        if (prevClasse && classes.includes(prevClasse)) classeSel.value = prevClasse; \/\/ conserve la classe choisie lors d'un rechargement\n        const cbAll = document.getElementById('a-dev-all-classes');\n        if (cbAll && cbAll.checked) classeSel.disabled = true;\n    }\n}\n\n\/\/ \u2500\u2500 Devoir : mati\u00e8re \"Autre\" (admin & enseignant) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndocument.addEventListener('change', function(e) {\n    if (e.target.id === 'a-dev-matiere') {\n        const wrap = document.getElementById('a-dev-autre-wrap');\n        if (wrap) wrap.style.display = (e.target.value === '__autre__') ? '' : 'none';\n    }\n    if (e.target.id === 't-dev-matiere') {\n        const wrap = document.getElementById('t-dev-autre-wrap');\n        if (wrap) wrap.style.display = (e.target.value === '__autre__') ? '' : 'none';\n    }\n});\n\n\/\/ \u2500\u2500 Devoir : mode lot (toutes les classes du niveau) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction toggleADevAllClasses() {\n    const cb = document.getElementById('a-dev-all-classes');\n    const classeSelect = document.getElementById('a-dev-classe');\n    if (!classeSelect) return;\n    classeSelect.disabled = !!(cb && cb.checked);\n    if (cb && cb.checked) {\n        classeSelect.dataset.prevValue = classeSelect.value;\n    } else {\n        if (classeSelect.dataset.prevValue) classeSelect.value = classeSelect.dataset.prevValue;\n    }\n}\n\nfunction toggleTDevAllClasses() {\n    const cb = document.getElementById('t-dev-all-classes');\n    const classeSelect = document.getElementById('t-dev-classe');\n    if (!classeSelect) return;\n    classeSelect.disabled = !!(cb && cb.checked);\n    if (cb && cb.checked) {\n        classeSelect.dataset.prevValue = classeSelect.value;\n    } else {\n        if (classeSelect.dataset.prevValue) classeSelect.value = classeSelect.dataset.prevValue;\n    }\n}\n\nfunction _getDevMatiere(prefix) {\n    const sel = document.getElementById(prefix+'-matiere');\n    if (!sel) return '';\n    if (sel.value === '__autre__') {\n        return (document.getElementById(prefix+'-matiere-autre') || {}).value || '';\n    }\n    return sel.value;\n}\n\nfunction _getDevClasses(prefix) {\n    const nivEl = document.getElementById(prefix+'-niveau');\n    const cbAll = document.getElementById(prefix+'-all-classes');\n    if (cbAll && cbAll.checked && nivEl && nivEl.value) {\n        const config = (ouzoudSessionData && ouzoudSessionData.classes_config) ? ouzoudSessionData.classes_config : {};\n        return config[nivEl.value] || [];\n    }\n    const classeEl = document.getElementById(prefix+'-classe');\n    return classeEl && classeEl.value ? [classeEl.value] : [];\n}\n\n\/\/ \u2500\u2500 Workspace Admin : publier un devoir \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction publishADevoir() {\n    const btnSubmit = document.querySelector('[onclick=\"publishADevoir()\"]');\n    \/\/ [FIX#19] : garde anti-double-soumission \u2014 coh\u00e9rent avec save_parent \/ save_absence\n    if (btnSubmit && btnSubmit.disabled) return;\n\n    const classes  = _getDevClasses('a-dev');\n    let matiere = _getDevMatiere('a-dev');\n    let titre   = document.getElementById('a-dev-titre').value.trim();\n    let devDate = document.getElementById('a-dev-date').value;\n    let fileInput = document.getElementById('a-dev-fichier');\n\n    const cbAll = document.getElementById('a-dev-all-classes');\n    const allClasses = cbAll && cbAll.checked;\n\n    if (!allClasses && classes.length === 0) { showToast(\"Recherchez et s\u00e9lectionnez une classe.\", 'error'); return; }\n    if (allClasses && classes.length === 0) { showToast(\"Aucune classe trouv\u00e9e pour ce niveau.\", 'error'); return; }\n    if (!matiere) { showToast(\"La mati\u00e8re est obligatoire.\", 'error'); return; }\n    if (!titre) { showToast(\"Le titre est obligatoire.\", 'error'); return; }\n    if (!devDate || !\/^\\d{4}-\\d{2}-\\d{2}$\/.test(devDate)) { showToast(\"Veuillez saisir une date de remise valide.\", 'error'); return; }\n\n    let file = null;\n    if (fileInput.files.length > 0) {\n        file = fileInput.files[0];\n        if (file.size > 2 * 1024 * 1024) { showToast('Le fichier d\u00e9passe la limite de 2 Mo.', 'error'); return; }\n    }\n\n    \/\/ Verrouiller le bouton\n    if (btnSubmit) { btnSubmit.disabled = true; btnSubmit.textContent = '\u23f3 Diffusion en cours\u2026'; }\n\n    const desc = document.getElementById('a-dev-desc').value;\n\n    function sendOne(classe, cb) {\n        let formData = new FormData();\n        formData.append('action', 'ouzoud_ajax_save_data');\n        formData.append('action_type', 'save_devoir');\n        formData.append('nonce', OuzoudAjax.nonce);\n        formData.append('classe', classe);\n        formData.append('matiere', matiere);\n        formData.append('titre', titre);\n        formData.append('desc', desc);\n        formData.append('date', devDate);\n        if (file) formData.append('dev_fichier', file);\n        fetch(OuzoudAjax.url, { method: 'POST', body: formData })\n            .then(r => r.text())\n            .then(text => { let res; try { res = JSON.parse(text); } catch(e) { res = { success: false }; } cb(res.success); })\n            .catch(() => cb(false));\n    }\n\n    let pending = classes.length;\n    let errors = 0;\n    classes.forEach(function(classe) {\n        sendOne(classe, function(ok) {\n            if (!ok) errors++;\n            pending--;\n            if (pending === 0) {\n                \/\/ [FIX#19] : d\u00e9verrouiller le bouton dans tous les cas (succ\u00e8s ET erreur)\n                if (btnSubmit) { btnSubmit.disabled = false; btnSubmit.textContent = '\\u{1F4E4} Diffuser le devoir'; }\n                if (errors === 0) {\n                    showToast(classes.length > 1 ? 'Devoir diffus\u00e9 \u00e0 ' + classes.length + ' classes avec succ\u00e8s.' : 'Devoir publi\u00e9 avec succ\u00e8s.', 'success');\n                    _aDevClearClasse(true);\n                    document.getElementById('a-dev-matiere').value = '';\n                    const aAutreWrap = document.getElementById('a-dev-autre-wrap');\n                    if (aAutreWrap) { aAutreWrap.style.display = 'none'; document.getElementById('a-dev-matiere-autre').value = ''; }\n                    const cbAllEl = document.getElementById('a-dev-all-classes');\n                    if (cbAllEl) cbAllEl.checked = false;\n                    document.getElementById('a-dev-titre').value = 'Devoir maison';\n                    document.getElementById('a-dev-desc').value = '';\n                    document.getElementById('a-dev-date').value = getTomorrowSQLDate();\n                    fileInput.value = '';\n                    const aDevSearchEl = document.getElementById('a-dev-classe-search');\n                    if(aDevSearchEl) aDevSearchEl.value = '';\n                    const aDevSearchResEl = document.getElementById('a-dev-classe-search-results');\n                    if(aDevSearchResEl) { aDevSearchResEl.classList.remove('open'); aDevSearchResEl.innerHTML = ''; }\n                    loadApplicationWorkspace();\n                } else {\n                    showToast(errors + ' erreur(s) lors de l\\'envoi.', 'error');\n                }\n            }\n        });\n    });\n}\n\nfunction renderTeacherPanels() {\n    \/\/ Mettre \u00e0 jour le nom et les classes dans la topbar (en sync avec les donn\u00e9es fra\u00eeches)\n    const nameBar = document.getElementById('teacher-name-bar');\n    if (nameBar) nameBar.innerText = (ouzoudSessionData && ouzoudSessionData.name) ? ouzoudSessionData.name : '';\n    const classesBar = document.getElementById('teacher-classes-bar');\n    if (classesBar) {\n        const classes = (ouzoudSessionData && Array.isArray(ouzoudSessionData.classes) && ouzoudSessionData.classes.length > 0)\n            ? ouzoudSessionData.classes : [];\n        classesBar.innerHTML = classes.map(c => `<span class=\"t-class-chip\">${escHtml(c.trim())}<\/span>`).join('');\n    }\n\n    \/\/ R\u00e9initialiser et d\u00e9clencher le rendu pagin\u00e9\n    _tDevCurrentPage = 1;\n    _tDevRenderPage(1);\n\n    \/\/ Mettre \u00e0 jour les dropdowns mati\u00e8re et niveau\/classe selon le profil enseignant\n    updateTDevoirMatiereDropdown();\n    updateTDevoirClasseDropdown();\n\n    \/\/ Date par d\u00e9faut = demain\n    const tDate = document.getElementById('t-dev-date');\n    if (tDate && !tDate.value) tDate.value = getTomorrowSQLDate();\n\n    if (typeof updateScrollIndicators === 'function') updateScrollIndicators();\n}\n\n\/\/ \u2500\u2500 Pagination devoirs (espace enseignant) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet _tDevCurrentPage = 1;\nfunction _tDevRenderPage(page) {\n    const list = ouzoudSessionData ? (ouzoudSessionData.devoirs || []) : [];\n    const total = list.length;\n    const totalPages = Math.max(1, Math.ceil(total \/ PARENTS_PER_PAGE));\n    _tDevCurrentPage = Math.min(Math.max(1, page), totalPages);\n    const slice = list.slice((_tDevCurrentPage-1)*PARENTS_PER_PAGE, _tDevCurrentPage*PARENTS_PER_PAGE);\n    const myCode = (ouzoudSessionData && ouzoudSessionData.code_enseignant) ? ouzoudSessionData.code_enseignant : '';\n    let html = slice.map(d => {\n        let fileBtn = d.fichier_joint\n            ? `<button class=\"btn btn-outline btn-sm ouzoud-file-btn\" data-fileurl=\"${escHtml(d.fichier_joint)}\" style=\"height:26px;padding:2px 7px;font-size:10px;white-space:nowrap;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:3px;\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"\/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"\/><path d=\"M10 9H8\"\/><path d=\"M16 13H8\"\/><path d=\"M16 17H8\"\/><\/svg>Ouvrir<\/button>`\n            : '<span style=\"color:var(--muted);\">\u2014<\/span>';\n        const publiePar = d.publie_par && d.publie_par !== '' ? d.publie_par : 'Administration';\n        const isAdmin = publiePar === 'Administration';\n        const isMine = myCode && publiePar.includes(myCode);\n        const badgeStyle = isAdmin\n            ? 'background:#FEF3C7;color:#92400E;'\n            : 'background:#D1FAE5;color:#065F46;';\n        \/\/ Un enseignant ne peut supprimer que ses propres devoirs (ceux publi\u00e9s par l'admin sont en lecture seule)\n        const canDelete = isMine;\n        const editBtnT = canDelete\n            ? `<button class=\"btn btn-outline btn-sm\" title=\"Modifier le devoir\" onclick=\"openDevoirEdit(${parseInt(d.id)})\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"\/><path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z\"\/><\/svg><\/button> `\n            : '';\n        const deleteBtn = canDelete\n            ? `<button class=\"btn btn-outline btn-sm\" style=\"color:var(--red);\" onclick=\"deleteItemRecord('devoir', ${parseInt(d.id)})\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"\/><path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\"\/><path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"\/><line x1=\"10\" y1=\"11\" x2=\"10\" y2=\"17\"\/><line x1=\"14\" y1=\"11\" x2=\"14\" y2=\"17\"\/><\/svg><\/button>`\n            : `<span style=\"color:var(--muted);font-size:11px;\" title=\"Publi\u00e9 par l'administration\">\u2014<\/span>`;\n        return `<tr>\n                ${bulkCell('tdevoir', d.id, canDelete)}\n                <td data-label=\"Classe\"><span class=\"chip chip-blue\">${escHtml(d.classe)}<\/span><\/td>\n                <td data-label=\"Mati\u00e8re\">${escHtml(d.matiere)}<\/td>\n                <td data-label=\"Titre\"><b>${escHtml(d.titre)}<\/b><\/td>\n                <td data-label=\"Publi\u00e9 par\"><span style=\"font-size:11px;padding:2px 7px;border-radius:20px;font-weight:600;${badgeStyle}\">${escHtml(isAdmin ? 'Administration' : 'Moi')}<\/span><\/td>\n                <td data-label=\"Date Butoir\" class=\"date-cell\">${escHtml(formatToFrenchDate(d.date_butoir))}<\/td>\n                <td data-label=\"Document\">${fileBtn}<\/td>\n                <td data-label=\"Action\" style=\"white-space:nowrap;min-width:92px;\">${editBtnT}${deleteBtn}<\/td>\n            <\/tr>`;\n    }).join('');\n    const tbody = document.getElementById('t-table-devoirs');\n    if (tbody) tbody.innerHTML = html || '<tr><td colspan=\"8\" style=\"text-align:center;color:var(--muted);padding:20px;\">Aucun devoir publi\u00e9 pour le moment.<\/td><\/tr>';\n    _renderPaginationBar('t-devoirs', total, totalPages, _tDevCurrentPage, '_tDevRenderPage');\n    bulkSync('tdevoir');\n    if (typeof updateScrollIndicators === 'function') updateScrollIndicators();\n}\n\n\/\/ \u2500\u2500 Recherche rapide d'\u00e9l\u00e8ve OU de classe (formulaire \"D\u00e9clarer une absence\") \u2500\u2500\nlet _absEleveSearchActiveIdx = -1;\nlet _absSelectedEleve = null; \/\/ { nom, classe } de l'\u00e9l\u00e8ve choisi via la recherche\nlet _absEleveSearchMatches = []; \/\/ \u00e9l\u00e9ments de type { type:'eleve', nom_enfant, classe } ou { type:'classe', classe, niveau, count }\n\nfunction filterAbsenceEleveSearch() {\n    const input = document.getElementById('abs-eleve-search');\n    const box = document.getElementById('abs-eleve-search-results');\n    if (!input || !box) return;\n    const q = input.value.trim().toLowerCase();\n    const allStudents = (ouzoudSessionData && ouzoudSessionData.tous_enfants) ? ouzoudSessionData.tous_enfants : [];\n\n    \/\/ Le texte a chang\u00e9 apr\u00e8s une s\u00e9lection : la s\u00e9lection n'est plus valable\n    if (_absSelectedEleve && input.value.trim() !== _absSelectedEleve.nom) _absClearSelection(false);\n\n    if (!q) { box.classList.remove('open'); box.innerHTML = ''; _absEleveSearchMatches = []; _absEleveSearchActiveIdx = -1; return; }\n\n    \/\/ Correspondances par nom d'\u00e9l\u00e8ve\n    const studentMatches = allStudents\n        .filter(s => (s.nom_enfant || '').toLowerCase().includes(q))\n        .map(s => ({ type: 'eleve', nom_enfant: s.nom_enfant, classe: s.classe }));\n\n    _absEleveSearchMatches = studentMatches.slice(0, 30);\n    _absEleveSearchActiveIdx = -1;\n\n    if (_absEleveSearchMatches.length === 0) {\n        box.innerHTML = '<div class=\"eleve-search-empty\">Aucun \u00e9l\u00e8ve trouv\u00e9.<\/div>';\n    } else {\n        box.innerHTML = _absEleveSearchMatches.map((m, i) =>\n            `<div class=\"eleve-search-item\" data-idx=\"${i}\" onclick=\"selectAbsenceEleveFromSearch(${i})\">\n                <span class=\"esi-name\">${escHtml(m.nom_enfant)}<\/span>\n                <span class=\"esi-classe\">${escHtml(m.classe)}<\/span>\n            <\/div>`\n        ).join('');\n    }\n    box.classList.add('open');\n}\n\nfunction absEleveSearchKeydown(evt) {\n    const box = document.getElementById('abs-eleve-search-results');\n    if (!box || !box.classList.contains('open') || _absEleveSearchMatches.length === 0) return;\n    const items = box.querySelectorAll('.eleve-search-item');\n    if (evt.key === 'ArrowDown') {\n        evt.preventDefault();\n        _absEleveSearchActiveIdx = Math.min(_absEleveSearchActiveIdx + 1, _absEleveSearchMatches.length - 1);\n        items.forEach((it, i) => it.classList.toggle('active', i === _absEleveSearchActiveIdx));\n        if (items[_absEleveSearchActiveIdx]) items[_absEleveSearchActiveIdx].scrollIntoView({ block: 'nearest' });\n    } else if (evt.key === 'ArrowUp') {\n        evt.preventDefault();\n        _absEleveSearchActiveIdx = Math.max(_absEleveSearchActiveIdx - 1, 0);\n        items.forEach((it, i) => it.classList.toggle('active', i === _absEleveSearchActiveIdx));\n        if (items[_absEleveSearchActiveIdx]) items[_absEleveSearchActiveIdx].scrollIntoView({ block: 'nearest' });\n    } else if (evt.key === 'Enter') {\n        evt.preventDefault();\n        if (_absEleveSearchActiveIdx >= 0) selectAbsenceEleveFromSearch(_absEleveSearchActiveIdx);\n        else if (_absEleveSearchMatches.length === 1) selectAbsenceEleveFromSearch(0);\n    } else if (evt.key === 'Escape') {\n        box.classList.remove('open');\n    }\n}\n\nfunction selectAbsenceEleveFromSearch(idx) {\n    const match = _absEleveSearchMatches[idx];\n    if (!match) return;\n\n    const niveauSel = document.getElementById('abs-niveau');\n    const classeSel = document.getElementById('abs-classe');\n    const studentSel = document.getElementById('abs-nom');\n    const config = (ouzoudSessionData && ouzoudSessionData.classes_config) ? ouzoudSessionData.classes_config : {};\n    let niveauTrouve = '';\n    for (const niveau in config) {\n        if ((config[niveau] || []).includes(match.classe)) { niveauTrouve = niveau; break; }\n    }\n    if (!niveauTrouve) {\n        showToast(\"La classe de cet \u00e9l\u00e8ve n'est pas configur\u00e9e.\", 'error');\n        return;\n    }\n\n    \/\/ Saisie \u00ab un seul \u00e9l\u00e8ve \u00bb par d\u00e9faut ; la case permet d'ajouter les autres \u00e9l\u00e8ves de la classe\n    const multiTg = document.getElementById('abs-multi-toggle');\n    if (multiTg) multiTg.checked = false;\n    toggleAbsenceMultiMode();\n\n    niveauSel.value = niveauTrouve;\n    updateAbsenceClasseDropdown();\n    classeSel.value = match.classe;\n    updateAbsencesStudentsDropdown();\n    studentSel.value = match.nom_enfant;\n\n    _absSelectedEleve = { nom: match.nom_enfant, classe: match.classe };\n    const wrap = document.getElementById('abs-same-class-wrap');\n    if (wrap) wrap.style.display = '';\n    const lbl = document.getElementById('abs-same-class-label');\n    if (lbl) lbl.textContent = \"S\u00e9lectionner d'autres \u00e9l\u00e8ves de la m\u00eame classe (\" + match.classe + \")\";\n\n    document.getElementById('abs-eleve-search').value = match.nom_enfant;\n    document.getElementById('abs-eleve-search-results').classList.remove('open');\n    showToast(`${match.nom_enfant} s\u00e9lectionn\u00e9(e) \u2014 ${match.classe}.`, 'success');\n}\n\n\/\/ Annule la s\u00e9lection d'\u00e9l\u00e8ve (champ modifi\u00e9 ou formulaire r\u00e9initialis\u00e9)\nfunction _absClearSelection(clearInput) {\n    _absSelectedEleve = null;\n    const niveauSel = document.getElementById('abs-niveau');\n    const classeSel = document.getElementById('abs-classe');\n    if (niveauSel) niveauSel.value = '';\n    updateAbsenceClasseDropdown();\n    if (classeSel) { classeSel.value = ''; classeSel.disabled = true; }\n    updateAbsencesStudentsDropdown();\n    const multiTg = document.getElementById('abs-multi-toggle');\n    if (multiTg) multiTg.checked = false;\n    const wrap = document.getElementById('abs-same-class-wrap');\n    if (wrap) wrap.style.display = 'none';\n    toggleAbsenceMultiMode();\n    if (clearInput) {\n        const inp = document.getElementById('abs-eleve-search');\n        if (inp) inp.value = '';\n    }\n}\n\n\/\/ Case \u00ab autres \u00e9l\u00e8ves de la m\u00eame classe \u00bb : affiche toute la classe, l'\u00e9l\u00e8ve cherch\u00e9 est pr\u00e9-coch\u00e9\nfunction onAbsSameClassToggle() {\n    toggleAbsenceMultiMode();\n    const multiTg = document.getElementById('abs-multi-toggle');\n    if (multiTg && multiTg.checked && _absSelectedEleve) {\n        document.querySelectorAll('#abs-multi-list .abs-multi-item').forEach(item => {\n            const cb = item.querySelector('input[type=\"checkbox\"]');\n            if (cb.value === _absSelectedEleve.nom) { cb.checked = true; _absRowRefresh(item); }\n        });\n        updateAbsenceMultiCount();\n    }\n}\n\n\/\/ Ferme la liste de suggestions au clic en dehors du champ\ndocument.addEventListener('click', function(evt) {\n    const wrap = document.getElementById('abs-eleve-search');\n    const box = document.getElementById('abs-eleve-search-results');\n    if (!wrap || !box) return;\n    if (evt.target !== wrap && !box.contains(evt.target)) box.classList.remove('open');\n});\n\n\/\/ \u2500\u2500 M\u00e9morisation de la derni\u00e8re classe utilis\u00e9e (formulaire d'absence) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst ABS_LAST_CLASS_KEY = 'ouzoud_abs_last_niveau_classe';\n\nfunction saveLastAbsenceClass(niveau, classe) {\n    try {\n        \/\/ [FIX#2] : sessionStorage au lieu de localStorage \u2014 les donn\u00e9es ne persistent\n        \/\/ pas au-del\u00e0 de l'onglet\/session, \u00e9vitant toute fuite sur poste partag\u00e9.\n        if (niveau && classe) sessionStorage.setItem(ABS_LAST_CLASS_KEY, JSON.stringify({ niveau, classe }));\n    } catch (e) { \/* sessionStorage indisponible : on ignore silencieusement *\/ }\n}\n\nfunction getLastAbsenceClass() {\n    try {\n        const raw = sessionStorage.getItem(ABS_LAST_CLASS_KEY);\n        return raw ? JSON.parse(raw) : null;\n    } catch (e) { return null; }\n}\n\n\/\/ [FIX] : efface la m\u00e9morisation niveau\/classe \u2014 utilis\u00e9 lors d'une r\u00e9initialisation\n\/\/ compl\u00e8te du formulaire pour qu'un rechargement de page ne r\u00e9applique pas l'ancienne classe.\nfunction clearLastAbsenceClass() {\n    try { sessionStorage.removeItem(ABS_LAST_CLASS_KEY); } catch (e) { \/* sessionStorage indisponible : on ignore *\/ }\n}\n\nfunction restoreLastAbsenceClass() {\n    return; \/\/ Plus de s\u00e9lecteur Niveau\/Classe : la s\u00e9lection passe uniquement par la recherche d'\u00e9l\u00e8ve\n    const last = getLastAbsenceClass();\n    if (!last || !last.niveau || !last.classe) return;\n    const niveauSel = document.getElementById('abs-niveau');\n    const classeSel = document.getElementById('abs-classe');\n    if (!niveauSel || !classeSel) return;\n    \/\/ Ne pas \u00e9craser une s\u00e9lection d\u00e9j\u00e0 en cours\n    if (niveauSel.value) return;\n    niveauSel.value = last.niveau;\n    updateAbsenceClasseDropdown();\n    if ([...classeSel.options].some(o => o.value === last.classe)) {\n        classeSel.value = last.classe;\n        updateAbsencesStudentsDropdown();\n    }\n}\n\n\/\/ \u2500\u2500 Mode \"saisie multiple\" (cocher plusieurs \u00e9l\u00e8ves d'une m\u00eame classe) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/\/ \u2500\u2500 Liste d'appel : tous pr\u00e9sents par d\u00e9faut, on marque les absents \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet _absMultiBuiltFor = '';\nfunction toggleAbsenceMultiMode() {\n    const isMulti = document.getElementById('abs-multi-toggle').checked;\n    document.getElementById('abs-nom-multi-wrap').style.display = isMulti ? '' : 'none';\n    document.getElementById('abs-submit-btn').textContent = isMulti ? 'Enregistrer les absences' : \"Enregistrer l'absence\";\n}\n\nfunction updateAbsenceMultiList(students) {\n    const list = document.getElementById('abs-multi-list');\n    if (!list) return;\n    if (!students || students.length === 0) {\n        list.innerHTML = '<div class=\"eleve-search-empty\">S\u00e9lectionnez d\\'abord une classe<\/div>';\n        _absMultiBuiltFor = '';\n        updateAbsenceMultiCount();\n        return;\n    }\n    const classeEl = document.getElementById('abs-classe');\n    const classe = classeEl ? classeEl.value : '';\n    \/\/ Si la liste est reconstruite pour la M\u00caME classe (ex. rechargement des donn\u00e9es apr\u00e8s une\n    \/\/ suppression), on conserve les absents d\u00e9j\u00e0 marqu\u00e9s ainsi que leur p\u00e9riode.\n    const prev = new Set();\n    if (classe && classe === _absMultiBuiltFor) {\n        list.querySelectorAll('.abs-multi-item').forEach(item => {\n            const cb = item.querySelector('input[type=\"checkbox\"]');\n            if (cb && cb.checked) prev.add(cb.value);\n        });\n    }\n    _absMultiBuiltFor = classe;\n    list.innerHTML = students.map((s, i) =>\n        `<label class=\"abs-multi-item\" id=\"abs-multi-item-${i}\">\n            <input type=\"checkbox\" value=\"${escHtml(s.nom_enfant)}\" onchange=\"onAbsenceMultiCheckChange(${i})\" title=\"Cocher = absent\">\n            <span class=\"abs-name\">${escHtml(s.nom_enfant)}<\/span>\n            <span class=\"abs-state\">Pr\u00e9sent<\/span>\n        <\/label>`\n    ).join('');\n    list.querySelectorAll('.abs-multi-item').forEach(item => {\n        const cb = item.querySelector('input[type=\"checkbox\"]');\n        if (prev.has(cb.value)) {\n            cb.checked = true;\n        }\n        _absRowRefresh(item);\n    });\n    updateAbsenceMultiCount();\n}\n\nfunction _absRowRefresh(item) {\n    const cb = item.querySelector('input[type=\"checkbox\"]');\n    item.classList.toggle('checked', cb.checked);\n    const st = item.querySelector('.abs-state');\n    if (st) st.textContent = cb.checked ? 'Absent' : 'Pr\u00e9sent';\n}\n\nfunction onAbsenceMultiCheckChange(i) {\n    const item = document.getElementById('abs-multi-item-' + i);\n    if (item) _absRowRefresh(item);\n    updateAbsenceMultiCount();\n}\n\n\/\/ checked=true \u2192 tous absents ; checked=false \u2192 tous pr\u00e9sents\nfunction toggleAllAbsenceCheckboxes(checked) {\n    const list = document.getElementById('abs-multi-list');\n    if (!list) return;\n    list.querySelectorAll('.abs-multi-item').forEach(item => {\n        item.querySelector('input[type=\"checkbox\"]').checked = checked;\n        _absRowRefresh(item);\n    });\n    updateAbsenceMultiCount();\n}\n\nfunction updateAbsenceMultiCount() {\n    const countEl = document.getElementById('abs-multi-count');\n    if (!countEl) return;\n    const list = document.getElementById('abs-multi-list');\n    const total = list ? list.querySelectorAll('input[type=\"checkbox\"]').length : 0;\n    const n = list ? list.querySelectorAll('input[type=\"checkbox\"]:checked').length : 0;\n    countEl.textContent = `${n} absent(s) \u00b7 ${total - n} pr\u00e9sent(s)`;\n}\n\n\/\/ \u2500\u2500 Plage de dates (absence sur plusieurs jours) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction _sqlDateFromObj(d) {\n    return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;\n}\n\/\/ Liste des jours [d1 ; d2] (AAAA-MM-JJ), week-ends exclus si noWE\nfunction _absListDates(d1, d2, noWE) {\n    const out = [];\n    const start = new Date(d1 + 'T00:00:00'), end = new Date(d2 + 'T00:00:00');\n    if (isNaN(start) || isNaN(end) || end < start) return out;\n    for (let d = new Date(start); d <= end && out.length < 400; d.setDate(d.getDate() + 1)) {\n        const dow = d.getDay();\n        if (noWE && (dow === 0 || dow === 6)) continue;\n        out.push(_sqlDateFromObj(d));\n    }\n    return out;\n}\n\nfunction toggleAbsenceRange() {\n    const on = document.getElementById('abs-range-toggle').checked;\n    document.getElementById('abs-range-wrap').style.display = on ? '' : 'none';\n    document.getElementById('abs-date-label').textContent = on ? 'Du *' : 'Date';\n    if (on) {\n        const fin = document.getElementById('abs-date-fin');\n        if (fin && !fin.value) fin.value = document.getElementById('abs-date').value;\n    }\n    updateAbsenceRangeSummary();\n}\n\nfunction updateAbsenceRangeSummary() {\n    const el = document.getElementById('abs-range-summary');\n    if (!el) return;\n    const on = document.getElementById('abs-range-toggle').checked;\n    const d1 = document.getElementById('abs-date').value;\n    const d2 = document.getElementById('abs-date-fin').value;\n    if (!on || !d1 || !d2) { el.textContent = ''; return; }\n    if (d2 < d1) { el.textContent = '\u26a0 La date de fin doit \u00eatre post\u00e9rieure \u00e0 la date de d\u00e9but.'; return; }\n    const noWE = document.getElementById('abs-range-noweekend').checked;\n    const days = _absListDates(d1, d2, noWE);\n    el.textContent = days.length\n        ? `${days.length} jour(s) : du ${formatToFrenchDate(d1)} au ${formatToFrenchDate(d2)}`\n        : 'Aucun jour \u00e0 enregistrer dans cette plage (week-ends exclus).';\n}\n\n\/\/ Valide la\/les date(s) du formulaire. Retourne { date, date_fin, noWE, days } ou null (toast affich\u00e9).\nfunction _absBuildDatePayload() {\n    const d1 = document.getElementById('abs-date').value;\n    if (!d1 || !\/^\\d{4}-\\d{2}-\\d{2}$\/.test(d1)) {\n        showToast(\"Veuillez saisir une date valide (format AAAA-MM-JJ).\", 'error');\n        return null;\n    }\n    if (!document.getElementById('abs-range-toggle').checked) {\n        return { date: d1, date_fin: '', noWE: false, days: 1 };\n    }\n    const d2 = document.getElementById('abs-date-fin').value;\n    if (!d2 || !\/^\\d{4}-\\d{2}-\\d{2}$\/.test(d2)) {\n        showToast(\"Veuillez saisir une date de fin valide.\", 'error');\n        return null;\n    }\n    if (d2 < d1) {\n        showToast(\"La date de fin doit \u00eatre post\u00e9rieure ou \u00e9gale \u00e0 la date de d\u00e9but.\", 'error');\n        return null;\n    }\n    if ((new Date(d2 + 'T00:00:00') - new Date(d1 + 'T00:00:00')) \/ 86400000 > 92) {\n        showToast(\"Plage trop longue : 92 jours maximum par saisie.\", 'error');\n        return null;\n    }\n    const noWE = document.getElementById('abs-range-noweekend').checked;\n    const days = _absListDates(d1, d2, noWE).length;\n    if (!days) {\n        showToast(\"Aucun jour \u00e0 enregistrer dans cette plage (week-ends exclus).\", 'error');\n        return null;\n    }\n    return { date: d1, date_fin: d2, noWE: noWE, days: days };\n}\n\nfunction updateAbsenceClasseDropdown() {\n    const niveauSel = document.getElementById('abs-niveau');\n    const classeSel = document.getElementById('abs-classe');\n    const studentSel = document.getElementById('abs-nom');\n    const selectedNiveau = niveauSel ? niveauSel.value : '';\n    const prevClasse = classeSel.value;\n    classeSel.innerHTML = '';\n    studentSel.innerHTML = '<option value=\"\" disabled selected>S\u00e9lectionnez d\\'abord une classe<\/option>';\n    studentSel.disabled = true;\n    if (!selectedNiveau) {\n        classeSel.innerHTML = '<option value=\"\" disabled selected>S\u00e9lectionnez d\\'abord un niveau<\/option>';\n        classeSel.disabled = true;\n        return;\n    }\n    const config = (ouzoudSessionData && ouzoudSessionData.classes_config) ? ouzoudSessionData.classes_config : {};\n    const classes = config[selectedNiveau] || [];\n    if (classes.length === 0) {\n        classeSel.innerHTML = '<option value=\"\" disabled selected>Aucune classe configur\u00e9e<\/option>';\n        classeSel.disabled = true;\n    } else {\n        classeSel.innerHTML = '<option value=\"\" disabled selected>-- Choisir la classe --<\/option>';\n        classes.forEach(c => {\n            let opt = document.createElement('option');\n            opt.value = c;\n            opt.textContent = c;\n            classeSel.appendChild(opt);\n        });\n        classeSel.disabled = false;\n        if (prevClasse && classes.includes(prevClasse)) classeSel.value = prevClasse;\n    }\n}\n\nfunction updateAbsencesStudentsDropdown() {\n    let classSelect = document.getElementById('abs-classe');\n    let studentSelect = document.getElementById('abs-nom');\n    let niveauSelect = document.getElementById('abs-niveau');\n    let selectedClass = classSelect ? classSelect.value : '';\n    const prevStudent = studentSelect.value;\n    \n    studentSelect.innerHTML = '';\n    \n    if (!selectedClass) {\n        studentSelect.innerHTML = '<option value=\"\" disabled selected>S\u00e9lectionnez d\\'abord une classe<\/option>';\n        studentSelect.disabled = true;\n        updateAbsenceMultiList([]);\n        return;\n    }\n\n    \/\/ [M\u00e9morisation] sauvegarder le dernier niveau\/classe choisi pour le r\u00e9utiliser au prochain enregistrement\n    saveLastAbsenceClass(niveauSelect ? niveauSelect.value : '', selectedClass);\n    \n    let filteredStudents = [];\n    if (ouzoudSessionData && ouzoudSessionData.tous_enfants) {\n        filteredStudents = ouzoudSessionData.tous_enfants.filter(student => student.classe === selectedClass);\n    }\n    \n    if (filteredStudents.length === 0) {\n        studentSelect.innerHTML = '<option value=\"\" disabled selected>Aucun \u00e9l\u00e8ve trouv\u00e9 dans cette classe<\/option>';\n        studentSelect.disabled = true;\n    } else {\n        studentSelect.innerHTML = '<option value=\"\" disabled selected>-- Choisir l\\'\u00e9l\u00e8ve --<\/option>';\n        filteredStudents.forEach(student => {\n            let opt = document.createElement('option');\n            opt.value = student.nom_enfant;\n            opt.textContent = student.nom_enfant;\n            studentSelect.appendChild(opt);\n        });\n        studentSelect.disabled = false;\n        if (prevStudent && filteredStudents.some(st => st.nom_enfant === prevStudent)) studentSelect.value = prevStudent;\n    }\n    updateAbsenceMultiList(filteredStudents);\n}\n\nfunction openParentModal(id) {\n    if(id === 0) {\n        document.getElementById('p-id').value = 0; document.getElementById('p-nom').value = '';\n        document.getElementById('p-code-parent').value = '';\n        document.getElementById('p-email').value = ''; document.getElementById('p-enfants').value = '';\n        document.getElementById('mp-title').innerText = \"Ajouter un Parent\";\n    } else {\n        let item = ouzoudSessionData.parents.find(x => x.id == id);\n        document.getElementById('p-id').value = item.id; document.getElementById('p-nom').value = item.nom_complet;\n        document.getElementById('p-code-parent').value = item.code_parent;\n        document.getElementById('p-email').value = item.email;\n        \/\/ [FIX#6] : convertir le s\u00e9parateur | (serveur) en \", \" (formulaire) pour l'affichage\n        document.getElementById('p-enfants').value = item.enfants ? item.enfants.replace(\/\\|\/g, ', ') : '';\n        \/\/ [FIX#11 v4.3] : innerText (pas innerHTML) \u2014 les caract\u00e8res HTML de nom_complet\n        \/\/ ne sont PAS interpr\u00e9t\u00e9s. Ne jamais remplacer par innerHTML ici : risque XSS.\n        document.getElementById('mp-title').innerText = \"Modifier la fiche de \" + item.nom_complet;\n    }\n    const btnSave = document.querySelector('#m-parent-form .btn-blue');\n    if (btnSave) { btnSave.disabled = false; btnSave.textContent = 'Enregistrer'; }\n    document.getElementById('m-parent-form').style.display = 'flex';\n}\n\nfunction closeOuzoudModal(id) { document.getElementById(id).style.display = 'none'; }\n\n\/\/ Normalise un nom : supprime la civilit\u00e9 et les espaces multiples\nfunction normaliserNom(nom) {\n    return nom.replace(\/^(M\\.?|Mr\\.?|Mme\\.?|Dr\\.?|Prof\\.?)\\s+\/i, '').replace(\/\\s+\/g, ' ').trim().toLowerCase();\n}\n\nfunction submitParentForm() {\n    let nom = document.getElementById('p-nom').value.trim();\n    let enfants = document.getElementById('p-enfants').value.trim();\n    let currentId = parseInt(document.getElementById('p-id').value);\n\n    if (!nom || !enfants) {\n        showToast(\"Le Nom et les Enfants rattach\u00e9s sont obligatoires.\", 'error');\n        return;\n    }\n\n    \/\/ \u2500\u2500 D\u00e9tection de doublon avant envoi (uniquement pour un nouveau parent) \u2500\n    if (currentId === 0 && ouzoudSessionData && ouzoudSessionData.parents) {\n        const nomNorm = normaliserNom(nom);\n        const doublon = ouzoudSessionData.parents.find(p => {\n            const pNorm = normaliserNom(p.nom_complet || '');\n            return pNorm === nomNorm;\n        });\n        if (doublon) {\n            \/\/ [FIX36] : un homonyme est une fiche DISTINCTE \u2014 plus de bascule automatique en mise \u00e0 jour\n            \/\/ (le serveur ne fusionne plus non plus). Pour modifier la fiche existante : bouton \u00ab \u00c9diter \u00bb.\n            const msg = `\u26a0\ufe0f Un parent portant le m\u00eame nom existe d\u00e9j\u00e0 :\\n\\n`\n                + `  \u2192 ${doublon.nom_complet}  (${doublon.code_parent})\\n\\n`\n                + `OK = cr\u00e9er une NOUVELLE fiche distincte (homonyme).\\n`\n                + `Annuler = abandonner (pour modifier la fiche existante, utilisez \u00ab \u00c9diter \u00bb).`;\n            if (!confirm(msg)) return;\n        }\n    }\n    \/\/ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n    \/\/ Valider les classes contre la configuration serveur uniquement (ex: 1APG-1, 3APG-3)\n    \/\/ \u2014 coh\u00e9rent avec ouzoud_is_valid_classe() c\u00f4t\u00e9 PHP qui n'accepte pas les niveaux bruts\n    const classesConfig = (ouzoudSessionData && ouzoudSessionData.classes_config) ? ouzoudSessionData.classes_config : {};\n    const classesValides = Object.values(classesConfig).flat();\n    if (classesValides.length === 0) {\n        showToast('Configuration des classes non charg\u00e9e. Rechargez la page et r\u00e9essayez.', 'error');\n        return;\n    }\n    const formatRegex = \/^.+\\(([^)]+)\\)$\/;\n    const partiesEnfants = enfants.split(',').filter(p => p.trim() !== '');\n    \/\/ [FIX#G] : limite 10 enfants c\u00f4t\u00e9 JS, coh\u00e9rente avec la validation serveur\n    if (partiesEnfants.length > 10) {\n        showToast('Nombre d\\'enfants trop \u00e9lev\u00e9. Maximum autoris\u00e9 : 10 par parent.', 'error');\n        return;\n    }\n    for (let partie of partiesEnfants) {\n        partie = partie.trim();\n        if (!partie) continue;\n        const match = partie.match(formatRegex);\n        if (!match) {\n            showToast('Format invalide : \"' + partie + '\". Format attendu : Pr\u00e9nom Nom (Classe), ex: Ali Benali (1APG-1)', 'error');\n            return;\n        }\n        const niveau = match[1].trim();\n        if (!classesValides.includes(niveau)) {\n            showToast('Classe invalide : \"' + niveau + '\". Classes accept\u00e9es : ' + classesValides.join(', '), 'error');\n            return;\n        }\n    }\n\n    let p = new URLSearchParams();\n    p.append('action', 'ouzoud_ajax_save_data'); p.append('action_type', 'save_parent');\n    p.append('nonce', OuzoudAjax.nonce);\n    p.append('id', document.getElementById('p-id').value); p.append('nom', nom);\n    \/\/ code_parent g\u00e9n\u00e9r\u00e9 c\u00f4t\u00e9 serveur \u2014 pas besoin de l'envoyer\n    p.append('email', document.getElementById('p-email').value); p.append('enfants', enfants);\n\n    const btnSave = document.querySelector('#m-parent-form .btn-blue');\n    if (btnSave) { btnSave.disabled = true; btnSave.textContent = 'Enregistrement\u2026'; }\n\n    fetch(OuzoudAjax.url, { method: 'POST', body: p }).then(r => r.text()).then(text => {\n        let res; try { res = JSON.parse(text); } catch(e) { res = { success: false, data: 'Echec de l\\'enregistrement. Vous devez vous connecter au site en tant qu\\'Admin.' }; }\n        if(res.success) {\n            if(res.data && res.data.code_parent) {\n                if(res.data.is_new) {\n                    showToast('Parent cr\u00e9\u00e9. Code : ' + res.data.code_parent, 'success', 6000);\n                    \/\/ Affichage d\u00e9taill\u00e9 dans une fen\u00eatre modale l\u00e9g\u00e8re\n                    setTimeout(() => showToast('Code parent : ' + res.data.code_parent + ' \u2014 Communiquez-le au parent.', 'success', 10000), 100);\n                } else if(document.getElementById('p-id').value == 0) {\n                    showToast('Fiche mise \u00e0 jour. Code : ' + res.data.code_parent, 'info', 5000);\n                } else {\n                    showToast('Fiche parent mise \u00e0 jour.', 'success');\n                }\n            } else {\n                showToast('Fiche enregistr\u00e9e.', 'success');\n            }\n            closeOuzoudModal('m-parent-form'); loadApplicationWorkspace();\n        }\n        else if(res.data && res.data.message) {\n            showToast('Erreur : ' + res.data.message, 'error');\n            if (btnSave) { btnSave.disabled = false; btnSave.textContent = 'Enregistrer'; }\n        } else {\n            showToast(res.data || 'Echec de l\\'enregistrement. Vous devez vous connecter au site en tant qu\\'Admin.', 'error');\n            if (btnSave) { btnSave.disabled = false; btnSave.textContent = 'Enregistrer'; }\n        }\n    }).catch(() => {\n        if (btnSave) { btnSave.disabled = false; btnSave.textContent = 'Enregistrer'; }\n        showToast(\"Erreur r\u00e9seau. La fiche n'a pas pu \u00eatre enregistr\u00e9e.\", 'error');\n    });\n}\n\nfunction publishAbsence() {\n    \/\/ [FIX#7] : guard anti-double-soumission \u2014 coh\u00e9rent avec publishADevoir() et publishAnnonce().\n    \/\/ Sans ce guard, un double-clic rapide pouvait d\u00e9clencher deux lots parall\u00e8les (mode multi)\n    \/\/ ou deux requ\u00eates simultan\u00e9es (mode simple) avant que le bouton ne soit d\u00e9sactiv\u00e9 par\n    \/\/ la fonction appel\u00e9e.\n    const submitBtn = document.getElementById('abs-submit-btn');\n    if (submitBtn && submitBtn.disabled) return;\n\n    const isMulti = document.getElementById('abs-multi-toggle').checked;\n    if (isMulti) {\n        publishAbsencesMulti();\n    } else {\n        publishAbsenceSingle();\n    }\n}\n\nfunction publishAbsenceSingle() {\n    let targetClass = document.getElementById('abs-classe').value;\n    let nomEleve = document.getElementById('abs-nom').value;\n\n    if (!targetClass || !nomEleve) {\n        showToast(\"Recherchez et s\u00e9lectionnez un \u00e9l\u00e8ve.\", 'error');\n        return;\n    }\n    \/\/ [FIX#3 v4.2] : v\u00e9rifier que l'\u00e9l\u00e8ve s\u00e9lectionn\u00e9 existe encore dans les donn\u00e9es\n    \/\/ courantes (un autre admin peut avoir modifi\u00e9 la liste entre temps)\n    if (ouzoudSessionData && ouzoudSessionData.tous_enfants) {\n        const eleveConnu = ouzoudSessionData.tous_enfants.some(\n            s => s.nom_enfant === nomEleve && s.classe === targetClass\n        );\n        if (!eleveConnu) {\n            showToast(\"Cet \u00e9l\u00e8ve n'est plus dans la liste. Actualisez la page avant de continuer.\", 'error');\n            updateAbsencesStudentsDropdown();\n            return;\n        }\n    }\n    _submitAbsences(targetClass, [{ n: nomEleve, p: '' }]);\n}\n\n\/\/ \u2500\u2500 Liste d'appel : une seule requ\u00eate pour tous les absents coch\u00e9s \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction publishAbsencesMulti() {\n    const targetClass = document.getElementById('abs-classe').value;\n    if (!targetClass) {\n        showToast(\"Le niveau et la classe sont obligatoires.\", 'error');\n        return;\n    }\n    const eleves = [...document.querySelectorAll('#abs-multi-list .abs-multi-item')]\n        .filter(item => item.querySelector('input[type=\"checkbox\"]').checked)\n        .map(item => ({\n            n: item.querySelector('input[type=\"checkbox\"]').value,\n            p: ''\n        }));\n    if (eleves.length === 0) {\n        showToast(\"Cochez au moins un \u00e9l\u00e8ve absent dans la liste.\", 'error');\n        return;\n    }\n    _submitAbsences(targetClass, eleves);\n}\n\n\/\/ Envoi commun (\u00e9l\u00e8ve seul, liste d'appel, plage de dates) : UNE requ\u00eate, un seul email par parent.\nfunction _submitAbsences(targetClass, eleves) {\n    const dp = _absBuildDatePayload();\n    if (!dp) return;\n    const total = eleves.length * dp.days;\n    if (total > 600) {\n        showToast(\"Trop d'absences \u00e0 cr\u00e9er en une fois (600 maximum). R\u00e9duisez la plage de dates ou la liste.\", 'error');\n        return;\n    }\n    if (total > 20 && !confirm(`Vous allez enregistrer ${total} absences (${eleves.length} \u00e9l\u00e8ve(s) \u00d7 ${dp.days} jour(s)). Continuer ?`)) {\n        return;\n    }\n\n    const p = new URLSearchParams();\n    p.append('action', 'ouzoud_ajax_save_data'); p.append('action_type', 'save_absence');\n    p.append('nonce', OuzoudAjax.nonce);\n    p.append('classe', targetClass);\n    p.append('eleves', JSON.stringify(eleves));\n    p.append('date', dp.date);\n    if (dp.date_fin) { p.append('date_fin', dp.date_fin); p.append('exclure_weekend', dp.noWE ? '1' : '0'); }\n    p.append('periode', document.getElementById('abs-periode').value);\n    p.append('statut', document.getElementById('abs-statut').value);\n    p.append('motif', document.getElementById('abs-motif').value);\n\n    const submitBtn = document.getElementById('abs-submit-btn');\n    if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Enregistrement\u2026'; }\n\n    fetch(OuzoudAjax.url, { method: 'POST', body: p }).then(r => r.text()).then(text => {\n        let res; try { res = JSON.parse(text); } catch(e) { res = { success: false, data: null }; }\n        if (res.success) {\n            const d = res.data || {};\n            let msg = `${d.crees || 0} absence(s) enregistr\u00e9e(s).`;\n            if (d.ignorees) msg += ` ${d.ignorees} d\u00e9j\u00e0 existante(s) ignor\u00e9e(s).`;\n            if (d.erreurs && d.erreurs.length) {\n                showToast(msg + ' Probl\u00e8mes : ' + d.erreurs.join(' '), 'error', 9000);\n            } else {\n                showToast(msg, 'success');\n            }\n            resetAbsenceFormAfterSave();\n            loadApplicationWorkspace();\n        } else {\n            showToast(res.data || 'Echec de l\\'enregistrement. Vous devez vous connecter au site en tant qu\\'Admin.', 'error');\n        }\n    }).catch(() => {\n        showToast(\"Erreur r\u00e9seau. Les absences n'ont pas pu \u00eatre enregistr\u00e9es.\", 'error');\n    }).finally(() => {\n        if (submitBtn) submitBtn.disabled = false;\n        toggleAbsenceMultiMode(); \/\/ restaure le libell\u00e9 du bouton selon le mode\n    });\n}\n\n\/\/ \u2500\u2500 Justification apr\u00e8s coup : modifier statut \/ motif d'une absence existante \u2500\n\/\/ \u2500\u2500 [FIX33] Modifier un devoir (admin + enseignant) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction toggleDevoirEditAutre() {\n    const sel = document.getElementById('de-matiere');\n    const wrap = document.getElementById('de-autre-wrap');\n    if (wrap && sel) wrap.style.display = (sel.value === '__autre__') ? '' : 'none';\n}\n\nfunction openDevoirEdit(id) {\n    const d = (ouzoudSessionData && ouzoudSessionData.devoirs || []).find(x => x.id == id);\n    if (!d) { showToast(\"Devoir introuvable. Actualisez la page.\", 'error'); return; }\n    document.getElementById('de-id').value = d.id;\n    document.getElementById('de-resume').textContent = `Classe ${d.classe} \u2014 ${d.date_butoir ? '\u00e0 rendre le ' + formatToFrenchDate(d.date_butoir) : ''}`;\n    \/\/ Mati\u00e8res propos\u00e9es : celles de l'enseignant (espace enseignant) ou toutes celles configur\u00e9es (admin)\n    const isTeacher = ouzoudSessionData && ouzoudSessionData.role === 'teacher';\n    let mats = (isTeacher && Array.isArray(ouzoudSessionData.matieres) && ouzoudSessionData.matieres.length)\n        ? ouzoudSessionData.matieres.slice()\n        : ((ouzoudSessionData && Array.isArray(ouzoudSessionData.matieres_config) && ouzoudSessionData.matieres_config.length)\n            ? ouzoudSessionData.matieres_config.slice() : ['Arabe', 'Fran\u00e7ais', 'Maths']);\n    if (d.matiere && !mats.includes(d.matiere)) mats.unshift(d.matiere); \/\/ mati\u00e8re libre d\u00e9j\u00e0 enregistr\u00e9e\n    const sel = document.getElementById('de-matiere');\n    sel.innerHTML = mats.map(m => `<option value=\"${escHtml(m)}\">${escHtml(m)}<\/option>`).join('') +\n        '<option value=\"__autre__\">Autre\u2026<\/option>';\n    sel.value = d.matiere || mats[0];\n    document.getElementById('de-matiere-autre').value = '';\n    toggleDevoirEditAutre();\n    document.getElementById('de-titre').value = d.titre || '';\n    document.getElementById('de-desc').value = d.description || '';\n    document.getElementById('de-date').value = d.date_butoir || '';\n    document.getElementById('m-dev-edit').style.display = 'flex';\n    setTimeout(() => { const t = document.getElementById('de-titre'); if (t) t.focus(); }, 50);\n}\n\nfunction submitDevoirEdit() {\n    const btn = document.getElementById('de-submit-btn');\n    if (btn && btn.disabled) return;\n    const sel = document.getElementById('de-matiere');\n    const matiere = (sel.value === '__autre__' ? document.getElementById('de-matiere-autre').value : sel.value).trim();\n    const titre = document.getElementById('de-titre').value.trim();\n    const date = document.getElementById('de-date').value;\n    if (!matiere) { showToast(\"Veuillez indiquer la mati\u00e8re.\", 'error'); return; }\n    if (!titre)   { showToast(\"Veuillez saisir le titre du devoir.\", 'error'); return; }\n    if (!date)    { showToast(\"Veuillez choisir la date de remise.\", 'error'); return; }\n    const p = new URLSearchParams();\n    p.append('action', 'ouzoud_ajax_save_data'); p.append('action_type', 'update_devoir');\n    p.append('nonce', OuzoudAjax.nonce);\n    p.append('id', document.getElementById('de-id').value);\n    p.append('matiere', matiere);\n    p.append('titre', titre);\n    p.append('desc', document.getElementById('de-desc').value);\n    p.append('date', date);\n    if (btn) btn.disabled = true;\n    fetch(OuzoudAjax.url, { method: 'POST', body: p }).then(r => r.text()).then(text => {\n        let res; try { res = JSON.parse(text); } catch(e) { res = { success: false, data: null }; }\n        if (res.success) {\n            closeOuzoudModal('m-dev-edit');\n            showToast('Devoir mis \u00e0 jour.', 'success');\n            loadApplicationWorkspace();\n        } else {\n            showToast(res.data || \"Echec de la mise \u00e0 jour.\", 'error');\n        }\n    }).catch(() => {\n        showToast(\"Erreur r\u00e9seau. La modification n'a pas pu \u00eatre enregistr\u00e9e.\", 'error');\n    }).finally(() => { if (btn) btn.disabled = false; });\n}\n\n\/\/ \u2500\u2500 Modifier une annonce (admin) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction openAnnonceEdit(id) {\n    const a = (ouzoudSessionData && ouzoudSessionData.annonces || []).find(x => x.id == id);\n    if (!a) { showToast(\"Annonce introuvable. Actualisez la page.\", 'error'); return; }\n    document.getElementById('ae-ann-id').value = a.id;\n    document.getElementById('ae-ann-resume').textContent = a.date_publication ? ('Publi\u00e9e le ' + formatToFrenchDate(String(a.date_publication).substring(0, 10))) : '';\n    document.getElementById('ae-ann-titre').value = a.titre || '';\n    document.getElementById('ae-ann-priorite').value = (a.priorite === 'haute') ? 'haute' : 'normale';\n    document.getElementById('ae-ann-contenu').value = a.contenu || '';\n    document.getElementById('m-ann-edit').style.display = 'flex';\n    setTimeout(() => { const t = document.getElementById('ae-ann-titre'); if (t) t.focus(); }, 50);\n}\n\nfunction submitAnnonceEdit() {\n    const btn = document.getElementById('ae-ann-submit-btn');\n    if (btn && btn.disabled) return;\n    const titre = document.getElementById('ae-ann-titre').value.trim();\n    if (!titre) { showToast(\"Veuillez saisir le titre de l'annonce.\", 'error'); return; }\n    const p = new URLSearchParams();\n    p.append('action', 'ouzoud_ajax_save_data'); p.append('action_type', 'update_annonce');\n    p.append('nonce', OuzoudAjax.nonce);\n    p.append('id', document.getElementById('ae-ann-id').value);\n    p.append('titre', titre);\n    p.append('priorite', document.getElementById('ae-ann-priorite').value);\n    p.append('contenu', document.getElementById('ae-ann-contenu').value);\n    if (btn) btn.disabled = true;\n    fetch(OuzoudAjax.url, { method: 'POST', body: p }).then(r => r.text()).then(text => {\n        let res; try { res = JSON.parse(text); } catch(e) { res = { success: false, data: null }; }\n        if (res.success) {\n            closeOuzoudModal('m-ann-edit');\n            showToast('Annonce mise \u00e0 jour.', 'success');\n            loadApplicationWorkspace();\n        } else {\n            showToast(res.data || \"Echec de la mise \u00e0 jour.\", 'error');\n        }\n    }).catch(() => {\n        showToast(\"Erreur r\u00e9seau. La modification n'a pas pu \u00eatre enregistr\u00e9e.\", 'error');\n    }).finally(() => { if (btn) btn.disabled = false; });\n}\n\nfunction openAbsenceEdit(id, justify) {\n    const a = (ouzoudSessionData && ouzoudSessionData.absences || []).find(x => x.id == id);\n    if (!a) { showToast(\"Absence introuvable. Actualisez la page.\", 'error'); return; }\n    document.getElementById('ae-id').value = a.id;\n    document.getElementById('ae-resume').textContent =\n        `${a.nom_eleve} \u2014 ${a.classe} \u2014 ${formatToFrenchDate(a.date_absence)} (${a.demi_journee})`;\n    document.getElementById('ae-statut').value = justify ? 'justified' : (a.statut === 'justified' ? 'justified' : 'unjustified');\n    document.getElementById('ae-motif').value = a.motif || '';\n    document.getElementById('m-abs-edit').style.display = 'flex';\n    if (justify) setTimeout(() => { const m = document.getElementById('ae-motif'); if (m) m.focus(); }, 50);\n}\n\nfunction submitAbsenceEdit() {\n    const btn = document.getElementById('ae-submit-btn');\n    if (btn && btn.disabled) return;\n    const id = document.getElementById('ae-id').value;\n    const p = new URLSearchParams();\n    p.append('action', 'ouzoud_ajax_save_data'); p.append('action_type', 'update_absence');\n    p.append('nonce', OuzoudAjax.nonce);\n    p.append('id', id);\n    p.append('statut', document.getElementById('ae-statut').value);\n    p.append('motif', document.getElementById('ae-motif').value);\n    if (btn) btn.disabled = true;\n    fetch(OuzoudAjax.url, { method: 'POST', body: p }).then(r => r.text()).then(text => {\n        let res; try { res = JSON.parse(text); } catch(e) { res = { success: false, data: null }; }\n        if (res.success) {\n            closeOuzoudModal('m-abs-edit');\n            showToast('Absence mise \u00e0 jour.', 'success');\n            loadApplicationWorkspace();\n        } else {\n            showToast(res.data || \"Echec de la mise \u00e0 jour.\", 'error');\n        }\n    }).catch(() => {\n        showToast(\"Erreur r\u00e9seau. La modification n'a pas pu \u00eatre enregistr\u00e9e.\", 'error');\n    }).finally(() => { if (btn) btn.disabled = false; });\n}\n\n\/\/ R\u00e9initialise le formulaire apr\u00e8s un enregistrement. Par d\u00e9faut, remet tout \u00e0 z\u00e9ro\n\/\/ (niveau, classe, \u00e9l\u00e8ve, dates, etc.) ; keepClass permet de conserver Niveau\/Classe\n\/\/ pr\u00e9-remplis pour faciliter la saisie de plusieurs absences cons\u00e9cutives sur la m\u00eame classe.\n\/\/ [FIX#6] : param\u00e8tre keepMulti supprim\u00e9 \u2014 il \u00e9tait d\u00e9clar\u00e9 mais jamais lu dans la fonction,\n\/\/ ce qui rendait la signature trompeuse. Le mode simple\/multiple est intentionnellement\n\/\/ conserv\u00e9 entre deux saisies (commentaire en fin de fonction), sans param\u00e8tre n\u00e9cessaire.\nfunction resetAbsenceFormAfterSave({ keepClass = false } = {}) {\n    if (!keepClass) {\n        _absClearSelection(true);\n        \/\/ [FIX] : effacer la m\u00e9morisation pour qu'un rechargement de page ne\n        \/\/ r\u00e9applique pas l'ancienne classe apr\u00e8s une r\u00e9initialisation compl\u00e8te.\n        clearLastAbsenceClass();\n    } else {\n        \/\/ Garder niveau + classe : juste r\u00e9initialiser le choix d'\u00e9l\u00e8ve \/ les cases coch\u00e9es\n        const studentSel = document.getElementById('abs-nom');\n        if (studentSel) studentSel.value = '';\n        toggleAllAbsenceCheckboxes(false);\n    }\n    document.getElementById('abs-date').value = getTodaySQLDate();\n    const rangeTg = document.getElementById('abs-range-toggle');\n    if (rangeTg) { rangeTg.checked = false; toggleAbsenceRange(); }\n    const dateFinEl = document.getElementById('abs-date-fin'); if (dateFinEl) dateFinEl.value = '';\n    const noWeEl = document.getElementById('abs-range-noweekend'); if (noWeEl) noWeEl.checked = true;\n    document.getElementById('abs-periode').value = 'matin';\n    document.getElementById('abs-statut').value = 'unjustified';\n    document.getElementById('abs-motif').value = '';\n    const eleveSearchEl = document.getElementById('abs-eleve-search');\n    if(eleveSearchEl) eleveSearchEl.value = '';\n    const eleveSearchResEl = document.getElementById('abs-eleve-search-results');\n    if(eleveSearchResEl) { eleveSearchResEl.classList.remove('open'); eleveSearchResEl.innerHTML = ''; }\n    \/\/ Le mode (simple\/multiple) n'est volontairement pas r\u00e9initialis\u00e9 : on respecte le choix de l'admin\n    \/\/ pour qu'il puisse encha\u00eener plusieurs saisies dans le m\u00eame mode.\n}\n\nfunction publishAnnonce() {\n    const btnSubmit = document.querySelector('[onclick=\"publishAnnonce()\"]');\n    \/\/ [FIX#19] : garde anti-double-soumission \u2014 coh\u00e9rent avec save_parent \/ save_absence\n    if (btnSubmit && btnSubmit.disabled) return;\n\n    let titre = document.getElementById('ann-titre').value.trim();\n    let fileInput = document.getElementById('ann-fichier');\n\n    if (!titre) {\n        showToast(\"Le titre de l'annonce est obligatoire.\", 'error');\n        return;\n    }\n\n    let formData = new FormData();\n    formData.append('action', 'ouzoud_ajax_save_data');\n    formData.append('action_type', 'save_annonce');\n    formData.append('nonce', OuzoudAjax.nonce);\n    formData.append('titre', titre);\n    formData.append('priorite', document.getElementById('ann-priorite').value);\n    formData.append('contenu', document.getElementById('ann-contenu').value);\n\n    if (fileInput.files.length > 0) {\n        let file = fileInput.files[0];\n        if (file.size > 2 * 1024 * 1024) {\n            showToast('Le fichier joint d\u00e9passe la limite de 2 Mo.', 'error');\n            return;\n        }\n        formData.append('ann_fichier', file);\n    }\n\n    \/\/ Verrouiller le bouton\n    if (btnSubmit) { btnSubmit.disabled = true; btnSubmit.textContent = '\u23f3 Publication en cours\u2026'; }\n\n    fetch(OuzoudAjax.url, { method: 'POST', body: formData })\n    .then(r => r.text())\n    .then(text => {\n        let res; try { res = JSON.parse(text); } catch(e) { res = { success: false, data: null }; }\n        \/\/ [FIX#19] : d\u00e9verrouiller dans tous les cas avant de traiter le r\u00e9sultat\n        if (btnSubmit) { btnSubmit.disabled = false; btnSubmit.textContent = 'Lancer la publication'; }\n        if(res.success) { \n            showToast('Annonce publi\u00e9e avec succ\u00e8s.', 'success');\n            document.getElementById('ann-titre').value = ''; \n            document.getElementById('ann-contenu').value = '';\n            \/\/ [FIX#X2] : r\u00e9initialiser la priorit\u00e9 au d\u00e9faut 'normale' apr\u00e8s publication\n            document.getElementById('ann-priorite').value = 'normale';\n            fileInput.value = ''; \n            loadApplicationWorkspace(); \n        } else {\n            showToast(res.data || 'Echec de l\\'enregistrement. Vous devez vous connecter au site en tant qu\\'Admin.', 'error');\n        }\n    })\n    \/\/ [FIX#5] : gestion des erreurs r\u00e9seau manquante sur publishAnnonce\n    .catch(() => {\n        if (btnSubmit) { btnSubmit.disabled = false; btnSubmit.textContent = 'Lancer la publication'; }\n        showToast(\"Erreur r\u00e9seau. L'annonce n'a pas pu \u00eatre publi\u00e9e.\", 'error');\n    });\n}\n\n\/\/ [FIX#22] : nettoyer les donn\u00e9es pour les bo\u00eetes de dialogue confirm() \u2014\n\/\/ le HTML n'est pas interpr\u00e9t\u00e9 dans confirm(), mais les backticks et guillemets\n\/\/ peuvent tronquer ou d\u00e9former le message dans certains moteurs JS. On normalise\n\/\/ en rempla\u00e7ant les caract\u00e8res de contr\u00f4le et les guillemets probl\u00e9matiques.\nfunction _safeConfirmStr(str) {\n    if (typeof str !== 'string') return String(str || '');\n    return str.replace(\/[\\u0000-\\u001F\\u007F]\/g, ' ').replace(\/`\/g, \"'\");\n}\n\nfunction deleteItemRecord(target, id) {\n    \/\/ [FIX#4] : message de confirmation informatif selon la cible\n    let confirmMsg = \"Confirmer la suppression d\u00e9finitive ?\";\n    if (target === 'parent' && ouzoudSessionData && ouzoudSessionData.parents) {\n        let p = ouzoudSessionData.parents.find(x => x.id == id);\n        if (p) {\n            \/\/ [FIX#22] : _safeConfirmStr appliqu\u00e9 sur toutes les valeurs dynamiques\n            confirmMsg = `Supprimer d\u00e9finitivement le parent \"${_safeConfirmStr(p.nom_complet)}\" (${_safeConfirmStr(p.code_parent)}) ?`\n                + (p.enfants ? `\\n\\nEnfants et absences associ\u00e9s supprim\u00e9s : ${_safeConfirmStr(p.enfants.replace(\/\\|\/g, ', '))}` : '')\n                + `\\n\\nCette action est irr\u00e9versible.`;\n        }\n    } else if (target === 'absence' && ouzoudSessionData && ouzoudSessionData.absences) {\n        let a = ouzoudSessionData.absences.find(x => x.id == id);\n        if (a) confirmMsg = `Supprimer l'absence de ${_safeConfirmStr(a.nom_eleve)} du ${formatToFrenchDate(a.date_absence)} ?`;\n    } else if (target === 'devoir' && ouzoudSessionData && ouzoudSessionData.devoirs) {\n        let d = ouzoudSessionData.devoirs.find(x => x.id == id);\n        if (d) confirmMsg = `Supprimer le devoir \"${_safeConfirmStr(d.titre)}\" (${_safeConfirmStr(d.classe)}) ?`;\n    } else if (target === 'annonce' && ouzoudSessionData && ouzoudSessionData.annonces) {\n        let a = ouzoudSessionData.annonces.find(x => x.id == id);\n        if (a) confirmMsg = `Supprimer l'annonce \"${_safeConfirmStr(a.titre)}\" ?`;\n    }\n    if(confirm(confirmMsg)) {\n        let p = new URLSearchParams();\n        p.append('action', 'ouzoud_ajax_delete_data');\n        p.append('nonce', OuzoudAjax.nonce);\n        p.append('target', target);\n        p.append('id', id);\n        fetch(OuzoudAjax.url, { method: 'POST', body: p }).then(r => r.text()).then(text => {\n            let res; try { res = JSON.parse(text); } catch(e) { res = { success: false, data: 'Echec de la suppression. Vous devez vous connecter au site en tant qu\\'Admin.' }; }\n            if(res.success) { showToast('Suppression effectu\u00e9e.', 'info'); loadApplicationWorkspace(); }\n            else { showToast(res.data || 'Echec de la suppression. Vous devez vous connecter au site en tant qu\\'Admin.', 'error'); }\n        }).catch(() => {\n            showToast(\"Erreur r\u00e9seau. La suppression n'a pas pu \u00eatre effectu\u00e9e.\", 'error');\n        });\n    }\n}\n\n\/\/ \u2550\u2550 [BULK] Suppression en lot \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\n\/\/ Cases \u00e0 cocher dans les registres (parents, enseignants, absences, devoirs, annonces,\n\/\/ devoirs de l'espace enseignant). La s\u00e9lection est conserv\u00e9e d'une page \u00e0 l'autre et\n\/\/ \u00ab Tout s\u00e9lectionner \u00bb couvre toute la liste filtr\u00e9e. Le serveur rev\u00e9rifie les droits\n\/\/ pour chaque \u00e9l\u00e9ment (action ouzoud_ajax_bulk_delete_data).\nconst BULK_CFG = {\n    parent:     { tbody:'a-table-parents',     one:'parent',     many:'parents',     fem:false, list:() => (typeof _parentFilteredList !== 'undefined' && _parentFilteredList !== null) ? _parentFilteredList : ((ouzoudSessionData && ouzoudSessionData.parents) || []), render:() => _parentsRenderPage(_parentCurrentPage) },\n    enseignant: { tbody:'a-table-enseignants', one:'enseignant', many:'enseignants', fem:false, list:() => (_ensFilteredList !== null) ? _ensFilteredList : ((ouzoudSessionData && ouzoudSessionData.enseignants) || []), render:() => _ensRenderPage(_ensCurrentPage) },\n    absence:    { tbody:'a-table-absences',    one:'absence',    many:'absences',    fem:true,  list:() => _absFilteredList() || [], render:() => _absRenderPage(_absCurrentPage) },\n    devoir:     { tbody:'a-table-devoirs',     one:'devoir',     many:'devoirs',     fem:false, list:() => _devFilteredList() || [], render:() => _devRenderPage(_devCurrentPage) },\n    annonce:    { tbody:'a-table-annonces',    one:'annonce',    many:'annonces',    fem:true,  list:() => (ouzoudSessionData && ouzoudSessionData.annonces) || [], render:() => _annRenderPage(_annCurrentPage) },\n    tdevoir:    { tbody:'t-table-devoirs',     one:'devoir',     many:'devoirs',     fem:false, serverTarget:'devoir',\n                  list:() => {\n                      const code = (ouzoudSessionData && ouzoudSessionData.code_enseignant) || '';\n                      return ((ouzoudSessionData && ouzoudSessionData.devoirs) || []).filter(d => code && (d.publie_par || '').includes(code));\n                  },\n                  render:() => _tDevRenderPage(_tDevCurrentPage) }\n};\nconst _bulkSel = {};\nlet _bulkBusy = false;\nfunction _bulkSet(t) { return _bulkSel[t] || (_bulkSel[t] = new Set()); }\n\nfunction bulkCell(target, id, enabled) {\n    if (enabled === false) return '<td class=\"bulk-cell\"><\/td>';\n    const checked = _bulkSet(target).has(String(id)) ? ' checked' : '';\n    return `<td class=\"bulk-cell\" data-label=\"\"><label class=\"bulk-lbl\"><input type=\"checkbox\" class=\"bulk-cb\" data-id=\"${parseInt(id)}\" aria-label=\"S\u00e9lectionner\" onchange=\"bulkToggle('${target}', ${parseInt(id)}, this.checked)\"${checked}><span class=\"bulk-lbl-txt\">S\u00e9lectionner<\/span><\/label><\/td>`;\n}\n\nfunction bulkToggle(target, id, on) {\n    const s = _bulkSet(target);\n    if (on) s.add(String(id)); else s.delete(String(id));\n    bulkRefresh(target);\n}\n\n\/\/ Cases de la page affich\u00e9e\nfunction _bulkPageBoxes(target) {\n    const tb = document.getElementById(BULK_CFG[target].tbody);\n    return tb ? Array.from(tb.querySelectorAll('input.bulk-cb')) : [];\n}\n\nfunction bulkTogglePage(target, on) {\n    const s = _bulkSet(target);\n    _bulkPageBoxes(target).forEach(cb => {\n        cb.checked = on;\n        if (on) s.add(String(cb.dataset.id)); else s.delete(String(cb.dataset.id));\n    });\n    bulkRefresh(target);\n}\n\nfunction bulkSelectAll(target) {\n    const s = _bulkSet(target);\n    BULK_CFG[target].list().forEach(x => s.add(String(x.id)));\n    _bulkPageBoxes(target).forEach(cb => { cb.checked = true; });\n    bulkRefresh(target);\n}\n\nfunction bulkClear(target) {\n    _bulkSet(target).clear();\n    _bulkPageBoxes(target).forEach(cb => { cb.checked = false; });\n    bulkRefresh(target);\n}\n\n\/\/ Appel\u00e9 apr\u00e8s chaque rendu de tableau : ajoute colonne d'en-t\u00eate + barre d'outils si besoin,\n\/\/ retire de la s\u00e9lection les \u00e9l\u00e9ments qui n'existent plus, met \u00e0 jour l'affichage.\nfunction bulkSync(target) {\n    const cfg = BULK_CFG[target];\n    const tb = document.getElementById(cfg.tbody);\n    if (!tb) return;\n    const table = tb.closest('table');\n    if (table) {\n        const tr = table.querySelector('thead tr');\n        if (tr && !tr.querySelector('th.bulk-th')) {\n            const th = document.createElement('th');\n            th.className = 'bulk-th';\n            th.innerHTML = `<input type=\"checkbox\" class=\"bulk-cb bulk-all\" id=\"bulk-all-${target}\" title=\"S\u00e9lectionner \/ d\u00e9s\u00e9lectionner la page\" aria-label=\"S\u00e9lectionner la page\" onchange=\"bulkTogglePage('${target}', this.checked)\">`;\n            tr.insertBefore(th, tr.firstChild);\n            const cg = table.querySelector('colgroup');\n            if (cg) { const c = document.createElement('col'); c.style.width = '44px'; cg.insertBefore(c, cg.firstChild); }\n        }\n        if (!document.getElementById('bulk-bar-' + target)) {\n            const bar = document.createElement('div');\n            bar.id = 'bulk-bar-' + target;\n            bar.className = 'bulk-bar';\n            bar.style.display = 'none';\n            bar.innerHTML = `<span class=\"bulk-count\" id=\"bulk-count-${target}\" aria-live=\"polite\"><\/span>\n                <button type=\"button\" class=\"btn btn-outline btn-sm\" id=\"bulk-selall-${target}\" onclick=\"bulkSelectAll('${target}')\"><\/button>\n                <button type=\"button\" class=\"btn btn-outline btn-sm\" onclick=\"bulkClear('${target}')\">D\u00e9s\u00e9lectionner<\/button>\n                <button type=\"button\" class=\"btn btn-sm bulk-del-btn\" id=\"bulk-del-${target}\" onclick=\"bulkDelete('${target}')\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:5px;\"><path d=\"M3 6h18\"\/><path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\"\/><path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"\/><line x1=\"10\" y1=\"11\" x2=\"10\" y2=\"17\"\/><line x1=\"14\" y1=\"11\" x2=\"14\" y2=\"17\"\/><\/svg>Supprimer la s\u00e9lection<\/button>`;\n            const wrap = table ? (table.closest('.tbl-wrap') || table) : tb;\n            wrap.parentNode.insertBefore(bar, wrap);\n        }\n    }\n    \/\/ Purger les ids qui n'existent plus (apr\u00e8s rechargement des donn\u00e9es)\n    const existing = new Set(cfg.list().map(x => String(x.id)));\n    const s = _bulkSet(target);\n    Array.from(s).forEach(id => { if (!existing.has(id)) s.delete(id); });\n    bulkRefresh(target);\n}\n\nfunction bulkRefresh(target) {\n    const cfg = BULK_CFG[target];\n    const bar = document.getElementById('bulk-bar-' + target);\n    const n = _bulkSet(target).size;\n    const boxes = _bulkPageBoxes(target);\n    const all = document.getElementById('bulk-all-' + target);\n    if (all) {\n        const checked = boxes.filter(b => b.checked).length;\n        all.checked = boxes.length > 0 && checked === boxes.length;\n        all.indeterminate = checked > 0 && checked < boxes.length;\n        all.disabled = boxes.length === 0;\n    }\n    if (!bar) return;\n    bar.style.display = n > 0 ? 'flex' : 'none';\n    const total = cfg.list().length;\n    const word = n > 1 ? cfg.many : cfg.one;\n    const sel = n > 1 ? (cfg.fem ? 's\u00e9lectionn\u00e9es' : 's\u00e9lectionn\u00e9s') : (cfg.fem ? 's\u00e9lectionn\u00e9e' : 's\u00e9lectionn\u00e9');\n    const c = document.getElementById('bulk-count-' + target);\n    if (c) c.textContent = `${n} ${word} ${sel}`;\n    const sa = document.getElementById('bulk-selall-' + target);\n    if (sa) { sa.textContent = `Tout s\u00e9lectionner (${total})`; sa.style.display = n < total ? '' : 'none'; }\n}\n\nfunction _bulkConfirmMsg(target, ids) {\n    const cfg = BULK_CFG[target];\n    const n = ids.length;\n    const noun = n > 1 ? cfg.many : cfg.one;\n    let msg = `Supprimer d\u00e9finitivement ${n} ${noun} ?`;\n    if (target === 'parent') msg += `\\n\\nLes enfants et les absences associ\u00e9s seront \u00e9galement supprim\u00e9s.`;\n    if (target === 'enseignant') msg += `\\n\\nLes devoirs publi\u00e9s par ces enseignants seront \u00e9galement supprim\u00e9s.`;\n    if (target === 'devoir' || target === 'tdevoir' || target === 'annonce') msg += `\\n\\nLes fichiers joints seront supprim\u00e9s.`;\n    return msg + `\\n\\nCette action est irr\u00e9versible.`;\n}\n\nasync function bulkDelete(target) {\n    if (_bulkBusy) return;\n    const cfg = BULK_CFG[target];\n    const ids = Array.from(_bulkSet(target));\n    if (!ids.length) return;\n    if (!confirm(_bulkConfirmMsg(target, ids))) return;\n    _bulkBusy = true;\n    const btn = document.getElementById('bulk-del-' + target);\n    const btnHtml = btn ? btn.innerHTML : '';\n    if (btn) { btn.disabled = true; btn.textContent = 'Suppression\u2026'; }\n    let deleted = 0; const failed = []; let fatal = null;\n    const CHUNK = 100;\n    try {\n        for (let i = 0; i < ids.length; i += CHUNK) {\n            const part = ids.slice(i, i + CHUNK);\n            const p = new URLSearchParams();\n            p.append('action', 'ouzoud_ajax_bulk_delete_data');\n            p.append('nonce', OuzoudAjax.nonce);\n            p.append('target', cfg.serverTarget || target);\n            p.append('ids', JSON.stringify(part.map(Number)));\n            const text = await fetch(OuzoudAjax.url, { method: 'POST', body: p }).then(r => r.text());\n            let res; try { res = JSON.parse(text); } catch (e) { res = { success: false, data: \"\u00c9chec de la suppression. Vous devez vous connecter au site en tant qu'Admin.\" }; }\n            if (!res.success) { fatal = res.data || '\u00c9chec de la suppression.'; break; }\n            deleted += res.data.deleted || 0;\n            (res.data.failed || []).forEach(f => failed.push(f));\n            part.forEach(id => { if (!(res.data.failed || []).some(f => String(f.id) === String(id))) _bulkSet(target).delete(String(id)); });\n        }\n    } catch (e) {\n        fatal = 'Erreur r\u00e9seau. La suppression n\\'a pas pu \u00eatre termin\u00e9e.';\n    }\n    _bulkBusy = false;\n    if (btn) { btn.disabled = false; btn.innerHTML = btnHtml; }\n    if (fatal && deleted === 0) {\n        showToast(fatal, 'error');\n        return;\n    }\n    const noun = deleted > 1 ? cfg.many : cfg.one;\n    if (failed.length || fatal) {\n        const reason = fatal || (failed[0] && failed[0].error) || '';\n        showToast(`${deleted} ${noun} supprim\u00e9${deleted > 1 ? 's' : ''}, ${fatal ? 'suite non trait\u00e9e' : failed.length + ' \u00e9chec' + (failed.length > 1 ? 's' : '')}. ${reason}`, 'error', 6000);\n    } else {\n        showToast(`${deleted} ${noun} supprim\u00e9${deleted > 1 ? 's' : ''}.`, 'info');\n    }\n    loadApplicationWorkspace();\n}\n\nfunction openCsvImportModal() {\n    document.getElementById('csv-file-input').value = '';\n    let resultDiv = document.getElementById('csv-import-result');\n    resultDiv.style.display = 'none';\n    resultDiv.innerHTML = '';\n    let btn = document.getElementById('btn-launch-import');\n    btn.disabled = false;\n    btn.innerHTML = '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><path d=\"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z\"\/><path d=\"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z\"\/><path d=\"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0\"\/><path d=\"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5\"\/><\/svg>Lancer l\\'importation';\n    btn.onclick = function() { launchCsvImport(); };\n    document.getElementById('m-csv-import').style.display = 'flex';\n}\n\nfunction downloadCsvTemplate() {\n    let csv = 'nom_complet,code_parent,email,enfants\\n';\n    csv += 'Mme N. Benali,OUZ-' + new Date().getFullYear() + '-1234,benali@email.ma,\"Yassine Benali (3APG-1)|Nour Benali (PS-1)\"\\n';\n    csv += 'M. K. Alaoui,,alaoui@email.ma,\"Karim Alaoui (4APG-1)\"\\n';\n    csv += 'Mme F. Tazi,,,\\n';\n    let blob = new Blob([csv], { type: 'text\/csv;charset=utf-8;' });\n    let url = URL.createObjectURL(blob);\n    let a = document.createElement('a');\n    a.href = url;\n    a.download = 'modele_import_parents.csv';\n    document.body.appendChild(a);\n    a.click();\n    document.body.removeChild(a);\n    URL.revokeObjectURL(url);\n}\n\nfunction launchCsvImport() {\n    let fileInput = document.getElementById('csv-file-input');\n    if (!fileInput.files || fileInput.files.length === 0) {\n        showToast('Veuillez s\u00e9lectionner un fichier CSV.', 'error');\n        return;\n    }\n\n    let btn = document.getElementById('btn-launch-import');\n    btn.disabled = true;\n    btn.innerText = '\u23f3 Importation en cours...';\n\n    let formData = new FormData();\n    formData.append('action', 'ouzoud_ajax_import_csv');\n    formData.append('nonce', OuzoudAjax.nonce);\n    formData.append('csv_file', fileInput.files[0]);\n\n    fetch(OuzoudAjax.url, { method: 'POST', body: formData })\n    .then(r => r.text())\n    .then(text => {\n        let res;\n        try { res = JSON.parse(text); } catch(e) {\n            res = { success: false, data: 'Echec de l\\'importation. Vous devez vous connecter au site en tant qu\\'Admin.' };\n        }\n        let resultDiv = document.getElementById('csv-import-result');\n        resultDiv.style.display = 'block';\n        if (res.success) {\n            let d = res.data;\n            let errorsHtml = '';\n            \/\/ [FIX#8] : afficher les erreurs de lignes m\u00eame si l'import est globalement r\u00e9ussi\n            if (d.errors && d.errors.length > 0) {\n                errorsHtml = `<div style=\"margin-top:10px;padding:10px 12px;background:#FEF3C7;border:1px solid #FCD34D;border-radius:8px;\">\n                    <p style=\"font-weight:700;color:#92400E;margin-bottom:6px;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z\"\/><path d=\"M12 9v4\"\/><path d=\"M12 17h.01\"\/><\/svg>${d.errors.length} ligne(s) avec erreur(s) :<\/p>\n                    <ul style=\"font-size:12px;color:#78350F;padding-left:16px;line-height:1.7;\">${d.errors.map(e => `<li>${escHtml(e)}<\/li>`).join('')}<\/ul>\n                <\/div>`;\n            }\n            resultDiv.innerHTML = `\n                <div style=\"padding:14px;background:#ECFDF5;border:1px solid #6EE7B7;border-radius:10px;\">\n                    <p style=\"font-weight:700;color:#065F46;margin-bottom:8px;\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"\/><path d=\"m9 11 3 3L22 4\"\/><\/svg>Importation r\u00e9ussie !<\/p>\n                    <div style=\"display:flex;gap:16px;flex-wrap:wrap;\">\n                        <span style=\"font-size:13px;color:#047857;\"><b>${d.inserted}<\/b> parent(s) ajout\u00e9(s)<\/span>\n                        <span style=\"font-size:13px;color:#0369A1;\"><b>${d.updated}<\/b> fiche(s) mise(s) \u00e0 jour<\/span>\n                        ${d.skipped > 0 ? `<span style=\"font-size:13px;color:var(--muted);\"><b>${d.skipped}<\/b> ligne(s) ignor\u00e9e(s)<\/span>` : ''}\n                        ${d.empty_lines > 0 ? `<span style=\"font-size:13px;color:var(--muted);\"><b>${d.empty_lines}<\/b> ligne(s) vide(s) ignor\u00e9e(s)<\/span>` : ''}\n                    <\/div>\n                <\/div>${errorsHtml}`;\n            \/\/ Renouveler le nonce apr\u00e8s l'import (l'op\u00e9ration WP peut l'invalider)\n            let nonceP = new URLSearchParams();\n            nonceP.append('action', 'ouzoud_ajax_refresh_nonce');\n            nonceP.append('nonce', OuzoudAjax.nonce);\n            fetch(OuzoudAjax.url, { method: 'POST', body: nonceP })\n                .then(r => r.json())\n                .then(nr => { if (nr.success && nr.data && nr.data.nonce) OuzoudAjax.nonce = nr.data.nonce; })\n                .catch(() => {})\n                .finally(() => loadApplicationWorkspace());\n            btn.disabled = false;\n            btn.innerHTML = '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"\/><path d=\"m9 11 3 3L22 4\"\/><\/svg>Termin\u00e9';\n            btn.onclick = function() { closeOuzoudModal('m-csv-import'); };\n        } else {\n            resultDiv.innerHTML = `<div style=\"padding:14px;background:var(--red-light);border:1px solid #FCA5A5;border-radius:10px;\"><p style=\"font-weight:700;color:var(--red-dark);\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><circle cx=\"12\" cy=\"12\" r=\"10\"\/><path d=\"m15 9-6 6\"\/><path d=\"m9 9 6 6\"\/><\/svg>Erreur : ${escHtml(res.data || 'Echec de l\\'importation. Vous devez vous connecter au site en tant qu\\'Admin.')}<\/p><\/div>`;\n            btn.disabled = false;\n            btn.innerHTML = '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><path d=\"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z\"\/><path d=\"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z\"\/><path d=\"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0\"\/><path d=\"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5\"\/><\/svg>Lancer l\\'importation';\n        }\n    })\n    .catch(() => {\n        btn.disabled = false;\n        btn.innerHTML = '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px;\"><path d=\"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z\"\/><path d=\"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z\"\/><path d=\"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0\"\/><path d=\"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5\"\/><\/svg>Lancer l\\'importation';\n        showToast('Erreur r\u00e9seau lors de l\\'importation.', 'error');\n    });\n}\n\n\/\/ \u2500\u2500 Recherche dans la liste des parents \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction filterParentsTable(query) {\n    \/\/ [CORR Bug#B10] : normalisation des accents pour que \"benali\" trouve \"B\u00e9nali\"\n    function normalizeAccents(str) {\n        return str.normalize('NFD').replace(\/[\\u0300-\\u036f]\/g, '').toLowerCase();\n    }\n    query = normalizeAccents(query.trim());\n\n    if (!query) {\n        _parentFilteredList = null;\n    } else {\n        const all = ouzoudSessionData ? ouzoudSessionData.parents : [];\n        _parentFilteredList = all.filter(p => {\n            const text = normalizeAccents([p.nom_complet, p.code_parent, p.email, p.enfants].filter(Boolean).join(' '));\n            return text.includes(query);\n        });\n    }\n\n    _parentsRenderPage(1);\n}\n\n\/\/ \u2500\u2500 Export CSV de la liste des parents \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction exportParentsCsv() {\n    \/\/ [CORR Bug#B11] : recharger les donn\u00e9es depuis le serveur avant d'exporter\n    \/\/ pour \u00e9viter un export bas\u00e9 sur une snapshot obsol\u00e8te\n    let p = new URLSearchParams();\n    p.append('action', 'ouzoud_ajax_refresh_data');\n    p.append('nonce', OuzoudAjax.nonce);\n\n    fetch(OuzoudAjax.url, { method: 'POST', body: p })\n    .then(r => r.json())\n    .then(res => {\n        if (res.success) {\n            let savedMeta = {\n                _loginType: ouzoudSessionData ? ouzoudSessionData._loginType : null\n            };\n            ouzoudSessionData = res.data;\n            Object.assign(ouzoudSessionData, savedMeta);\n        }\n        _doExportParentsCsv();\n    })\n    .catch(() => {\n        showToast(\"Erreur r\u00e9seau. Export annul\u00e9.\", 'error');\n    });\n}\n\n\/\/ [SEC] Neutralise l'injection de formule CSV (Excel\/LibreOffice\/Google Sheets) :\n\/\/ si un champ commence par =, +, -, @, tabulation ou retour chariot, le tableur peut\n\/\/ l'interpr\u00e9ter comme une formule \u00e0 l'ouverture du fichier (risque RCE\/exfiltration\n\/\/ c\u00f4t\u00e9 poste de l'administrateur). On pr\u00e9fixe d'une apostrophe pour forcer un texte brut,\n\/\/ en plus de l'\u00e9chappement des guillemets d\u00e9j\u00e0 en place.\nfunction csvSafeField(val) {\n    let s = String(val || '').replace(\/\"\/g, '\"\"');\n    if (\/^[=+\\-@\\t\\r]\/.test(s)) s = \"'\" + s;\n    return s;\n}\n\nfunction _doExportParentsCsv() {\n    if (!ouzoudSessionData || !ouzoudSessionData.parents || ouzoudSessionData.parents.length === 0) {\n        showToast('Aucun parent \u00e0 exporter.', 'info');\n        return;\n    }\n\n    const sep = ';';\n    const bom = '\\uFEFF'; \/\/ BOM UTF-8 pour compatibilit\u00e9 Excel\n    let csv = bom + ['nom_complet', 'code_parent', 'email', 'enfants'].join(sep) + '\\n';\n\n    ouzoudSessionData.parents.forEach(p => {\n        const nom     = csvSafeField(p.nom_complet);\n        const code    = csvSafeField(p.code_parent);\n        const email   = csvSafeField(p.email);\n        \/\/ [FIX#6] : p.enfants est d\u00e9j\u00e0 au format \"Nom (Classe)|Nom2 (Classe2)\" c\u00f4t\u00e9 serveur\n        const enfants = csvSafeField(p.enfants);\n        csv += `\"${nom}\"${sep}\"${code}\"${sep}\"${email}\"${sep}\"${enfants}\"\\n`;\n    });\n\n    const blob = new Blob([csv], { type: 'text\/csv;charset=utf-8;' });\n    const url  = URL.createObjectURL(blob);\n    const a    = document.createElement('a');\n    const today = new Date();\n    const dateStr = `${today.getFullYear()}${String(today.getMonth()+1).padStart(2,'0')}${String(today.getDate()).padStart(2,'0')}`;\n    a.href     = url;\n    a.download = `parents_ecole_ouzoud_${dateStr}.csv`;\n    document.body.appendChild(a);\n    a.click();\n    document.body.removeChild(a);\n    URL.revokeObjectURL(url);\n    showToast(`${ouzoudSessionData.parents.length} parent(s) export\u00e9(s) en CSV.`, 'success');\n}\n\/\/ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\/\/ [FIX#4 v4.3] : gestionnaire d\u00e9l\u00e9gu\u00e9 pour les boutons \"Ouvrir fichier\".\n\/\/ R\u00e9sout le token \u00e9ph\u00e9m\u00e8re au moment du clic (pas \u00e0 la construction du DOM),\n\/\/ ce qui \u00e9vite d'exposer le nonce dans les URLs et les tokens trop en avance.\ndocument.addEventListener('click', async function(e) {\n    const btn = e.target.closest('.ouzoud-file-btn');\n    if (!btn) return;\n    e.preventDefault();\n    const rawUrl = btn.getAttribute('data-fileurl');\n    if (!rawUrl) return;\n    const origText = btn.textContent;\n    btn.disabled = true;\n    btn.textContent = '\u23f3';\n    const secureUrl = await buildSecureFileUrl(rawUrl);\n    btn.disabled = false;\n    btn.textContent = origText;\n    if (secureUrl) {\n        window.open(secureUrl, '_blank', 'noopener,noreferrer');\n    } else {\n        showToast('Impossible d\\'ouvrir le fichier. Veuillez r\u00e9essayer.', 'error');\n    }\n});\n\n\/\/ [FIX#20] : remet \u00e0 z\u00e9ro toutes les listes\/\u00e9tats en cache (pagination, filtres,\n\/\/ recherches rapides) pour \u00e9viter qu'une session suivante (m\u00eame r\u00f4le ou non)\n\/\/ n'affiche des pages, filtres ou r\u00e9sultats de recherche d'une session pr\u00e9c\u00e9dente.\nfunction _resetOuzoudCachedState() {\n    \/\/ Pagination espace parent\n    _pAbsCurrentPage = 1;\n    _pDevCurrentPage = 1;\n    _pAnnCurrentPage = 1;\n\n    \/\/ Pagination + filtre espace admin\n    _absCurrentPage = 1;\n    _annCurrentPage = 1;\n    _devCurrentPage = 1;\n    _parentCurrentPage = 1;\n    _parentFilteredList = null;\n\n    \/\/ Pagination espace enseignant\n    _tDevCurrentPage = 1;\n\n    \/\/ Recherche rapide classe (formulaires \"Publier un devoir\" admin & enseignant)\n    _classeSearchActiveIdx = {};\n    _classeSearchMatches = {};\n\n    \/\/ Recherche rapide \u00e9l\u00e8ve\/classe (formulaire \"D\u00e9clarer une absence\")\n    _absEleveSearchActiveIdx = -1;\n    _absEleveSearchMatches = [];\n\n    \/\/ Champ de recherche parents (admin) s'il est encore dans le DOM\n    const parentsSearchInput = document.getElementById('parents-search-input');\n    if (parentsSearchInput) parentsSearchInput.value = '';\n    const parentsSearchEmpty = document.getElementById('parents-search-empty');\n    if (parentsSearchEmpty) parentsSearchEmpty.style.display = 'none';\n\n    \/\/ [FIX32] Filtres des registres (absences \/ devoirs)\n    ['abs-f-search','abs-f-classe','abs-f-statut','abs-f-periode','dev-f-search'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; });\n\n    \/\/ Champ de recherche \u00e9l\u00e8ve\/classe (absences) s'il est encore dans le DOM\n    const absEleveSearch = document.getElementById('abs-eleve-search');\n    if (absEleveSearch) absEleveSearch.value = '';\n    const absEleveSearchResults = document.getElementById('abs-eleve-search-results');\n    if (absEleveSearchResults) { absEleveSearchResults.classList.remove('open'); absEleveSearchResults.innerHTML = ''; }\n}\n\nfunction triggerLogout() {\n    let p = new URLSearchParams();\n    p.append('action', 'ouzoud_ajax_logout');\n    p.append('nonce', OuzoudAjax.nonce);\n    fetch(OuzoudAjax.url, { method: 'POST', body: p }).catch(() => {});\n    ouzoudSessionData = null;\n    toggleLoginTab('parent');\n    _resetOuzoudCachedState();\n    clearParentField();\n    clearAdminField();\n    \/\/ Effacer le nom et les classes enseignant de la topbar pour ne pas les exposer \u00e0 la session suivante\n    const nameBarOut = document.getElementById('teacher-name-bar');\n    if (nameBarOut) nameBarOut.innerText = '';\n    const classesBarOut = document.getElementById('teacher-classes-bar');\n    if (classesBarOut) classesBarOut.innerHTML = '';\n    document.getElementById('ouzoud-parent-app').classList.remove('active');\n    document.getElementById('ouzoud-admin-app').classList.remove('active');\n    document.getElementById('ouzoud-teacher-app').classList.remove('active');\n    document.getElementById('ouzoud-login').style.display = 'flex';\n}\n\n\/\/ \u2500\u2500 Workspace Enseignant : remplir le dropdown Niveau selon les classes de l'enseignant \u2500\u2500\nfunction updateTDevoirNiveauDropdown() {\n    const niveauSel = document.getElementById('t-dev-niveau');\n    if (!niveauSel) return;\n    const config = (ouzoudSessionData && ouzoudSessionData.classes_config) ? ouzoudSessionData.classes_config : {};\n    const teacherClasses = (ouzoudSessionData && Array.isArray(ouzoudSessionData.classes) && ouzoudSessionData.classes.length > 0)\n        ? ouzoudSessionData.classes : null;\n    const ALL_NIVEAUX = [\"PS\",\"MS\",\"GS\",\"1APG\",\"2APG\",\"3APG\",\"4APG\",\"5APG\",\"6APG\",\"1APIC\",\"2APIC\",\"3APIC\",\"TCSF\",\"TCL\",\"1BACL\",\"1BACSEX\",\"1BACECO\",\"1BACSM\",\"2BACPC\",\"2BACSVT\",\"2BACECO\",\"2BACSM\"];\n\n    \/\/ Niveaux ayant au moins une classe assign\u00e9e \u00e0 l'enseignant\n    const niveaux = ALL_NIVEAUX.filter(n => {\n        const cls = config[n] || [];\n        return teacherClasses ? cls.some(c => teacherClasses.includes(c)) : cls.length > 0;\n    });\n\n    const prevNiveau = niveauSel.value;\n    niveauSel.innerHTML = '<option value=\"\" disabled selected>-- Choisir le niveau --<\/option>';\n    niveaux.forEach(n => {\n        const opt = document.createElement('option');\n        opt.value = n; opt.textContent = n;\n        niveauSel.appendChild(opt);\n    });\n    if (prevNiveau && niveaux.includes(prevNiveau)) niveauSel.value = prevNiveau; \/\/ conserve la s\u00e9lection lors d'un rechargement\n}\n\n\/\/ \u2500\u2500 Workspace Enseignant : remplir le dropdown Mati\u00e8re selon les mati\u00e8res de l'enseignant \u2500\u2500\nfunction updateTDevoirMatiereDropdown() {\n    updateTDevoirNiveauDropdown();\n    const matSel = document.getElementById('t-dev-matiere');\n    if (!matSel) return;\n    \/\/ Utiliser les mati\u00e8res de la session (filtr\u00e9es sur cet enseignant si disponibles,\n    \/\/ sinon toutes les mati\u00e8res configur\u00e9es dans les R\u00e9glages WP)\n    const teacherMatieres = (ouzoudSessionData && Array.isArray(ouzoudSessionData.matieres) && ouzoudSessionData.matieres.length > 0)\n        ? ouzoudSessionData.matieres : null;\n    if (teacherMatieres) {\n        matSel.innerHTML = '';\n        teacherMatieres.forEach((m, idx) => {\n            const opt = document.createElement('option');\n            opt.value = m; opt.textContent = m;\n            if (idx === 0) opt.selected = true;\n            matSel.appendChild(opt);\n        });\n    } else {\n        \/\/ Fallback : toutes les mati\u00e8res configur\u00e9es (R\u00e9glages Espace Parents)\n        _buildMatiereOptions(matSel);\n    }\n}\n\n\/\/ \u2500\u2500 Workspace Enseignant : cascade niveau \u2192 classe (filtr\u00e9e sur les classes de l'enseignant) \u2500\u2500\nfunction updateTDevoirClasseDropdown() {\n    const niveauSel = document.getElementById('t-dev-niveau');\n    const classeSel = document.getElementById('t-dev-classe');\n    if (!niveauSel || !classeSel) return;\n    const selectedNiveau = niveauSel.value;\n    const prevClasse = classeSel.value;\n    classeSel.innerHTML = '';\n    if (!selectedNiveau) {\n        classeSel.innerHTML = '<option value=\"\" disabled selected>S\u00e9lectionnez d\\'abord un niveau<\/option>';\n        classeSel.disabled = true;\n        return;\n    }\n    const config = (ouzoudSessionData && ouzoudSessionData.classes_config) ? ouzoudSessionData.classes_config : {};\n    const allClasses = config[selectedNiveau] || [];\n\n    \/\/ Filtrer selon les classes assign\u00e9es \u00e0 l'enseignant (si d\u00e9fini)\n    const teacherClasses = (ouzoudSessionData && Array.isArray(ouzoudSessionData.classes) && ouzoudSessionData.classes.length > 0)\n        ? ouzoudSessionData.classes : null;\n    const classes = teacherClasses ? allClasses.filter(c => teacherClasses.includes(c)) : allClasses;\n\n    if (classes.length === 0) {\n        classeSel.innerHTML = '<option value=\"\" disabled selected>Aucune classe assign\u00e9e pour ce niveau<\/option>';\n        classeSel.disabled = true;\n    } else {\n        classeSel.innerHTML = '<option value=\"\" disabled selected>-- Choisir la classe --<\/option>';\n        classes.forEach(c => {\n            let opt = document.createElement('option');\n            opt.value = c; opt.textContent = c;\n            classeSel.appendChild(opt);\n        });\n        classeSel.disabled = false;\n        if (prevClasse && classes.includes(prevClasse)) classeSel.value = prevClasse;\n        const cbAllT = document.getElementById('t-dev-all-classes');\n        if (cbAllT && cbAllT.checked) classeSel.disabled = true;\n    }\n}\n\n\/\/ \u2500\u2500 Workspace Enseignant : charger l'historique des devoirs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction loadTeacherWorkspace() {\n    loadApplicationWorkspace(); \/\/ refresh ouzoudSessionData puis appelle renderTeacherPanels()\n}\n\n\/\/ \u2500\u2500 Workspace Enseignant : publier un devoir \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction publishTDevoir() {\n    const btnSubmit = document.querySelector('[onclick=\"publishTDevoir()\"]');\n    \/\/ [FIX#19] : garde anti-double-soumission\n    if (btnSubmit && btnSubmit.disabled) return;\n\n    const classes  = _getDevClasses('t-dev');\n    let matiere = _getDevMatiere('t-dev');\n    let titre   = document.getElementById('t-dev-titre').value.trim();\n    let devDate = document.getElementById('t-dev-date').value;\n    let fileInput = document.getElementById('t-dev-fichier');\n\n    const cbAll = document.getElementById('t-dev-all-classes');\n    const allClasses = cbAll && cbAll.checked;\n\n    if (!allClasses && classes.length === 0) { showToast(\"Recherchez et s\u00e9lectionnez une classe.\", 'error'); return; }\n    if (allClasses && classes.length === 0) { showToast(\"Aucune classe trouv\u00e9e pour ce niveau.\", 'error'); return; }\n    if (!matiere) { showToast(\"La mati\u00e8re est obligatoire.\", 'error'); return; }\n    if (!titre) { showToast(\"Le titre est obligatoire.\", 'error'); return; }\n    if (!devDate || !\/^\\d{4}-\\d{2}-\\d{2}$\/.test(devDate)) { showToast(\"Veuillez saisir une date de remise valide.\", 'error'); return; }\n\n    let file = null;\n    if (fileInput.files.length > 0) {\n        file = fileInput.files[0];\n        if (file.size > 2 * 1024 * 1024) { showToast('Le fichier d\u00e9passe la limite de 2 Mo.', 'error'); return; }\n    }\n\n    \/\/ Verrouiller le bouton\n    if (btnSubmit) { btnSubmit.disabled = true; btnSubmit.textContent = '\u23f3 Diffusion en cours\u2026'; }\n\n    const desc = document.getElementById('t-dev-desc').value;\n\n    function sendOne(classe, cb) {\n        let formData = new FormData();\n        formData.append('action', 'ouzoud_ajax_save_data');\n        formData.append('action_type', 'save_devoir');\n        formData.append('nonce', OuzoudAjax.nonce);\n        formData.append('classe', classe);\n        formData.append('matiere', matiere);\n        formData.append('titre', titre);\n        formData.append('desc', desc);\n        formData.append('date', devDate);\n        if (file) formData.append('dev_fichier', file);\n        fetch(OuzoudAjax.url, { method: 'POST', body: formData })\n            .then(r => r.text())\n            .then(text => { let res; try { res = JSON.parse(text); } catch(e) { res = { success: false }; } cb(res.success); })\n            .catch(() => cb(false));\n    }\n\n    let pending = classes.length;\n    let errors = 0;\n    classes.forEach(function(classe) {\n        sendOne(classe, function(ok) {\n            if (!ok) errors++;\n            pending--;\n            if (pending === 0) {\n                \/\/ [FIX#19] : d\u00e9verrouiller le bouton dans tous les cas\n                if (btnSubmit) { btnSubmit.disabled = false; btnSubmit.textContent = '\\u{1F4E4} Diffuser le devoir'; }\n                if (errors === 0) {\n                    showToast(classes.length > 1 ? 'Devoir diffus\u00e9 \u00e0 ' + classes.length + ' classes avec succ\u00e8s.' : 'Devoir publi\u00e9 avec succ\u00e8s.', 'success');\n                    _tDevClearClasse(true);\n                    document.getElementById('t-dev-matiere').value = '';\n                    const tAutreWrap = document.getElementById('t-dev-autre-wrap');\n                    if (tAutreWrap) { tAutreWrap.style.display = 'none'; document.getElementById('t-dev-matiere-autre').value = ''; }\n                    const cbAllEl = document.getElementById('t-dev-all-classes');\n                    if (cbAllEl) cbAllEl.checked = false;\n                    document.getElementById('t-dev-titre').value = 'Devoir maison';\n                    document.getElementById('t-dev-desc').value = '';\n                    document.getElementById('t-dev-date').value = getTomorrowSQLDate();\n                    fileInput.value = '';\n                    const tDevSearchEl = document.getElementById('t-dev-classe-search');\n                    if(tDevSearchEl) tDevSearchEl.value = '';\n                    const tDevSearchResEl = document.getElementById('t-dev-classe-search-results');\n                    if(tDevSearchResEl) { tDevSearchResEl.classList.remove('open'); tDevSearchResEl.innerHTML = ''; }\n                    loadTeacherWorkspace();\n                } else {\n                    showToast(errors + ' erreur(s) lors de l\\'envoi.', 'error');\n                }\n            }\n        });\n    });\n}\n\n\/\/ \u2500\u2500 Workspace Enseignant : supprimer un devoir \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction deleteTDevoir(id) {\n    if (!confirm('Supprimer ce devoir ?')) return;\n    let p = new URLSearchParams();\n    p.append('action', 'ouzoud_ajax_delete_data');\n    p.append('target', 'devoir');\n    p.append('nonce', OuzoudAjax.nonce);\n    p.append('id', id);\n    fetch(OuzoudAjax.url, { method: 'POST', body: p })\n    .then(r => r.json())\n    .then(res => {\n        if (res.success) { showToast('Devoir supprim\u00e9.', 'success'); loadTeacherWorkspace(); }\n        else showToast('\u00c9chec de la suppression.', 'error');\n    })\n    .catch(() => showToast('Erreur r\u00e9seau.', 'error'));\n}\n<\/script>[\/et_pb_fullwidth_code][et_pb_fullwidth_code _builder_version=\u00a0\u00bb4.27.9&Prime; _module_preset=\u00a0\u00bbdefault\u00a0\u00bb hover_enabled=\u00a0\u00bb0&Prime; global_colors_info=\u00a0\u00bb{}\u00a0\u00bb sticky_enabled=\u00a0\u00bb0&Prime;]<\/p>\n<div dir=\"rtl\" style=\"background: linear-gradient(135deg, #e6f1fb 0%, #eff6ff 100%); <!-- [et_pb_line_break_holder] -->            padding: 24px; border-radius: 12px; border-right: 4px solid #3b82f6; <!-- [et_pb_line_break_holder] -->            margin: 30px 0; font-family: &lsquo;Cairo&rsquo;, sans-serif; text-align: right;\u00a0\u00bb><!-- [et_pb_line_break_holder] -->  <\/p>\n<h3 style=\"color: #1e293b; margin: 0 0 12px 0; display: flex; align-items: center; gap: 10px; font-family: 'Cairo', sans-serif; font-size: 16px; font-weight: 700;\"><!-- [et_pb_line_break_holder] -->    <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#3b82f6\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"flex-shrink:0;\"><!-- [et_pb_line_break_holder] -->      <path d=\"M4 19.5A2.5 2.5 0 0 1 6.5 17H20\"\/><!-- [et_pb_line_break_holder] -->      <path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2Z\"\/><!-- [et_pb_line_break_holder] -->      <path d=\"M9 7h7\"\/><!-- [et_pb_line_break_holder] -->      <path d=\"M9 11h7\"\/><!-- [et_pb_line_break_holder] -->    <\/svg><!-- [et_pb_line_break_holder] -->    \u062f\u0644\u064a\u0644 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0641\u0636\u0627\u0621 \u0623\u0648\u0644\u064a\u0627\u0621 \u0627\u0644\u0623\u0645\u0648\u0631<!-- [et_pb_line_break_holder] -->  <\/h3>\n<p><!-- [et_pb_line_break_holder] -->  <pee style=\"color: #475569; margin: 0 0 16px 0;\"><!-- [et_pb_line_break_holder] -->    \u0627\u0643\u062a\u0634\u0641\u0648\u0627 \u0643\u064a\u0641\u064a\u0629 \u0627\u0644\u0648\u0644\u0648\u062c \u0625\u0644\u0649 \u0641\u0636\u0627\u0626\u0643\u0645\u060c \u0648\u0645\u062a\u0627\u0628\u0639\u0629 \u0627\u0644\u063a\u064a\u0627\u0628\u0627\u062a \u0648\u0627\u0644\u0648\u0627\u062c\u0628\u0627\u062a \u0648\u0625\u0639\u0644\u0627\u0646\u0627\u062a \u0627\u0644\u0645\u062f\u0631\u0633\u0629.<!-- [et_pb_line_break_holder] -->  <\/pee><!-- [et_pb_line_break_holder] -->  <a href=\"https:\/\/ecoleouzoud.ac.ma\/?page_id=261170\" <!-- [et_pb_line_break_holder] -->     style=\u00a0\u00bbbackground: #3b82f6; color: white; padding: 12px 28px; <!-- [et_pb_line_break_holder] -->            border-radius: 8px; display: inline-flex; align-items: center; gap: 8px;<!-- [et_pb_line_break_holder] -->            text-decoration: none; font-weight: 800;\u00a0\u00bb><!-- [et_pb_line_break_holder] -->    \u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u062f\u0644\u064a\u0644<!-- [et_pb_line_break_holder] -->    <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"white\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><!-- [et_pb_line_break_holder] -->      <path d=\"M19 12H5\"\/><!-- [et_pb_line_break_holder] -->      <path d=\"m12 19-7-7 7-7\"\/><!-- [et_pb_line_break_holder] -->    <\/svg><!-- [et_pb_line_break_holder] -->  <\/a><!-- [et_pb_line_break_holder] --><\/div>\n<p>[\/et_pb_fullwidth_code][\/et_pb_section]<\/p>\n","protected":false},"excerpt":{"rendered":"\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_et_pb_use_builder":"on","_et_pb_old_content":"","_et_gb_content_width":"","footnotes":""},"class_list":["post-261180","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/ecoleouzoud.ac.ma\/index.php?rest_route=\/wp\/v2\/pages\/261180","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ecoleouzoud.ac.ma\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/ecoleouzoud.ac.ma\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/ecoleouzoud.ac.ma\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/ecoleouzoud.ac.ma\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=261180"}],"version-history":[{"count":5,"href":"https:\/\/ecoleouzoud.ac.ma\/index.php?rest_route=\/wp\/v2\/pages\/261180\/revisions"}],"predecessor-version":[{"id":261552,"href":"https:\/\/ecoleouzoud.ac.ma\/index.php?rest_route=\/wp\/v2\/pages\/261180\/revisions\/261552"}],"wp:attachment":[{"href":"https:\/\/ecoleouzoud.ac.ma\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=261180"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}