Uniting Family Voices.
Parent-led support for families whose children are failed by schools and local authorities—SEND, mental health, bullying, isolation, or any reason. No one understands the struggle better than us. Real hope begins when we unite.

Families struggling with schools and local authorities are everywhere—yet somehow, we remain invisible. We're forced to fight alone when we should have a seat at the table. We're promised support, yet in reality, we’re left isolated, blamed, and unheard.
That’s why we're taking charge ourselves—uniting parent-to-parent, school by school, ensuring no family faces this fight alone.
– Anna, Founder
Why SEND Unity Circle Exists
I lived this fight.
My kids were failed—bullied, tormented every day. The SENCO made it her power trip, blocking our rightful Education, Health, and Care Plan (EHCP). Teachers didn’t help—they were clueless.
My kids stopped sleeping, sank into dark thoughts, and refused school to survive. Who’d stay in a hellhole like that?
The school gaslit us, pinned the blame on us, cut us off. The local Council wouldn’t challenge them; the PTA left us stranded—no one linked us to other parents. We were alone.
I started a WhatsApp group for SEND parents at their school. We swapped tactics, propped each other up, broke the isolation.
This began with SEND—but we quickly saw isolation affects many families. SEND Unity Circle is now a lifeline for any family whose child is let down by the system.
Now, SEND Unity Circle delivers that real, local lifeline to every UK school—for families like us, kids like mine.
The Crisis Our Kids Face
Numbers are soaring—support’s collapsing. Schools and councils profit while our kids bleed.
1 in 5
pupils have SEND (DfE 2024).
1.7M
kids are falling through the cracks.
141,000
school refusers ditched over half their school time in 2023/24-a 25% spike from last year (DfE, Oct 2024).
1 in 4
of them fight suicidal thoughts because their needs get ignored.
These are our kids—bullied, crushed, fleeing hostile schools. When the system screws them, families like mine watch them fall apart.
The System Evades Accountability
It's a rigged game.
The system avoids accountability, blaming budget cuts and funding.
  1. Schools save £6,000 by excluding kids—cutting corners instead of providing support.
  1. Councils dodge £15,000 by rejecting EHCPs—power trips outweigh our kids' futures.
They win when we're divided. The long-term effects extend past school age, undermining the future of our society.
The Fix: We Unite, Our Kids Win
No silence. No isolation. The Unity Circle connects parents school by school—WhatsApp groups that deliver.
One Per School:
Instant backup from parents who know your fight.
Real Tactics:
Swap strategies with those who've survived it.
United Fury:
Forge our rage into a voice schools can't ignore.
We’re through being smashed alone. Together, we guard our kids from collapse-and force the system to face us.
Explore Your Family Tool in Action
We’re almost ready — and you’re getting early access!
This is your space to get clear, confident answers and take action for your child’s needs.
Start with one of these:
<!DOCTYPE html>
<html>
<head>
<title>Chat Test (INSECURE + File Parsing)</title>

<!-- Libraries for File Parsing -->
<!-- pdf.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script>
  // IMPORTANT: Specify the worker source for pdf.js
  pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
</script>
<!-- SheetJS (xlsx) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<!-- Mammoth.js (docx) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.6.0/mammoth.browser.min.js"></script>


<style>
  /* --- Basic Styling (Mostly Unchanged) --- */
  body { font-family: sans-serif; margin: 0; }
  #gpt-chat-widget {
      width: 400px; max-width: 90%; margin: 20px auto;
      border: 1px solid #ccc; border-radius: 8px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2); display: flex;
      flex-direction: column; height: 600px;
      overflow: hidden;
  }
  #gpt-chat-container {
    display: flex; flex-direction: column; flex-grow: 1;
    background-color: #f9f9f9; overflow: hidden;
     border-bottom-left-radius: 8px; border-bottom-right-radius: 8px;
  }
  #gpt-chat-header {
      background-color: #007bff; color: white; padding: 10px 15px;
      display: flex; justify-content: space-between; align-items: center;
       border-top-left-radius: 8px; border-top-right-radius: 8px;
       flex-shrink: 0;
  }
   #gpt-chat-header button {
       background: none; border: none; color: white; font-size: 1.2em; cursor: pointer;
   }
   .warning { color: red; font-weight: bold; text-align: center; padding: 5px; background: #ffeeee; border: 1px solid red; font-size: 0.8em;}

  #gpt-chat-messages {
    flex-grow: 1; overflow-y: auto; padding: 15px; display: flex;
    flex-direction: column; gap: 10px; background-color: #ffffff;
  }
  .gpt-chat-message { padding: 8px 12px; border-radius: 15px; max-width: 80%; word-wrap: break-word; line-height: 1.4; }
  .gpt-chat-message.user { background-color: #007bff; color: white; align-self: flex-end; border-bottom-right-radius: 5px; }
  .gpt-chat-message.assistant { background-color: #e9e9eb; color: #333; align-self: flex-start; border-bottom-left-radius: 5px; }
  /* Style for File Parsing info messages */
  .gpt-chat-message.system-info {
    background-color: #d1ecf1; color: #0c5460; align-self: center;
    font-size: 0.9em; font-style: italic; max-width: 90%;
    text-align: center; border: 1px solid #bee5eb;
   }
  .gpt-chat-message.loading { background-color: #e0e0e0; color: #555; align-self: flex-start; font-style: italic; border-bottom-left-radius: 5px; }
  .gpt-chat-message.error { background-color: #f8d7da; color: #721c24; align-self: flex-start; border: 1px solid #f5c6cb; border-bottom-left-radius: 5px;}

  #gpt-chat-input-area { display: flex; border-top: 1px solid #ccc; padding: 10px; background-color: #f9f9f9; flex-shrink: 0;}
  #gpt-chat-input { flex-grow: 1; border: 1px solid #ccc; border-radius: 20px; padding: 10px 15px; margin-right: 10px; outline: none; }
  #gpt-chat-send { padding: 10px 15px; border: none; background-color: #007bff; color: white; border-radius: 20px; cursor: pointer; transition: background-color 0.2s; }
  #gpt-chat-send:hover { background-color: #0056b3; }
  #gpt-chat-send:disabled { background-color: #a0cfff; cursor: not-allowed; }

  /* --- Settings Modal --- */
  #gpt-settings-modal {
    display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%;
    overflow: auto; background-color: rgba(0,0,0,0.5);
    align-items: center; justify-content: center;
  }
  #gpt-settings-content {
    background-color: #fefefe; margin: auto; padding: 20px;
    border: 1px solid #888; border-radius: 8px; width: 80%;
    max-width: 500px; box-shadow: 0 5px 15px rgba(0,0,0,0.3);
    display: flex; flex-direction: column; gap: 15px;
  }
   #gpt-settings-content h2 { margin-top: 0; text-align: center; }
   #gpt-settings-content label { font-weight: bold; margin-bottom: -10px; }
   #gpt-settings-content select,
   #gpt-settings-content textarea,
   #gpt-settings-content input[type="file"] {
       padding: 8px; border: 1px solid #ccc; border-radius: 4px; width: 100%; box-sizing: border-box;
   }
   #gpt-settings-buttons { display: flex; justify-content: flex-end; gap: 10px; }
   #gpt-settings-buttons button { padding: 10px 20px; border-radius: 5px; cursor: pointer; border: none; }
   #gpt-save-settings { background-color: #28a745; color: white; }
   #gpt-save-settings:hover:not(:disabled) { background-color: #218838; }
   #gpt-save-settings:disabled { background-color: #94d3a2; cursor: not-allowed; }
   #gpt-close-settings { background-color: #6c757d; color: white; }
   #gpt-close-settings:hover { background-color: #5a6268; }
   .gpt-settings-note { font-size: 0.9em; color: #555; }
   .gpt-file-warning { font-weight: bold; color: #dc3545; }
    /* Renamed from ocr-status */
   #file-processing-status {
        font-size: 0.9em;
        color: #007bff;
        margin-top: -10px;
        min-height: 1.2em; /* Prevent layout jump */
   }

</style>
</head>
<body>

<!-- The Chat Widget Container -->
<div id="gpt-chat-widget">
    <!-- Header -->
     <div id="gpt-chat-header">
        <span>Chat Test (INSECURE+File Parse)</span>
        <button id="gpt-settings-open" title="Settings">
âš™</button> <!-- Gear icon --> </div> <!-- Main Chat Area --> <div id="gpt-chat-container"> <div class="warning">INSECURE: API Key visible! Client-Side File Parsing! TESTING ONLY.</div> <div id="gpt-chat-messages"> <div class="gpt-chat-message assistant">Hello! How can I help? You can upload TXT, DOCX, XLSX, or PDF files via Settings to extract text.</div> </div> <div id="gpt-chat-input-area"> <input type="text" id="gpt-chat-input" placeholder="Type your message..."> <button id="gpt-chat-send">Send</button> </div> </div> </div> <!-- Settings Modal --> <div id="gpt-settings-modal"> <div id="gpt-settings-content"> <h2>Settings</h2> <label for="gpt-model-select">Model:</label> <select id="gpt-model-select"> <option value="gpt-4o-mini">GPT-4o Mini</option> <option value="gpt-4o">GPT-4o</option> <!-- Add other CHAT models like gpt-3.5-turbo if needed --> </select> <label for="gpt-system-message">System Message (Optional):</label> <textarea id="gpt-system-message" rows="3" placeholder="e.g., You are a helpful assistant analyzing document text."></textarea> <p class="gpt-settings-note">Guides the AI's behavior. Sent first in every API request.</p> <label for="gpt-file-upload">Upload Document for Text Extraction:</label> <!-- Updated accept attribute --> <input type="file" id="gpt-file-upload" accept=".txt,.pdf,.doc,.docx,.xls,.xlsx,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/plain"> <p class="gpt-settings-note">Select TXT, DOCX, XLSX, or PDF. Text will be extracted when you click 'Save Settings'. Can be slow for large/complex files.</p> <div id="file-processing-status"></div> <!-- Renamed Status Area --> <p class="gpt-settings-note gpt-file-warning">Accuracy varies (esp. for PDF). Older formats (.doc, .xls) may not work reliably. Large files can strain browser & exceed API limits.</p> <div id="gpt-settings-buttons"> <button id="gpt-close-settings">Close</button> <button id="gpt-save-settings">Save Settings</button> </div> </div> </div> <script> // --- Configuration (INSECURE - FOR TESTING ONLY) --- const OPENAI_API_KEY = "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"; // <--- !!! EXTREMELY INSECURE !!! const OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"; // --- DOM Elements --- const messagesContainer = document.getElementById('gpt-chat-messages'); const inputField = document.getElementById('gpt-chat-input'); const sendButton = document.getElementById('gpt-chat-send'); const settingsOpenButton = document.getElementById('gpt-settings-open'); const settingsModal = document.getElementById('gpt-settings-modal'); const settingsCloseButton = document.getElementById('gpt-close-settings'); const settingsSaveButton = document.getElementById('gpt-save-settings'); const modelSelect = document.getElementById('gpt-model-select'); const fileInput = document.getElementById('gpt-file-upload'); const systemMessageInput = document.getElementById('gpt-system-message'); const fileProcessingStatusDiv = document.getElementById('file-processing-status'); // Renamed // --- State --- let conversationHistory = []; let currentModel = modelSelect.value; let currentSystemMessage = ""; let isFileProcessing = false; // Renamed flag // --- Functions --- /** Appends a message to the chat window */ function displayMessage(sender, text, type = 'message') { const messageElement = document.createElement('div'); const senderClass = (sender === 'system-info') ? 'system-info' : sender; messageElement.classList.add('gpt-chat-message', senderClass); if (type === 'error') { messageElement.classList.add('error'); messageElement.textContent = Error: ${String(text).split('\n')[0]}; console.error("Detailed Error:", text); } else if (type === 'loading') { messageElement.classList.add('loading'); messageElement.textContent = text; } else { messageElement.innerHTML = text.replace(/\n/g, '<br>'); } messagesContainer.appendChild(messageElement); messagesContainer.scrollTop = messagesContainer.scrollHeight; return messageElement; } /** Shows/hides loading indicator for API calls */ let apiLoadingElement = null; function showApiLoading(show) { if (show) { if (!apiLoadingElement) { apiLoadingElement = displayMessage('loading', '...', 'loading'); } } else { if (apiLoadingElement) { apiLoadingElement.remove(); apiLoadingElement = null; } } } /** Updates file processing status message in the settings modal */ function updateFileProcessingStatus(message, isError = false) { fileProcessingStatusDiv.textContent = message; fileProcessingStatusDiv.style.color = isError ? '#dc3545' : '#007bff'; } /** Opens the settings modal */ function openSettingsModal() { modelSelect.value = currentModel; systemMessageInput.value = currentSystemMessage; fileInput.value = ''; // Clear file input updateFileProcessingStatus(''); // Clear status settingsSaveButton.disabled = false; isFileProcessing = false; // Reset flag settingsModal.style.display = 'flex'; } /** Closes the settings modal */ function closeSettingsModal() { settingsModal.style.display = 'none'; updateFileProcessingStatus(''); } /** Adds extracted text to conversation history and displays confirmation */ function addExtractedTextToConversation(fileName, text) { if (text && text.trim().length > 0) { const CONTEXT_MESSAGE_LIMIT = 15000; // Arbitrary limit for context message size - adjust as needed let truncatedText = text.trim(); let wasTruncated = false; // Simple truncation if text exceeds the limit if (truncatedText.length > CONTEXT_MESSAGE_LIMIT) { truncatedText = truncatedText.substring(0, CONTEXT_MESSAGE_LIMIT) + "... (truncated due to length)"; wasTruncated = true; console.warn(Text from ${fileName} was truncated to ${CONTEXT_MESSAGE_LIMIT} characters.); } const contextMessage = --- Start of text extracted from ${fileName} ---\n${truncatedText}\n--- End of text extracted from ${fileName} ---; conversationHistory.push({ role: "user", content: contextMessage }); let confirmationMsg = Successfully extracted text from ${fileName}. It has been added to the chat context.; if (wasTruncated) { confirmationMsg += " (Text was truncated due to length)."; } displayMessage('system-info', confirmationMsg); } else { displayMessage('system-info', File ${fileName} processed, but no text content was extracted., 'error'); updateFileProcessingStatus('Processing complete, no text found.', true); } } /** Processes the selected file based on its type */ async function processSelectedFile(file) { if (!file || isFileProcessing) return; const fileName = file.name; const fileExtension = fileName.split('.').pop().toLowerCase(); isFileProcessing = true; settingsSaveButton.disabled = true; updateFileProcessingStatus(Processing ${fileName}...); displayMessage('system-info', Starting text extraction for: ${fileName}... (This may take a while)); try { const reader = new FileReader(); // --- Promise wrapper for FileReader --- const readFileAs = (method) => new Promise((resolve, reject) => { reader.onload = () => resolve(reader.result); reader.onerror = (error) => reject(error); reader[method](file); // Call the appropriate read method (readAsText, readAsArrayBuffer) }); // ------------------------------------ let extractedText = ""; switch (fileExtension) { case 'txt': if (typeof FileReader === "undefined") throw new Error("FileReader API not supported by this browser."); updateFileProcessingStatus('Reading text file...'); extractedText = await readFileAs('readAsText'); break; case 'docx': if (typeof mammoth === "undefined") throw new Error("Mammoth.js library (for DOCX) not loaded."); updateFileProcessingStatus('Reading DOCX file (this may take a moment)...'); const arrayBufferDocx = await readFileAs('readAsArrayBuffer'); updateFileProcessingStatus('Extracting text from DOCX...'); const resultDocx = await mammoth.extractRawText({ arrayBuffer: arrayBufferDocx }); extractedText = resultDocx.value; break; case 'xlsx': if (typeof XLSX === "undefined") throw new Error("SheetJS (XLSX) library not loaded."); updateFileProcessingStatus('Reading XLSX file...'); const arrayBufferXlsx = await readFileAs('readAsArrayBuffer'); updateFileProcessingStatus('Parsing spreadsheet data...'); const workbook = XLSX.read(arrayBufferXlsx, { type: 'array' }); extractedText = ""; workbook.SheetNames.forEach((sheetName, index) => { updateFileProcessingStatus(Extracting text from sheet: ${sheetName}...); const worksheet = workbook.Sheets[sheetName]; const sheetText = XLSX.utils.sheet_to_txt(worksheet, { /* options */ }); if (sheetText && sheetText.trim().length > 0) { extractedText += --- Sheet: ${sheetName} ---\n${sheetText.trim()}\n\n; } }); extractedText = extractedText.trim(); // Remove trailing newlines break; case 'pdf': if (typeof pdfjsLib === "undefined") throw new Error("PDF.js library not loaded."); updateFileProcessingStatus('Reading PDF file...'); const arrayBufferPdf = await readFileAs('readAsArrayBuffer'); updateFileProcessingStatus('Loading PDF document...'); const loadingTask = pdfjsLib.getDocument({ data: arrayBufferPdf }); const pdf = await loadingTask.promise; updateFileProcessingStatus(PDF loaded (${pdf.numPages} pages). Extracting text...); let pdfText = ""; for (let i = 1; i <= pdf.numPages; i++) { updateFileProcessingStatus(Extracting text from PDF page ${i}/${pdf.numPages}...); const page = await pdf.getPage(i); const textContent = await page.getTextContent(); // Simple text concatenation - might lose some formatting/spacing context textContent.items.forEach(item => { pdfText += item.str + (item.hasEOL ? '\n' : ' '); // Add space or newline approximation }); page.cleanup(); // Release page resources pdfText += '\n\n--- Page Break ---\n\n'; // Add page separators } extractedText = pdfText.trim(); break; case 'doc': case 'xls': throw new Error(Parsing older formats (.${fileExtension}) is not reliably supported in the browser.); default: throw new Error(Unsupported file type: .${fileExtension}); } updateFileProcessingStatus('Processing Complete!'); addExtractedTextToConversation(fileName, extractedText); } catch (error) { console.error(Error processing file ${fileName}:, error); updateFileProcessingStatus(Error: ${error.message || 'Unknown processing error'}, true); displayMessage('system-info', Failed to process file ${fileName}. Error: ${error.message}, 'error'); } finally { isFileProcessing = false; // Re-enable save button only if modal is still open if (settingsModal.style.display === 'flex') { settingsSaveButton.disabled = false; // Optionally clear status after a delay setTimeout(() => { if (settingsModal.style.display === 'flex') updateFileProcessingStatus(''); }, 5000); } } } /** Handles saving settings AND triggers file processing if selected */ async function handleSaveSettings() { // 1. Save Model and System Message currentModel = modelSelect.value; currentSystemMessage = systemMessageInput.value.trim(); console.log("Settings updated: Model =", currentModel, "| System Message =", currentSystemMessage || "(none)"); // 2. Check for file and trigger processing (asynchronously) const selectedFile = fileInput.files[0]; if (selectedFile && !isFileProcessing) { // Ensure not already processing await processSelectedFile(selectedFile); // Wait for processing } else if (!selectedFile) { updateFileProcessingStatus(''); // Clear status if no file was selected } // 3. Provide feedback and close modal (if processing isn't active) if (!isFileProcessing) { let feedback = Model set to: ${currentModel}.; if (currentSystemMessage) feedback += ` System message updated.`; if (selectedFile) feedback += ` File processing started/completed for ${selectedFile.name}.`; else feedback += ` No file selected.` alert(feedback); closeSettingsModal(); } else { alert(Model and System Message saved. File processing is running for ${selectedFile.name}. Modal will remain open.); // Don't close modal, let processSelectedFile handle UI updates. } } /** Sends message to OpenAI API */ async function handleSendMessage() { const userMessage = inputField.value.trim(); if (!userMessage) return; if (!OPENAI_API_KEY || OPENAI_API_KEY === "YOUR_API_KEY_HERE" || !OPENAI_API_KEY.startsWith("sk-")) { displayMessage('assistant', "API Key is missing or invalid in the code.", 'error'); return; } inputField.disabled = true; sendButton.disabled = true; displayMessage('user', userMessage); conversationHistory.push({ role: "user", content: userMessage }); inputField.value = ''; showApiLoading(true); const messagesToSend = []; if (currentSystemMessage) { messagesToSend.push({ role: "system", content: currentSystemMessage }); } messagesToSend.push(...conversationHistory); // Includes user chat & extracted file text // --- Context Length Check (VERY Basic) --- const estimatedTokens = JSON.stringify(messagesToSend).length / 3; // Rough estimate const TOKEN_LIMIT_WARNING = 30000; // Set lower than actual model limit (e.g. gpt-4o-mini 128k) if (estimatedTokens > TOKEN_LIMIT_WARNING) { console.warn(Estimated token count (${estimatedTokens.toFixed(0)}) is high. May exceed API limits.); displayMessage('system-info', 'Warning: Conversation history + file content is long and might exceed API limits, potentially causing errors or dropped context.'); } // ----------------------------------------- console.log("Sending messages to API:", messagesToSend); // Log for debugging try { const requestData = { model: currentModel, messages: messagesToSend, }; const response = await fetch(OPENAI_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': Bearer ${OPENAI_API_KEY} // INSECURE }, body: JSON.stringify(requestData), }); showApiLoading(false); if (!response.ok) { let errorMsg = API Error (${response.status}); try { const errorData = await response.json(); if (errorData.error && errorData.error.message) { errorMsg = errorData.error.message; if (errorMsg.includes('context_length_exceeded')) { errorMsg += "\n(Try asking shorter questions, using smaller files, or clear the chat history [feature not implemented])."; } } else { errorMsg = ${errorMsg} - ${response.statusText}; } } catch (e) { errorMsg = ${errorMsg} - ${response.statusText}; } throw new Error(errorMsg); } const result = await response.json(); if (result.choices && result.choices.length > 0 && result.choices[0].message) { const assistantReply = result.choices[0].message.content; const assistantRole = result.choices[0].message.role || 'assistant'; displayMessage(assistantRole, assistantReply); conversationHistory.push({ role: assistantRole, content: assistantReply }); } else { throw new Error('Received an unexpected response format from the API.'); } } catch (error) { showApiLoading(false); console.error('Error sending message:', error); if (error.message && error.message.includes('context_length_exceeded')) { displayMessage('assistant', error.message, 'error'); } else if (error instanceof TypeError) { displayMessage('assistant', 'Network Error: Could not connect to API. Often CORS related. Check browser console (F12).', 'error'); } else { displayMessage('assistant', error.message || 'An unknown error occurred.', 'error'); } } finally { inputField.disabled = false; sendButton.disabled = false; inputField.focus(); } } // --- Event Listeners --- sendButton.addEventListener('click', handleSendMessage); inputField.addEventListener('keypress', (event) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); handleSendMessage(); } }); settingsOpenButton.addEventListener('click', openSettingsModal); settingsCloseButton.addEventListener('click', closeSettingsModal); settingsSaveButton.addEventListener('click', handleSaveSettings); settingsModal.addEventListener('click', (event) => { if (event.target === settingsModal && !isFileProcessing) { // Don't close if processing closeSettingsModal(); } }); fileInput.addEventListener('change', () => { if (fileInput.files && fileInput.files.length > 0) { updateFileProcessingStatus(Selected: ${fileInput.files[0].name}); } else { updateFileProcessingStatus(''); } }); // --- Initial Setup --- inputField.focus(); console.warn("-----------------------------------------------------"); console.warn("EXTREME SECURITY WARNING: API Key hardcoded & public!"); console.warn("DO NOT use this on a live site. For isolated testing ONLY."); console.warn("Client-side parsing enabled for TXT, DOCX, XLSX, PDF."); console.warn("Parsing can be SLOW and resource-intensive. Accuracy varies (esp. PDF)."); console.warn("Large file content + chat history may exceed API context limits."); console.warn("Direct API calls may fail due to CORS."); console.warn("-----------------------------------------------------"); </script> </body> </html>
How To Join
Break the isolation. Join our collective voice. Together, we win.
1. Click Here:
2. Find Your School
Check your region and school in the hub.
Not there?
Email anna@sendunitycircle.com or ping an admin in the chat to add it.
3. Start Your Own
đź“© Email us at anna@sendunitycircle.com to add your school or launch your own group.
No family alone. No kid left behind.