<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Interactive Archives - Tantrumming Trailblazers</title>
	<atom:link href="https://tantrummingtrailblazers.com/tag/interactive/feed/" rel="self" type="application/rss+xml" />
	<link>https://tantrummingtrailblazers.com/tag/interactive/</link>
	<description>Where Travel and Tantrums Collide in Epic Journeys!</description>
	<lastBuildDate>Sat, 19 Jul 2025 07:04:34 +0000</lastBuildDate>
	<language>en-GB</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9</generator>

<image>
	<url>https://tantrummingtrailblazers.com/wp-content/uploads/2024/06/cropped-DALL·E-2024-06-22-12.00.14-A-minimalist-logo-featuring-three-lines-and-three-colors.-The-design-includes-one-diagonal-line-from-the-bottom-left-to-the-top-right-in-bright-orange-32x32.webp</url>
	<title>Interactive Archives - Tantrumming Trailblazers</title>
	<link>https://tantrummingtrailblazers.com/tag/interactive/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">229502464</site>	<item>
		<title>Digital Emoji Learning Tool SEN Visual Communication Cards </title>
		<link>https://tantrummingtrailblazers.com/ai/sen-visual-communication-cards/</link>
					<comments>https://tantrummingtrailblazers.com/ai/sen-visual-communication-cards/#respond</comments>
		
		<dc:creator><![CDATA[Dennis]]></dc:creator>
		<pubDate>Thu, 13 Mar 2025 16:19:03 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[Customizable]]></category>
		<category><![CDATA[Educational]]></category>
		<category><![CDATA[Emoji-Based]]></category>
		<category><![CDATA[Interactive]]></category>
		<category><![CDATA[Learning]]></category>
		<category><![CDATA[Tool]]></category>
		<guid isPermaLink="false">https://tantrummingtrailblazers.com/?p=4083</guid>

					<description><![CDATA[<p>Hello, after realising we don’t always have our cards with us I decided to create a SEN Digital</p>
<p>The post <a href="https://tantrummingtrailblazers.com/ai/sen-visual-communication-cards/">Digital Emoji Learning Tool SEN Visual Communication Cards </a> appeared first on <a href="https://tantrummingtrailblazers.com">Tantrumming Trailblazers</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><a class="a2a_button_facebook" href="https://www.addtoany.com/add_to/facebook?linkurl=https%3A%2F%2Ftantrummingtrailblazers.com%2Fai%2Fsen-visual-communication-cards%2F&amp;linkname=Digital%20Emoji%20Learning%20Tool%20SEN%20Visual%20Communication%20Cards%C2%A0" title="Facebook" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_twitter" href="https://www.addtoany.com/add_to/twitter?linkurl=https%3A%2F%2Ftantrummingtrailblazers.com%2Fai%2Fsen-visual-communication-cards%2F&amp;linkname=Digital%20Emoji%20Learning%20Tool%20SEN%20Visual%20Communication%20Cards%C2%A0" title="Twitter" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Ftantrummingtrailblazers.com%2Fai%2Fsen-visual-communication-cards%2F&amp;linkname=Digital%20Emoji%20Learning%20Tool%20SEN%20Visual%20Communication%20Cards%C2%A0" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Ftantrummingtrailblazers.com%2Fai%2Fsen-visual-communication-cards%2F&#038;title=Digital%20Emoji%20Learning%20Tool%20SEN%20Visual%20Communication%20Cards%C2%A0" data-a2a-url="https://tantrummingtrailblazers.com/ai/sen-visual-communication-cards/" data-a2a-title="Digital Emoji Learning Tool SEN Visual Communication Cards "></a></p>
<p>Hello, after realising we don’t always have our cards with us I decided to create a SEN Digital Emoji PECs system using emojis that brings interactive, emoji-based learning right to you. Designed with my daughter Dotty in mind, it lets you effortlessly create and manage custom categories and items with editable emojis and names. Whether you&#8217;re adding everyday items or creating a playful learning journey, everything is at your fingertips.</p>



<p>P.S &#8211; if you sign up and login the edits you make will save and it syncs seamlessly across devices all for free, as Maui said, you’re welcome! Emoji Learning Tool<br><br>    <div id="dotty-app-root"></div>

    <script>
      window.DottyToolConfig = {
        ajaxUrl: "https:\/\/tantrummingtrailblazers.com\/wp-admin\/admin-ajax.php",
        nonce: "3afe0d7ce1",
        serviceWorkerUrl: "https:\/\/tantrummingtrailblazers.com\/wp-content\/plugins\/SEN TOOL\/service-worker.js",
        isLoggedIn: false,
        canUpload: false      };
    </script>

    <style>
      #dotty-app-root,
      #dotty-app-root * {
        box-sizing: border-box;
      }

      #dotty-app-root {
        --dt-bg: #f6f8fc;
        --dt-surface: #ffffff;
        --dt-surface-2: #eef4ff;
        --dt-border: #d9e2f1;
        --dt-text: #172033;
        --dt-muted: #5f6f8c;
        --dt-primary: #2f80ed;
        --dt-primary-2: #1f66c7;
        --dt-success: #16a34a;
        --dt-danger: #dc2626;
        --dt-warning: #f59e0b;
        --dt-purple: #8b5cf6;
        --dt-shadow: 0 12px 30px rgba(31, 60, 116, 0.12);
        --dt-gap: 14px;
        --dt-card-size: 190px;
        --dt-font: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
        font-family: var(--dt-font);
        color: var(--dt-text);
        margin: 18px 0;
      }

      #dotty-app-root.dt-high-contrast {
        --dt-bg: #000000;
        --dt-surface: #111111;
        --dt-surface-2: #1a1a1a;
        --dt-border: #ffffff;
        --dt-text: #ffffff;
        --dt-muted: #d4d4d4;
        --dt-primary: #00c2ff;
        --dt-primary-2: #00a3d6;
        --dt-success: #22c55e;
        --dt-danger: #ff4d4d;
        --dt-warning: #ffd166;
        --dt-shadow: none;
      }

      #dotty-app-root.dt-large-text {
        --dt-card-size: 205px;
      }

      .dt-shell {
        background: linear-gradient(180deg, var(--dt-bg) 0%, #ffffff 100%);
        border: 1px solid var(--dt-border);
        border-radius: 28px;
        overflow: hidden;
        box-shadow: var(--dt-shadow);
      }

      .dt-header {
        background: linear-gradient(135deg, var(--dt-primary), #6aa8f5);
        color: #fff;
        padding: 18px 18px 16px;
      }

      .dt-header-top {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
        flex-wrap: wrap;
      }

      .dt-title-wrap h1 {
        margin: 0;
        font-size: 1.8rem;
        line-height: 1.1;
        font-weight: 800;
        letter-spacing: -0.02em;
      }

      .dt-title-wrap p {
        margin: 6px 0 0;
        opacity: 0.95;
        font-size: 0.98rem;
      }

      .dt-toolbar {
        display: flex;
        gap: 10px;
        flex-wrap: wrap;
      }

      .dt-btn {
        appearance: none;
        border: none;
        border-radius: 999px;
        padding: 11px 16px;
        cursor: pointer;
        font-weight: 700;
        font-size: 0.95rem;
        transition: transform .15s ease, filter .15s ease, background .15s ease;
        min-height: 44px;
      }

      .dt-btn:hover {
        transform: translateY(-1px);
        filter: brightness(.98);
      }

      .dt-btn:focus-visible,
      .dt-card:focus-visible,
      .dt-action-btn:focus-visible,
      .dt-input:focus-visible,
      .dt-select:focus-visible,
      .dt-range:focus-visible,
      .dt-chip:focus-visible,
      .dt-template-chip:focus-visible,
      .dt-mini-btn:focus-visible,
      .dt-routine-card-main:focus-visible,
      .dt-fav-card:focus-visible,
      .dt-sentence-token-remove:focus-visible {
        outline: 3px solid rgba(47, 128, 237, 0.35);
        outline-offset: 2px;
      }

      .dt-btn-primary { background: #fff; color: var(--dt-primary-2); }
      .dt-btn-warning { background: #fff3cd; color: #6a4c00; }
      .dt-btn-success { background: #dcfce7; color: #166534; }
      .dt-btn-danger  { background: #fee2e2; color: #991b1b; }
      .dt-btn-purple  { background: #f3e8ff; color: #6b21a8; }
      .dt-btn-ghost   { background: rgba(255,255,255,.15); color: #fff; border: 1px solid rgba(255,255,255,.25); }

      .dt-main {
        padding: 14px;
        background: var(--dt-bg);
      }

      .dt-topbar {
        display: grid;
        grid-template-columns: 1fr auto;
        gap: 14px;
        align-items: center;
        margin-bottom: 14px;
      }

      .dt-breadcrumbs {
        display: flex;
        gap: 8px;
        align-items: center;
        flex-wrap: wrap;
        color: var(--dt-muted);
        font-weight: 700;
      }

      .dt-chip {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        background: var(--dt-surface);
        border: 1px solid var(--dt-border);
        border-radius: 999px;
        padding: 8px 12px;
        font-size: 0.95rem;
        cursor: pointer;
      }

      .dt-settings {
        display: flex;
        gap: 10px;
        flex-wrap: wrap;
        justify-content: flex-end;
      }

      .dt-switch {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        background: var(--dt-surface);
        border: 1px solid var(--dt-border);
        border-radius: 999px;
        padding: 8px 12px;
        font-weight: 700;
        color: var(--dt-text);
      }

      .dt-switch input {
        width: 18px;
        height: 18px;
      }

      .dt-panel {
        background: var(--dt-surface);
        border: 1px solid var(--dt-border);
        border-radius: 20px;
        padding: 12px;
        margin-bottom: 12px;
      }

      .dt-panel-grid {
        display: grid;
        grid-template-columns: 1.2fr .8fr;
        gap: 14px;
      }

      .dt-search-wrap {
        display: flex;
        gap: 10px;
        align-items: center;
      }

      .dt-input,
      .dt-select,
      .dt-textarea {
        width: 100%;
        min-height: 46px;
        padding: 12px 14px;
        border: 1px solid var(--dt-border);
        border-radius: 14px;
        background: #fff;
        color: var(--dt-text);
        font-size: 1rem;
      }

      .dt-textarea {
        min-height: 96px;
        resize: vertical;
      }

      .dt-range {
        width: 100%;
      }

      .dt-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(var(--dt-card-size), 1fr));
        gap: var(--dt-gap);
      }

      .dt-card {
        position: relative;
        min-height: var(--dt-card-size);
        border: 1px solid var(--dt-border);
        border-radius: 22px;
        background: linear-gradient(180deg, var(--dt-surface) 0%, var(--dt-surface-2) 100%);
        box-shadow: 0 8px 18px rgba(0,0,0,.06);
        padding: 12px;
        text-align: center;
        display: grid;
        grid-template-rows: 92px auto auto;
        align-content: start;
        gap: 6px;
        cursor: pointer;
        user-select: none;
        transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
        overflow: hidden;
      }

      .dt-card:hover {
        transform: translateY(-2px) scale(1.01);
        box-shadow: 0 12px 24px rgba(0,0,0,.1);
      }

      .dt-card.is-active {
        border-color: var(--dt-primary);
        box-shadow: 0 0 0 4px rgba(47,128,237,.14);
      }

      .dt-card.is-add {
        border-style: dashed;
        background: #f8fff9;
      }

      .dt-card.style-success { background: linear-gradient(180deg, #ecfdf3 0%, #d1fae5 100%); }
      .dt-card.style-danger  { background: linear-gradient(180deg, #fef2f2 0%, #fee2e2 100%); }
      .dt-card.style-warning { background: linear-gradient(180deg, #fffbeb 0%, #fde68a 100%); }
      .dt-card.style-primary { background: linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%); }
      .dt-card.style-purple  { background: linear-gradient(180deg, #f5f3ff 0%, #ede9fe 100%); }
      .dt-card.style-muted   { background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%); }

      .dt-card-image-wrap {
        width: 100%;
        height: 92px;
        border-radius: 16px;
        overflow: hidden;
        background: #fff;
        border: 1px solid rgba(0,0,0,.05);
        display: flex;
        align-items: center;
        justify-content: center;
      }

      .dt-card.has-image-only {
        grid-template-rows: 120px auto;
      }

      .dt-card.has-image-only .dt-card-image-wrap {
        height: 120px;
      }

      .dt-card-image {
        width: 100%;
        height: 100%;
        object-fit: cover;
        display: block;
      }

      .dt-card-media {
        width: 100%;
        height: 72px !important;
        min-height: 72px !important;
        max-height: 72px !important;
        display: flex !important;
        align-items: center !important;
        justify-content: center !important;
        position: relative !important;
        overflow: hidden !important;
        padding: 4px !important;
      }

      .dt-card-emoji {
        width: 100% !important;
        height: 100% !important;
        display: flex !important;
        align-items: center !important;
        justify-content: center !important;
        text-align: center !important;
        line-height: 1 !important;
        overflow: hidden !important;
        font-size: 2rem !important;
      }

      .dt-card-emoji img,
      .dt-card-emoji img.emoji,
      .dt-card-emoji svg,
      .dt-card-media img.emoji {
        width: 56px !important;
        height: 56px !important;
        max-width: 56px !important;
        max-height: 56px !important;
        object-fit: contain !important;
        display: block !important;
        margin: 0 auto !important;
      }

      .dt-card-emoji-badge {
        position: absolute;
        right: 10px;
        bottom: -8px;
        width: 38px;
        height: 38px;
        border-radius: 999px;
        background: rgba(255,255,255,.96);
        border: 1px solid var(--dt-border);
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 1rem;
        box-shadow: 0 6px 14px rgba(0,0,0,.1);
      }

      .dt-card-title {
        font-size: 1.08rem;
        font-weight: 800;
        line-height: 1.15;
        word-break: break-word;
        margin-top: 2px;
      }

      .dt-card-sub {
        font-size: .85rem;
        color: var(--dt-muted);
        font-weight: 700;
      }

      .dt-card-actions {
        position: absolute;
        inset: 8px 8px auto 8px;
        display: flex;
        justify-content: space-between;
        gap: 8px;
        pointer-events: none;
        z-index: 4;
      }

      .dt-card-actions-right,
      .dt-card-actions-left {
        display: flex;
        gap: 6px;
        pointer-events: auto;
      }

      .dt-action-btn {
        appearance: none;
        border: none;
        border-radius: 10px;
        padding: 7px 9px;
        cursor: pointer;
        font-weight: 800;
        min-height: 34px;
        min-width: 34px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
      }

      .dt-action-edit   { background: #f3e8ff; color: #6b21a8; }
      .dt-action-delete { background: #fee2e2; color: #991b1b; }
      .dt-action-move   { background: #dbeafe; color: #1d4ed8; }
      .dt-action-star   { background: #fef3c7; color: #92400e; }

      .dt-title-row {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
        margin-bottom: 10px;
        flex-wrap: wrap;
      }

      .dt-title-row h2 {
        margin: 0;
        font-size: 1.28rem;
        font-weight: 900;
        letter-spacing: -0.02em;
      }

      .dt-favourites-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
        gap: 10px;
      }

      .dt-fav-card {
        position: relative;
        border: 1px solid var(--dt-border);
        background: linear-gradient(180deg, #fff 0%, #f8fbff 100%);
        border-radius: 16px;
        min-height: 98px;
        padding: 8px;
        display: grid;
        grid-template-rows: 42px auto;
        align-items: center;
        justify-items: center;
        text-align: center;
        cursor: pointer;
        font-weight: 800;
        transition: transform .15s ease, box-shadow .15s ease;
      }

      .dt-fav-card:hover {
        transform: translateY(-1px);
        box-shadow: 0 6px 14px rgba(0,0,0,.08);
      }

      .dt-fav-card .emoji {
        font-size: 1.7rem;
        line-height: 1;
      }

      .dt-fav-card .img-wrap {
        width: 42px;
        height: 42px;
        border-radius: 10px;
        overflow: hidden;
        border: 1px solid var(--dt-border);
        background: #fff;
      }

      .dt-fav-card .img-wrap img {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }

      .dt-fav-card .label {
        font-size: .82rem;
        line-height: 1.05;
        word-break: break-word;
      }

      .dt-sentence {
        position: sticky;
        bottom: 0;
        z-index: 20;
        background: rgba(246, 248, 252, 0.96);
        backdrop-filter: blur(10px);
        border-top: 1px solid var(--dt-border);
        padding: 10px 14px 14px;
      }

      .dt-sentence-inner {
        background: var(--dt-surface);
        border: 1px solid var(--dt-border);
        border-radius: 20px;
        padding: 10px;
        box-shadow: var(--dt-shadow);
      }

      .dt-sentence-top {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
        margin-bottom: 10px;
        flex-wrap: wrap;
      }

      .dt-sentence-top h3 {
        margin: 0;
        font-size: 1.05rem;
      }

      .dt-sentence-actions {
        display: flex;
        gap: 8px;
        flex-wrap: wrap;
      }

      .dt-sentence-strip {
        display: flex;
        gap: 10px;
        overflow-x: auto;
        padding-bottom: 2px;
      }

      .dt-sentence-token {
        position: relative;
        min-width: 92px;
        background: linear-gradient(180deg, #fff 0%, #f6faff 100%);
        border: 1px solid var(--dt-border);
        border-radius: 18px;
        padding: 10px 8px;
        text-align: center;
        font-weight: 800;
        flex: 0 0 auto;
      }

      .dt-sentence-token .emoji {
        display: block;
        font-size: 2rem;
        line-height: 1;
        margin-bottom: 6px;
      }

      .dt-sentence-token .img-wrap {
        width: 56px;
        height: 56px;
        margin: 0 auto 6px;
        overflow: hidden;
        border-radius: 12px;
        background: #fff;
        border: 1px solid var(--dt-border);
      }

      .dt-sentence-token .img-wrap img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        display: block;
      }

      .dt-sentence-token-remove {
        position: absolute;
        top: 6px;
        right: 6px;
        width: 24px;
        height: 24px;
        border-radius: 999px;
        border: none;
        background: #fee2e2;
        color: #991b1b;
        font-weight: 900;
        cursor: pointer;
        display: inline-flex;
        align-items: center;
        justify-content: center;
      }

      .dt-routine-list {
        display: flex;
        gap: 12px;
        flex-wrap: wrap;
      }

      .dt-routine-card {
        background: linear-gradient(180deg, #fff 0%, #f4f8ff 100%);
        border: 1px solid var(--dt-border);
        border-radius: 18px;
        padding: 12px;
        min-width: 150px;
        max-width: 220px;
      }

      .dt-routine-card-main {
        width: 100%;
        border: none;
        background: transparent;
        cursor: pointer;
        text-align: center;
        padding: 0;
      }

      .dt-routine-card-main .e {
        display: block;
        font-size: 2rem;
        margin-bottom: 4px;
      }

      .dt-routine-card-main .t {
        display: block;
        font-size: .92rem;
        font-weight: 800;
        line-height: 1.15;
      }

      .dt-routine-card-main .s {
        display: block;
        margin-top: 6px;
        font-size: .78rem;
        color: var(--dt-muted);
        font-weight: 700;
      }

      .dt-routine-actions {
        display: flex;
        gap: 6px;
        flex-wrap: wrap;
        justify-content: center;
        margin-top: 10px;
      }

      .dt-mini-btn {
        appearance: none;
        border: none;
        border-radius: 999px;
        min-height: 32px;
        padding: 6px 10px;
        font-size: .82rem;
        font-weight: 800;
        cursor: pointer;
      }

      .dt-mini-btn.edit   { background: #f3e8ff; color: #6b21a8; }
      .dt-mini-btn.delete { background: #fee2e2; color: #991b1b; }
      .dt-mini-btn.move   { background: #dbeafe; color: #1d4ed8; }
      .dt-mini-btn.copy   { background: #ecfdf3; color: #166534; }

      .dt-template-list {
        display: flex;
        gap: 10px;
        flex-wrap: wrap;
      }

      .dt-template-chip {
        border: 1px solid var(--dt-border);
        background: linear-gradient(180deg, #fff 0%, #f8fbff 100%);
        padding: 10px 12px;
        border-radius: 999px;
        font-weight: 800;
        cursor: pointer;
        font-size: .9rem;
        line-height: 1;
      }

      .dt-template-chip:hover {
        transform: translateY(-1px);
      }

      .dt-template-chip.is-disabled {
        opacity: .65;
      }

      .dt-empty {
        padding: 16px;
        text-align: center;
        color: var(--dt-muted);
        font-weight: 700;
        border: 2px dashed var(--dt-border);
        border-radius: 18px;
        background: #fff;
      }

      .dt-modal {
        border: none;
        border-radius: 20px;
        padding: 0;
        width: min(760px, calc(100vw - 20px));
        max-height: calc(100vh - 20px);
        overflow: hidden;
        background: transparent;
      }

      .dt-modal::backdrop {
        background: rgba(10, 20, 40, 0.45);
      }

      .dt-modal-card {
        background: var(--dt-surface);
        color: var(--dt-text);
        border-radius: 20px;
        overflow: hidden;
        border: 1px solid var(--dt-border);
      }

      .dt-modal-header {
        padding: 16px 18px;
        background: linear-gradient(135deg, var(--dt-primary), #6aa8f5);
        color: #fff;
      }

      .dt-modal-header h3 {
        margin: 0;
        font-size: 1.2rem;
      }

      .dt-modal-body {
        padding: 14px 16px 16px;
        max-height: 78vh;
        overflow: auto;
      }

      .dt-form-grid {
        display: grid;
        grid-template-columns: 1fr;
        gap: 12px;
      }

      .dt-form-group {
        margin-bottom: 12px;
      }

      .dt-form-group label {
        display: block;
        margin-bottom: 6px;
        font-weight: 800;
      }

      .dt-form-help {
        font-size: .9rem;
        color: var(--dt-muted);
      }

      .dt-emoji-search {
        margin-bottom: 14px;
      }

      .dt-emoji-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
        gap: 8px;
        max-height: 240px;
        overflow-y: auto;
        padding-right: 4px;
      }

      .dt-emoji-option {
        border: 1px solid var(--dt-border);
        border-radius: 12px;
        background: #fff;
        padding: 8px 4px;
        text-align: center;
        cursor: pointer;
        min-height: 72px;
        transition: transform .15s ease, border-color .15s ease, background .15s ease;
      }

      .dt-emoji-option:hover {
        transform: translateY(-1px);
      }

      .dt-emoji-option.is-selected {
        border-color: var(--dt-primary);
        background: #eff6ff;
        box-shadow: 0 0 0 3px rgba(47,128,237,.14);
      }

      .dt-emoji-option .e {
        display: block;
        font-size: 1.7rem;
        line-height: 1;
        margin-bottom: 4px;
      }

      .dt-emoji-option .n {
        display: block;
        font-size: .68rem;
        color: var(--dt-muted);
        font-weight: 700;
        line-height: 1.1;
      }

      .dt-modal-actions {
        display: flex;
        justify-content: flex-end;
        gap: 10px;
        flex-wrap: wrap;
        margin-top: 14px;
      }

      .dt-image-preview-wrap {
        display: flex;
        align-items: center;
        gap: 12px;
        flex-wrap: wrap;
      }

      .dt-image-preview {
        width: 76px;
        height: 76px;
        border-radius: 14px;
        overflow: hidden;
        border: 1px solid var(--dt-border);
        background: #fff;
        display: flex;
        align-items: center;
        justify-content: center;
      }

      .dt-image-preview img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        display: block;
      }

      .dt-image-preview-empty {
        font-size: .85rem;
        color: var(--dt-muted);
        font-weight: 700;
        padding: 8px;
        text-align: center;
      }

      .dt-hidden {
        display: none !important;
      }

      .dt-toast-wrap {
        position: fixed;
        right: 16px;
        bottom: 16px;
        z-index: 99999;
        display: flex;
        flex-direction: column;
        gap: 10px;
      }

      .dt-toast {
        background: #111827;
        color: #fff;
        padding: 12px 14px;
        border-radius: 14px;
        box-shadow: 0 12px 30px rgba(0,0,0,.25);
        min-width: 220px;
        max-width: 320px;
        font-weight: 700;
      }

      .dt-toast.success { background: #166534; }
      .dt-toast.error   { background: #991b1b; }
      .dt-toast.info    { background: #1f2937; }

      #dotty-app-root.dt-editing .dt-card {
        min-height: 160px;
        grid-template-rows: 70px auto auto;
        padding: 10px;
      }

      #dotty-app-root.dt-editing .dt-grid {
        grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
      }

      #dotty-app-root.dt-editing .dt-card-title {
        font-size: .98rem;
      }

      #dotty-app-root.dt-editing .dt-card-sub {
        font-size: .78rem;
      }

      #dotty-app-root.dt-editing .dt-panel {
        background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%);
      }

      @media (min-width: 860px) {
        .dt-form-grid {
          grid-template-columns: 1fr 1fr;
          align-items: start;
        }
      }

      @media (max-width: 900px) {
        .dt-panel-grid,
        .dt-topbar {
          grid-template-columns: 1fr;
        }

        .dt-settings {
          justify-content: flex-start;
        }
      }

      @media (max-width: 640px) {
        #dotty-app-root {
          margin: 0;
        }

        .dt-shell {
          border-radius: 0;
          border-left: none;
          border-right: none;
        }

        .dt-header {
          padding: 16px 14px;
        }

        .dt-main {
          padding: 12px;
        }

        .dt-grid {
          grid-template-columns: repeat(2, minmax(0, 1fr));
          gap: 12px;
        }

        .dt-card {
          min-height: 154px;
          border-radius: 20px;
          grid-template-rows: 78px auto auto;
        }

        .dt-card-media,
        .dt-card-image-wrap {
          min-height: 70px !important;
          height: 70px !important;
          max-height: 70px !important;
        }

        .dt-card.has-image-only .dt-card-image-wrap {
          height: 100px;
        }

        .dt-card-title {
          font-size: 1rem;
        }

        .dt-sentence {
          padding: 10px 12px 12px;
        }

        .dt-card-emoji img,
        .dt-card-emoji img.emoji,
        .dt-card-emoji svg,
        .dt-card-media img.emoji {
          width: 46px !important;
          height: 46px !important;
          max-width: 46px !important;
          max-height: 46px !important;
        }

        .dt-favourites-grid {
          grid-template-columns: repeat(3, minmax(0, 1fr));
        }
      }

      #dotty-app-root.dt-fullscreen {
        position: fixed;
        inset: 0;
        z-index: 99998;
        background: var(--dt-bg);
        margin: 0;
      }

      #dotty-app-root.dt-fullscreen .dt-shell {
        height: 100vh;
        border-radius: 0;
      }
    </style>

    <script>
      (() => {
        const STORAGE_KEY = 'dotty_tool_data_v421';
        const ACTIVE_CLASS = 'is-active';

        const DEFAULT_DATA = {
          version: 421,
          updatedAt: Date.now(),
          settings: {
            autoSpeak: true,
            speechRate: 0.95,
            highContrast: false,
            largeText: true
          },
          ui: {
            showRoutines: false
          },
          sentence: [],
          routines: [
            { name: 'Morning', emoji: '&#x1f305;', items: [] },
            { name: 'Bedtime', emoji: '&#x1f319;', items: [] },
            { name: 'Toilet', emoji: '&#x1f6bd;', items: [] }
          ],
          categories: [
            {
              name: 'Food',
              emoji: '&#x1f34e;',
              imageId: 0,
              imageUrl: '',
              displayMode: 'emoji_text',
              items: [
                { name: 'Dinner', emoji: '&#x1f37d;', imageId: 0, imageUrl: '', displayMode: 'emoji_text', favourite: false, style: '' },
                { name: 'Drink', emoji: '&#x1f964;', imageId: 0, imageUrl: '', displayMode: 'emoji_text', favourite: false, style: '' },
                { name: 'Milk', emoji: '&#x1f95b;', imageId: 0, imageUrl: '', displayMode: 'emoji_text', favourite: false, style: '' },
                { name: 'Juice', emoji: '&#x1f9c3;', imageId: 0, imageUrl: '', displayMode: 'emoji_text', favourite: false, style: '' }
              ]
            },
            {
              name: 'Every Day',
              emoji: '&#x1f506;',
              imageId: 0,
              imageUrl: '',
              displayMode: 'emoji_text',
              items: [
                { name: 'Yes', emoji: '&#x2705;', imageId: 0, imageUrl: '', displayMode: 'emoji_text', favourite: true, style: 'success' },
                { name: 'No', emoji: '&#x1f6ab;', imageId: 0, imageUrl: '', displayMode: 'emoji_text', favourite: true, style: 'danger' },
                { name: 'Wait', emoji: '&#x1faf8;', imageId: 0, imageUrl: '', displayMode: 'emoji_text', favourite: true, style: 'warning' },
                { name: 'Calm', emoji: '&#x1f92b;', imageId: 0, imageUrl: '', displayMode: 'emoji_text', favourite: true, style: 'primary' },
                { name: 'Home', emoji: '&#x1f3e0;', imageId: 0, imageUrl: '', displayMode: 'emoji_text', favourite: false, style: '' },
                { name: 'Sleep', emoji: '&#x1f6cc;', imageId: 0, imageUrl: '', displayMode: 'emoji_text', favourite: false, style: '' },
                { name: 'Swim', emoji: '&#x1f3ca;&#x200d;&#x2642;', imageId: 0, imageUrl: '', displayMode: 'emoji_text', favourite: false, style: '' },
                { name: 'Toilet', emoji: '&#x1f6bd;', imageId: 0, imageUrl: '', displayMode: 'emoji_text', favourite: true, style: 'warning' }
              ]
            }
          ]
        };

        const DISPLAY_MODES = [
          { value: 'emoji_text', label: 'Emoji + Text' },
          { value: 'image_text', label: 'Image + Text' },
          { value: 'image_only', label: 'Image Only' },
          { value: 'image_emoji_text', label: 'Image + Emoji + Text' }
        ];

        const TEMPLATE_PACKS = [
          {
            key: 'core',
            name: 'Core Needs',
            items: [
              { name: 'Yes', emoji: '&#x2705;', favourite: true, style: 'success' },
              { name: 'No', emoji: '&#x1f6ab;', favourite: true, style: 'danger' },
              { name: 'Wait', emoji: '&#x1faf8;', favourite: true, style: 'warning' },
              { name: 'Help', emoji: '&#x1f198;', favourite: true, style: 'danger' },
              { name: 'More', emoji: '&#x2795;', favourite: true, style: 'primary' },
              { name: 'Finished', emoji: '&#x274c;', favourite: true, style: 'muted' },
              { name: 'Stop', emoji: '&#x1f6d1;', favourite: true, style: 'danger' },
              { name: 'Go', emoji: '&#x25b6;', favourite: true, style: 'success' },
              { name: 'Again', emoji: '&#x1f501;', favourite: true, style: 'primary' },
              { name: 'Please', emoji: '&#x1f64f;', favourite: true, style: 'purple' }
            ]
          },
          {
            key: 'food',
            name: 'Food & Drink',
            items: [
              { name: 'Drink', emoji: '&#x1f964;' },
              { name: 'Water', emoji: '&#x1f4a7;' },
              { name: 'Juice', emoji: '&#x1f9c3;' },
              { name: 'Milk', emoji: '&#x1f95b;' },
              { name: 'Eat', emoji: '&#x1f374;' },
              { name: 'Snack', emoji: '&#x1f36a;' },
              { name: 'Apple', emoji: '&#x1f34e;' },
              { name: 'Banana', emoji: '&#x1f34c;' },
              { name: 'Toast', emoji: '&#x1f35e;' },
              { name: 'Dinner', emoji: '&#x1f37d;' }
            ]
          },
          {
            key: 'home',
            name: 'Home',
            items: [
              { name: 'Home', emoji: '&#x1f3e0;' },
              { name: 'Bed', emoji: '&#x1f6cf;' },
              { name: 'Bath', emoji: '&#x1f6c1;' },
              { name: 'Shower', emoji: '&#x1f6bf;' },
              { name: 'Toilet', emoji: '&#x1f6bd;', favourite: true, style: 'warning' },
              { name: 'Wash Hands', emoji: '&#x1f9fc;' },
              { name: 'Light', emoji: '&#x1f4a1;' },
              { name: 'Door', emoji: '&#x1f6aa;' },
              { name: 'Sofa', emoji: '&#x1f6cb;' },
              { name: 'Television', emoji: '&#x1f4fa;' }
            ]
          },
          {
            key: 'people',
            name: 'People',
            items: [
              { name: 'Mum', emoji: '&#x1f469;' },
              { name: 'Dad', emoji: '&#x1f468;' },
              { name: 'Sister', emoji: '&#x1f467;' },
              { name: 'Teacher', emoji: '&#x1f469;&#x200d;&#x1f3eb;' },
              { name: 'Friend', emoji: '&#x1f642;' },
              { name: 'Doctor', emoji: '&#x1f469;&#x200d;&#x2695;' },
              { name: 'Family', emoji: '&#x1f468;&#x200d;&#x1f469;&#x200d;&#x1f467;' },
              { name: 'Me', emoji: '&#x1f64b;' }
            ]
          },
          {
            key: 'feelings',
            name: 'Feelings',
            items: [
              { name: 'Happy', emoji: '&#x1f642;' },
              { name: 'Sad', emoji: '&#x1f622;' },
              { name: 'Cross', emoji: '&#x1f620;' },
              { name: 'Scared', emoji: '&#x1f628;' },
              { name: 'Tired', emoji: '&#x1f634;' },
              { name: 'Poorly', emoji: '&#x1f912;' },
              { name: 'Calm', emoji: '&#x1f92b;', favourite: true, style: 'primary' },
              { name: 'Excited', emoji: '&#x1f929;' }
            ]
          },
          {
            key: 'places',
            name: 'Places',
            items: [
              { name: 'Park', emoji: '&#x1f3de;' },
              { name: 'School', emoji: '&#x1f3eb;' },
              { name: 'Shop', emoji: '&#x1f3ea;' },
              { name: 'Hospital', emoji: '&#x1f3e5;' },
              { name: 'Beach', emoji: '&#x1f3d6;' },
              { name: 'Car', emoji: '&#x1f697;' },
              { name: 'Garden', emoji: '&#x1f333;' },
              { name: 'Playground', emoji: '&#x1f6dd;' }
            ]
          },
          {
            key: 'actions',
            name: 'Actions',
            items: [
              { name: 'Go', emoji: '&#x25b6;' },
              { name: 'Come', emoji: '&#x1f44b;' },
              { name: 'Sit', emoji: '&#x1fa91;' },
              { name: 'Sleep', emoji: '&#x1f6cc;' },
              { name: 'Eat', emoji: '&#x1f374;' },
              { name: 'Drink', emoji: '&#x1f964;' },
              { name: 'Open', emoji: '&#x1f4c2;' },
              { name: 'Close', emoji: '&#x1f4d5;' },
              { name: 'Play', emoji: '&#x1f389;' },
              { name: 'Read', emoji: '&#x1f4d6;' }
            ]
          },
          {
            key: 'transport',
            name: 'Transport',
            items: [
              { name: 'Car', emoji: '&#x1f697;' },
              { name: 'Bus', emoji: '&#x1f68c;' },
              { name: 'Train', emoji: '&#x1f686;' },
              { name: 'Bike', emoji: '&#x1f6b2;' },
              { name: 'Taxi', emoji: '&#x1f695;' },
              { name: 'Boat', emoji: '&#x26f5;' },
              { name: 'Airplane', emoji: '&#x2708;' },
              { name: 'Walk', emoji: '&#x1f6b6;' }
            ]
          },
          {
            key: 'play',
            name: 'Play',
            items: [
              { name: 'Ball', emoji: '&#x26bd;' },
              { name: 'Toy', emoji: '&#x1f9f8;' },
              { name: 'Music', emoji: '&#x1f3b5;' },
              { name: 'Tablet', emoji: '&#x1f4f1;' },
              { name: 'Television', emoji: '&#x1f4fa;' },
              { name: 'Puzzle', emoji: '&#x1f9e9;' },
              { name: 'Game', emoji: '&#x1f3ae;' },
              { name: 'Slide', emoji: '&#x1f6dd;' }
            ]
          },
          {
            key: 'routine',
            name: 'Routine',
            items: [
              { name: 'Morning', emoji: '&#x1f305;' },
              { name: 'Breakfast', emoji: '&#x1f963;' },
              { name: 'School Time', emoji: '&#x1f3eb;' },
              { name: 'Lunch', emoji: '&#x1f37d;' },
              { name: 'Bath Time', emoji: '&#x1f6c1;' },
              { name: 'Bedtime', emoji: '&#x1f319;' },
              { name: 'Toilet Time', emoji: '&#x1f6bd;' },
              { name: 'Quiet Time', emoji: '&#x1f92b;' }
            ]
          },
          {
            key: 'health',
            name: 'Health',
            items: [
              { name: 'Doctor', emoji: '&#x1f469;&#x200d;&#x2695;' },
              { name: 'Medicine', emoji: '&#x1f48a;' },
              { name: 'Pain', emoji: '&#x1f623;' },
              { name: 'Head', emoji: '&#x1f915;' },
              { name: 'Tummy', emoji: '&#x1f922;' },
              { name: 'Hot', emoji: '&#x1f975;' },
              { name: 'Cold', emoji: '&#x1f976;' },
              { name: 'Rest', emoji: '&#x1f6cc;' }
            ]
          }
        ];

        const EMOJIS = [
          ['&#x1f34e;','Apple'],['&#x1f350;','Pear'],['&#x1f34c;','Banana'],['&#x1f353;','Strawberry'],['&#x1f347;','Grapes'],['&#x1f349;','Watermelon'],
          ['&#x1f34a;','Orange'],['&#x1f95d;','Kiwi'],['&#x1f34d;','Pineapple'],['&#x1f96d;','Mango'],['&#x1f352;','Cherry'],['&#x1f351;','Peach'],
          ['&#x1f955;','Carrot'],['&#x1f966;','Broccoli'],['&#x1f33d;','Corn'],['&#x1f345;','Tomato'],['&#x1f954;','Potato'],['&#x1f952;','Cucumber'],
          ['&#x1f35e;','Bread'],['&#x1f9c0;','Cheese'],['&#x1f95a;','Egg'],['&#x1f357;','Chicken'],['&#x1f35f;','Chips'],['&#x1f36a;','Biscuit'],
          ['&#x1f370;','Cake'],['&#x1f37f;','Popcorn'],['&#x1f37d;','Dinner'],['&#x1f964;','Drink'],['&#x1f95b;','Milk'],['&#x1f9c3;','Juice'],
          ['&#x1f4a7;','Water'],['&#x1f374;','Eat'],['&#x1f96a;','Sandwich'],['&#x1f371;','Sushi'],['&#x1f35d;','Noodles'],['&#x1f963;','Cereal'],
          ['&#x1f3e0;','House'],['&#x1f3e1;','Home'],['&#x1f6cf;','Bed'],['&#x1f6cb;','Sofa'],['&#x1fa91;','Chair'],['&#x1f6aa;','Door'],
          ['&#x1fa9f;','Window'],['&#x1f4a1;','Light'],['&#x1f6bf;','Shower'],['&#x1f6c1;','Bath'],['&#x1f6bd;','Toilet'],['&#x1f9fc;','Soap'],
          ['&#x1f4fa;','Television'],['&#x1f4f1;','Phone'],['&#x1f4bb;','Computer'],['&#x1f697;','Car'],['&#x1f695;','Taxi'],['&#x1f69a;','Truck'],
          ['&#x1f68c;','Bus'],['&#x1f686;','Train'],['&#x1f6b2;','Bicycle'],['&#x1f3cd;','Motorbike'],['&#x2708;','Airplane'],['&#x26f5;','Boat'],
          ['&#x1f333;','Tree'],['&#x1f338;','Flower'],['&#x1f33f;','Grass'],['&#x2600;','Sun'],['&#x2601;','Cloud'],['&#x1f327;','Rain'],
          ['&#x2744;','Snow'],['&#x2b50;','Star'],['&#x1f319;','Moon'],['&#x1f3de;','Park'],['&#x1f3d6;','Beach'],['&#x1f30a;','Sea'],
          ['&#x2705;','Yes'],['&#x1f6ab;','No'],['&#x1f44c;','OK'],['&#x1f92b;','Calm'],['&#x1faf8;','Wait'],['&#x274c;','Finished'],
          ['&#x2764;','Love'],['&#x1f642;','Happy'],['&#x1f622;','Sad'],['&#x1f634;','Sleep'],['&#x1f912;','Poorly'],['&#x1f198;','Help'],
          ['&#x1f64f;','Please'],['&#x1f44f;','Hands'],['&#x1f389;','Fun'],['&#x1f3b5;','Music'],['&#x1f4d6;','Book'],['&#x270f;','Pencil'],
          ['&#x1f9e9;','Puzzle'],['&#x1f3ae;','Game'],['&#x1f9f8;','Teddy'],['&#x26bd;','Ball'],['&#x1f3ca;&#x200d;&#x2642;','Swim'],
          ['&#x1f436;','Dog'],['&#x1f431;','Cat'],['&#x1f430;','Rabbit'],['&#x1f43b;','Bear'],['&#x1f438;','Frog'],['&#x1f422;','Turtle'],
          ['&#x1f981;','Lion'],['&#x1f42f;','Tiger'],['&#x1f435;','Monkey'],['&#x1f418;','Elephant'],['&#x1f992;','Giraffe'],['&#x1f993;','Zebra'],
          ['&#x1f3eb;','School'],['&#x1f3e5;','Hospital'],['&#x1f3ea;','Shop'],['&#x1f6d5;','Temple'],['&#x1f6e3;','Road'],['&#x1f309;','Bridge'],
          ['&#x1f511;','Key'],['&#x1f392;','Bag'],['&#x1f4e6;','Box'],['&#x1f381;','Gift'],['&#x1f4f7;','Camera'],['&#x1f57a;','Blippi'],
          ['&#x1f469;','Mum'],['&#x1f468;','Dad'],['&#x1f467;','Girl'],['&#x1f9d2;','Child'],['&#x1f469;&#x200d;&#x1f3eb;','Teacher']
        ].map(([emoji, name]) => ({ emoji, name }));

        class DottyTool {
          constructor(root) {
            this.root = root;
            this.state = this.normaliseData(this.clone(DEFAULT_DATA));
            this.currentView = 'main';
            this.currentCategoryIndex = null;
            this.editMode = false;
            this.searchTerm = '';
            this.emojiSearchTerm = '';
            this.editingContext = null;
            this.editingRoutineIndex = null;
            this.pendingDelete = null;
            this.saveTimer = null;
            this.parentCode = sessionStorage.getItem('dotty_parent_code') || null;
            this.elements = {};
            this.showRoutines = false;
          }

          init() {
            this.loadLocal();
            this.showRoutines = !!this.state.ui?.showRoutines;
            this.renderShell();
            this.bindGlobalEvents();
            this.applySettingsClasses();
            this.render();
            this.loadRemote();
            this.registerServiceWorker();
          }

          clone(obj) {
            return JSON.parse(JSON.stringify(obj));
          }

          decodePossiblyEscapedUnicode(str) {
            if (typeof str !== 'string') return '';
            if (!/\\u[0-9a-fA-F]{4}/.test(str)) return str;
            try {
              return JSON.parse('"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"');
            } catch (e) {
              return str;
            }
          }

          cleanText(str, fallback = 'Untitled') {
            let value = String(str ?? '');
            value = this.decodePossiblyEscapedUnicode(value);
            value = value.replace(/\s+/g, ' ').trim();
            return value.slice(0, 40) || fallback;
          }

          cleanEmoji(str, fallback = '&#x2753;') {
            let value = String(str ?? '');
            value = this.decodePossiblyEscapedUnicode(value).trim();

            if (/^u[dD][0-9a-fA-F]{3,}/.test(value)) {
              value = '';
            }

            return value.slice(0, 16) || fallback;
          }

          cleanImageUrl(str) {
            return String(str ?? '').trim().slice(0, 500);
          }

          cleanDisplayMode(str) {
            const allowed = ['emoji_text', 'image_text', 'image_only', 'image_emoji_text'];
            return allowed.includes(str) ? str : 'emoji_text';
          }

          clamp(n, min, max) {
            return Math.min(max, Math.max(min, n));
          }

          touchUpdatedAt() {
            this.state.updatedAt = Date.now();
            this.state.ui = this.state.ui || {};
            this.state.ui.showRoutines = !!this.showRoutines;
          }

          normaliseItem(item) {
            return {
              name: this.cleanText(item?.name || 'Item', 'Item'),
              emoji: this.cleanEmoji(item?.emoji || '&#x2753;', '&#x2753;'),
              imageId: Number(item?.imageId || 0) || 0,
              imageUrl: this.cleanImageUrl(item?.imageUrl || ''),
              displayMode: this.cleanDisplayMode(item?.displayMode || 'emoji_text'),
              favourite: !!item?.favourite,
              style: String(item?.style || '').slice(0, 20)
            };
          }

          normaliseCategory(cat) {
            return {
              name: this.cleanText(cat?.name || 'Untitled'),
              emoji: this.cleanEmoji(cat?.emoji || '&#x1f4c1;', '&#x1f4c1;'),
              imageId: Number(cat?.imageId || 0) || 0,
              imageUrl: this.cleanImageUrl(cat?.imageUrl || ''),
              displayMode: this.cleanDisplayMode(cat?.displayMode || 'emoji_text'),
              items: Array.isArray(cat?.items) ? cat.items.slice(0, 120).map(item => this.normaliseItem(item)) : []
            };
          }

          normaliseRoutine(routine) {
            return {
              name: this.cleanText(routine?.name || 'Routine'),
              emoji: this.cleanEmoji(routine?.emoji || '&#x2b50;', '&#x2b50;'),
              items: Array.isArray(routine?.items) ? routine.items.slice(0, 30).map(item => this.normaliseItem(item)) : []
            };
          }

          dedupeFavouriteItems() {
            const seen = new Set();

            this.state.categories.forEach(cat => {
              cat.items.forEach(item => {
                if (!item.favourite) return;

                const key = `${item.name.toLowerCase()}|${item.emoji}|${item.imageUrl}`;
                if (seen.has(key)) {
                  item.favourite = false;
                } else {
                  seen.add(key);
                }
              });
            });
          }

          normaliseData(data) {
            const safe = this.clone(DEFAULT_DATA);
            const input = data && typeof data === 'object' ? data : {};

            safe.version = 421;
            safe.updatedAt = Number(input.updatedAt || safe.updatedAt || Date.now()) || Date.now();
            safe.settings.autoSpeak = input.settings?.autoSpeak === false ? false : true;
            safe.settings.speechRate = this.clamp(Number(input.settings?.speechRate ?? safe.settings.speechRate), 0.6, 1.3);
            safe.settings.highContrast = !!(input.settings && input.settings.highContrast);
            safe.settings.largeText = input.settings?.largeText === false ? false : true;
            safe.ui.showRoutines = !!input.ui?.showRoutines;

            safe.sentence = Array.isArray(input.sentence)
              ? input.sentence.slice(0, 20).map(x => this.normaliseItem(x))
              : [];

            safe.routines = Array.isArray(input.routines)
              ? input.routines.slice(0, 20).map(r => this.normaliseRoutine(r))
              : safe.routines;

            const cats = Array.isArray(input.categories) ? input.categories : safe.categories;
            safe.categories = cats.slice(0, 30).map(cat => this.normaliseCategory(cat));

            this.state = safe;
            this.dedupeFavouriteItems();

            return this.state;
          }

          loadLocal() {
            try {
              const raw = localStorage.getItem(STORAGE_KEY);
              if (!raw) return;
              this.state = this.normaliseData(JSON.parse(raw));
            } catch (e) {
              console.warn('Dotty local load failed', e);
            }
          }

          async loadRemote() {
            if (!window.DottyToolConfig?.isLoggedIn) return;

            try {
              const body = new URLSearchParams({
                action: 'load_dotty_data',
                nonce: window.DottyToolConfig.nonce
              });

              const res = await fetch(window.DottyToolConfig.ajaxUrl, {
                method: 'POST',
                credentials: 'same-origin',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
                body
              });

              const json = await res.json();

              if (json?.success && json?.data) {
                const remoteState = this.normaliseData(json.data);
                const localUpdated = Number(this.state.updatedAt || 0);
                const remoteUpdated = Number(remoteState.updatedAt || 0);

                if (remoteUpdated > localUpdated) {
                  this.state = remoteState;
                  this.showRoutines = !!this.state.ui?.showRoutines;
                  this.applySettingsClasses();
                  this.render();
                  this.toast('Cloud data loaded', 'success');
                }
              }
            } catch (e) {
              console.warn('Dotty remote load failed', e);
            }
          }

          saveLocal() {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(this.state));
          }

          queueSave() {
            this.dedupeFavouriteItems();
            this.touchUpdatedAt();
            this.saveLocal();
            clearTimeout(this.saveTimer);
            this.saveTimer = setTimeout(() => this.saveRemote(), 450);
          }

          async saveRemote() {
            if (!window.DottyToolConfig?.isLoggedIn) return;

            try {
              const body = new URLSearchParams({
                action: 'save_dotty_data',
                nonce: window.DottyToolConfig.nonce,
                data: JSON.stringify(this.state)
              });

              const res = await fetch(window.DottyToolConfig.ajaxUrl, {
                method: 'POST',
                credentials: 'same-origin',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
                body
              });

              const json = await res.json();
              if (!json?.success) {
                console.warn('Dotty remote save failed', json);
              }
            } catch (e) {
              console.warn('Dotty remote save failed', e);
            }
          }

          registerServiceWorker() {
            if (!('serviceWorker' in navigator)) return;
            if (!window.DottyToolConfig?.serviceWorkerUrl) return;
            navigator.serviceWorker.register(window.DottyToolConfig.serviceWorkerUrl).catch(() => {});
          }

          renderShell() {
            this.root.innerHTML = `
              <div class="dt-shell" aria-live="polite">
                <div class="dt-header">
                  <div class="dt-header-top">
                    <div class="dt-title-wrap">
                      <h1>Emoji & Image PECs Tool</h1>
                      <p>Offline-friendly communication and learning support for Dotty</p>
                    </div>
                    <div class="dt-toolbar">
                      <button class="dt-btn dt-btn-ghost" id="dtHomeBtn" type="button">Home</button>
                      <button class="dt-btn dt-btn-ghost" id="dtEditBtn" type="button">Edit</button>
                      <button class="dt-btn dt-btn-ghost" id="dtFullBtn" type="button">Full Screen</button>
                      <button class="dt-btn dt-btn-ghost dt-hidden" id="dtExportBtn" type="button">Save JSON</button>
                      <button class="dt-btn dt-btn-ghost dt-hidden" id="dtImportBtn" type="button">Load JSON</button>
                    </div>
                  </div>
                </div>

                <div class="dt-main">
                  <div class="dt-topbar">
                    <div class="dt-breadcrumbs" id="dtBreadcrumbs"></div>
                    <div class="dt-settings">
                      <label class="dt-switch"><input type="checkbox" id="dtAutoSpeak"> Auto Speak</label>
                      <label class="dt-switch"><input type="checkbox" id="dtLargeText"> Large Text</label>
                      <label class="dt-switch"><input type="checkbox" id="dtHighContrast"> High Contrast</label>
                    </div>
                  </div>

                  <div class="dt-panel">
                    <div class="dt-panel-grid">
                      <div>
                        <div class="dt-form-group">
                          <label for="dtSearch">Search tiles</label>
                          <div class="dt-search-wrap">
                            <input class="dt-input" id="dtSearch" type="search" placeholder="Search categories or items">
                            <button class="dt-btn dt-btn-warning" id="dtBackBtn" type="button">Back</button>
                          </div>
                        </div>
                      </div>
                      <div>
                        <div class="dt-form-group">
                          <label for="dtSpeechRate">Speech speed</label>
                          <input class="dt-range" id="dtSpeechRate" type="range" min="0.6" max="1.3" step="0.05">
                          <div class="dt-form-help" id="dtSpeechRateValue"></div>
                        </div>
                      </div>
                    </div>
                  </div>

                  <div id="dtView"></div>
                </div>

                <div class="dt-sentence">
                  <div class="dt-sentence-inner">
                    <div class="dt-sentence-top">
                      <h3>Sentence Builder</h3>
                      <div class="dt-sentence-actions">
                        <button class="dt-btn dt-btn-primary" id="dtSpeakSentenceBtn" type="button">Speak</button>
                        <button class="dt-btn dt-btn-warning" id="dtUndoSentenceBtn" type="button">Backspace</button>
                        <button class="dt-btn dt-btn-danger" id="dtClearSentenceBtn" type="button">Clear</button>
                        <button class="dt-btn dt-btn-purple" id="dtSaveRoutineBtn" type="button">Save as Routine</button>
                      </div>
                    </div>
                    <div id="dtSentenceStrip"></div>
                  </div>
                </div>
              </div>

              <dialog class="dt-modal" id="dtTileModal">
                <div class="dt-modal-card">
                  <div class="dt-modal-header">
                    <h3 id="dtTileModalTitle">Edit Tile</h3>
                  </div>
                  <div class="dt-modal-body">
                    <div class="dt-form-grid">
                      <div>
                        <div class="dt-form-group">
                          <label for="dtTileName">Name</label>
                          <input class="dt-input" id="dtTileName" type="text" maxlength="40" placeholder="Enter a name">
                        </div>

                        <div class="dt-form-group">
                          <label for="dtTileDisplayMode">Display Mode</label>
                          <select class="dt-select" id="dtTileDisplayMode">
                            ${DISPLAY_MODES.map(mode => `<option value="${mode.value}">${mode.label}</option>`).join('')}
                          </select>
                        </div>

                        <div class="dt-form-group" id="dtFavouriteWrap">
                          <label class="dt-switch"><input type="checkbox" id="dtTileFavourite"> Favourite</label>
                        </div>

                        <div class="dt-form-group">
                          <label>Image</label>
                          <div class="dt-image-preview-wrap">
                            <div class="dt-image-preview" id="dtImagePreview">
                              <div class="dt-image-preview-empty">No image</div>
                            </div>
                            <div style="display:flex;gap:10px;flex-wrap:wrap;">
                              <button class="dt-btn dt-btn-primary" id="dtPickImageBtn" type="button">Choose Image</button>
                              <button class="dt-btn dt-btn-success" id="dtCameraImageBtn" type="button">Use Camera</button>
                              <button class="dt-btn dt-btn-danger" id="dtRemoveImageBtn" type="button">Remove Image</button>
                            </div>
                          </div>
                          <div class="dt-form-help">Great for real cups, beds, snacks, toys, people, and routines.</div>
                        </div>

                        <div class="dt-form-group">
                          <label for="dtEmojiSearch">Find an emoji</label>
                          <input class="dt-input dt-emoji-search" id="dtEmojiSearch" type="search" placeholder="Search emoji names">
                        </div>

                        <div class="dt-form-group">
                          <label for="dtTileEmoji">Or type your own emoji</label>
                          <input class="dt-input" id="dtTileEmoji" type="text" maxlength="16" placeholder="&#x1f600;">
                        </div>
                      </div>

                      <div>
                        <div class="dt-form-group">
                          <label>Choose emoji</label>
                          <div class="dt-emoji-grid" id="dtEmojiGrid"></div>
                        </div>
                      </div>
                    </div>

                    <div class="dt-modal-actions">
                      <button class="dt-btn dt-btn-warning" id="dtTileCancelBtn" type="button">Cancel</button>
                      <button class="dt-btn dt-btn-success" id="dtTileSaveBtn" type="button">Save</button>
                    </div>
                  </div>
                </div>
              </dialog>

              <dialog class="dt-modal" id="dtRoutineModal">
                <div class="dt-modal-card">
                  <div class="dt-modal-header">
                    <h3>Save Sentence as Routine</h3>
                  </div>
                  <div class="dt-modal-body">
                    <div class="dt-form-group">
                      <label for="dtRoutineName">Routine name</label>
                      <input class="dt-input" id="dtRoutineName" type="text" maxlength="40" placeholder="e.g. Bedtime">
                    </div>
                    <div class="dt-form-group">
                      <label for="dtRoutineEmoji">Routine emoji</label>
                      <input class="dt-input" id="dtRoutineEmoji" type="text" maxlength="16" placeholder="&#x1f319;">
                    </div>
                    <div class="dt-form-group">
                      <label>Preview</label>
                      <div id="dtRoutinePreview" class="dt-empty"></div>
                    </div>
                    <div class="dt-modal-actions">
                      <button class="dt-btn dt-btn-warning" id="dtRoutineCancelBtn" type="button">Cancel</button>
                      <button class="dt-btn dt-btn-success" id="dtRoutineSaveBtn" type="button">Save Routine</button>
                    </div>
                  </div>
                </div>
              </dialog>

              <dialog class="dt-modal" id="dtRoutineEditModal">
                <div class="dt-modal-card">
                  <div class="dt-modal-header">
                    <h3 id="dtRoutineEditTitle">Edit Routine</h3>
                  </div>
                  <div class="dt-modal-body">
                    <div class="dt-form-group">
                      <label for="dtRoutineEditName">Routine name</label>
                      <input class="dt-input" id="dtRoutineEditName" type="text" maxlength="40" placeholder="Routine name">
                    </div>

                    <div class="dt-form-group">
                      <label for="dtRoutineEditEmoji">Routine emoji</label>
                      <input class="dt-input" id="dtRoutineEditEmoji" type="text" maxlength="16" placeholder="&#x2b50;">
                    </div>

                    <div class="dt-form-group">
                      <label>Routine items</label>
                      <div id="dtRoutineEditPreview" class="dt-empty"></div>
                    </div>

                    <div class="dt-modal-actions">
                      <button class="dt-btn dt-btn-warning" id="dtRoutineEditCancelBtn" type="button">Cancel</button>
                      <button class="dt-btn dt-btn-purple" id="dtRoutineOverwriteBtn" type="button">Replace with Current Sentence</button>
                      <button class="dt-btn dt-btn-success" id="dtRoutineEditSaveBtn" type="button">Save Changes</button>
                    </div>
                  </div>
                </div>
              </dialog>

              <dialog class="dt-modal" id="dtConfirmModal">
                <div class="dt-modal-card">
                  <div class="dt-modal-header">
                    <h3>Are you sure?</h3>
                  </div>
                  <div class="dt-modal-body">
                    <p id="dtConfirmText" style="margin:0 0 14px;font-weight:700;"></p>
                    <div class="dt-modal-actions">
                      <button class="dt-btn dt-btn-warning" id="dtConfirmCancelBtn" type="button">Cancel</button>
                      <button class="dt-btn dt-btn-danger" id="dtConfirmOkBtn" type="button">Delete</button>
                    </div>
                  </div>
                </div>
              </dialog>

              <input type="file" id="dtImportFile" accept="application/json,.json" class="dt-hidden">
              <input type="file" id="dtCameraFile" accept="image/*" capture="environment" class="dt-hidden">

              <div class="dt-toast-wrap" id="dtToastWrap" aria-live="polite" aria-atomic="true"></div>
            `;

            this.elements = {
              homeBtn: this.root.querySelector('#dtHomeBtn'),
              editBtn: this.root.querySelector('#dtEditBtn'),
              fullBtn: this.root.querySelector('#dtFullBtn'),
              exportBtn: this.root.querySelector('#dtExportBtn'),
              importBtn: this.root.querySelector('#dtImportBtn'),
              backBtn: this.root.querySelector('#dtBackBtn'),
              breadcrumbs: this.root.querySelector('#dtBreadcrumbs'),
              search: this.root.querySelector('#dtSearch'),
              speechRate: this.root.querySelector('#dtSpeechRate'),
              speechRateValue: this.root.querySelector('#dtSpeechRateValue'),
              autoSpeak: this.root.querySelector('#dtAutoSpeak'),
              largeText: this.root.querySelector('#dtLargeText'),
              highContrast: this.root.querySelector('#dtHighContrast'),
              view: this.root.querySelector('#dtView'),
              sentenceStrip: this.root.querySelector('#dtSentenceStrip'),
              speakSentenceBtn: this.root.querySelector('#dtSpeakSentenceBtn'),
              undoSentenceBtn: this.root.querySelector('#dtUndoSentenceBtn'),
              clearSentenceBtn: this.root.querySelector('#dtClearSentenceBtn'),
              saveRoutineBtn: this.root.querySelector('#dtSaveRoutineBtn'),

              tileModal: this.root.querySelector('#dtTileModal'),
              tileModalTitle: this.root.querySelector('#dtTileModalTitle'),
              tileName: this.root.querySelector('#dtTileName'),
              tileEmoji: this.root.querySelector('#dtTileEmoji'),
              tileDisplayMode: this.root.querySelector('#dtTileDisplayMode'),
              tileFavourite: this.root.querySelector('#dtTileFavourite'),
              favouriteWrap: this.root.querySelector('#dtFavouriteWrap'),
              emojiSearch: this.root.querySelector('#dtEmojiSearch'),
              emojiGrid: this.root.querySelector('#dtEmojiGrid'),
              imagePreview: this.root.querySelector('#dtImagePreview'),
              pickImageBtn: this.root.querySelector('#dtPickImageBtn'),
              cameraImageBtn: this.root.querySelector('#dtCameraImageBtn'),
              removeImageBtn: this.root.querySelector('#dtRemoveImageBtn'),
              tileCancelBtn: this.root.querySelector('#dtTileCancelBtn'),
              tileSaveBtn: this.root.querySelector('#dtTileSaveBtn'),

              routineModal: this.root.querySelector('#dtRoutineModal'),
              routineName: this.root.querySelector('#dtRoutineName'),
              routineEmoji: this.root.querySelector('#dtRoutineEmoji'),
              routinePreview: this.root.querySelector('#dtRoutinePreview'),
              routineCancelBtn: this.root.querySelector('#dtRoutineCancelBtn'),
              routineSaveBtn: this.root.querySelector('#dtRoutineSaveBtn'),

              routineEditModal: this.root.querySelector('#dtRoutineEditModal'),
              routineEditTitle: this.root.querySelector('#dtRoutineEditTitle'),
              routineEditName: this.root.querySelector('#dtRoutineEditName'),
              routineEditEmoji: this.root.querySelector('#dtRoutineEditEmoji'),
              routineEditPreview: this.root.querySelector('#dtRoutineEditPreview'),
              routineEditCancelBtn: this.root.querySelector('#dtRoutineEditCancelBtn'),
              routineOverwriteBtn: this.root.querySelector('#dtRoutineOverwriteBtn'),
              routineEditSaveBtn: this.root.querySelector('#dtRoutineEditSaveBtn'),

              confirmModal: this.root.querySelector('#dtConfirmModal'),
              confirmText: this.root.querySelector('#dtConfirmText'),
              confirmCancelBtn: this.root.querySelector('#dtConfirmCancelBtn'),
              confirmOkBtn: this.root.querySelector('#dtConfirmOkBtn'),

              importFile: this.root.querySelector('#dtImportFile'),
              cameraFile: this.root.querySelector('#dtCameraFile'),
              toastWrap: this.root.querySelector('#dtToastWrap')
            };
          }

          bindGlobalEvents() {
            this.elements.homeBtn.addEventListener('click', () => this.goHome());
            this.elements.editBtn.addEventListener('click', () => this.toggleEditMode());
            this.elements.fullBtn.addEventListener('click', () => this.toggleFullscreen());
            this.elements.exportBtn.addEventListener('click', () => this.exportJsonFile());
            this.elements.importBtn.addEventListener('click', () => this.elements.importFile.click());
            this.elements.importFile.addEventListener('change', (e) => this.importJsonFile(e));
            this.elements.backBtn.addEventListener('click', () => this.goBack());

            this.elements.search.addEventListener('input', (e) => {
              this.searchTerm = e.target.value.trim().toLowerCase();
              this.render();
            });

            this.elements.autoSpeak.addEventListener('change', (e) => {
              this.state.settings.autoSpeak = !!e.target.checked;
              this.queueSave();
            });

            this.elements.largeText.addEventListener('change', (e) => {
              this.state.settings.largeText = !!e.target.checked;
              this.applySettingsClasses();
              this.queueSave();
            });

            this.elements.highContrast.addEventListener('change', (e) => {
              this.state.settings.highContrast = !!e.target.checked;
              this.applySettingsClasses();
              this.queueSave();
            });

            this.elements.speechRate.addEventListener('input', (e) => {
              this.state.settings.speechRate = this.clamp(Number(e.target.value), 0.6, 1.3);
              this.updateSpeechRateText();
              this.queueSave();
            });

            this.elements.speakSentenceBtn.addEventListener('click', () => this.speakSentence());
            this.elements.undoSentenceBtn.addEventListener('click', () => this.undoSentence());
            this.elements.clearSentenceBtn.addEventListener('click', () => this.clearSentence());
            this.elements.saveRoutineBtn.addEventListener('click', () => this.openRoutineModal());

            this.elements.tileCancelBtn.addEventListener('click', () => this.closeTileModal());
            this.elements.tileSaveBtn.addEventListener('click', () => this.saveTileModal());

            this.elements.emojiSearch.addEventListener('input', (e) => {
              this.emojiSearchTerm = e.target.value.trim().toLowerCase();
              this.renderEmojiPicker();
            });

            this.elements.tileEmoji.addEventListener('input', () => this.renderEmojiPicker());
            this.elements.pickImageBtn.addEventListener('click', () => this.pickImage());
            this.elements.cameraImageBtn.addEventListener('click', () => this.pickCameraImage());
            this.elements.cameraFile.addEventListener('change', (e) => this.handleCameraUpload(e));
            this.elements.removeImageBtn.addEventListener('click', () => this.removeModalImage());

            this.elements.routineCancelBtn.addEventListener('click', () => this.elements.routineModal.close());
            this.elements.routineSaveBtn.addEventListener('click', () => this.saveRoutine());

            this.elements.routineEditCancelBtn.addEventListener('click', () => this.closeRoutineEditModal());
            this.elements.routineEditSaveBtn.addEventListener('click', () => this.saveRoutineEdit());
            this.elements.routineOverwriteBtn.addEventListener('click', () => this.overwriteRoutineFromSentence());

            this.elements.confirmCancelBtn.addEventListener('click', () => {
              this.pendingDelete = null;
              this.elements.confirmModal.close();
            });
            this.elements.confirmOkBtn.addEventListener('click', () => this.confirmDelete());

            this.elements.view.addEventListener('click', (e) => this.handleViewClick(e));

            this.elements.sentenceStrip.addEventListener('click', (e) => {
              const removeBtn = e.target.closest('[data-remove-sentence-index]');
              if (removeBtn) {
                const index = Number(removeBtn.dataset.removeSentenceIndex);
                this.removeSentenceToken(index);
              }
            });

            [this.elements.tileModal, this.elements.routineModal, this.elements.routineEditModal, this.elements.confirmModal].forEach(dialog => {
              if (!dialog) return;
              dialog.addEventListener('click', (e) => {
                const rect = dialog.getBoundingClientRect();
                const clickedInDialog =
                  e.clientX >= rect.left &&
                  e.clientX <= rect.right &&
                  e.clientY >= rect.top &&
                  e.clientY <= rect.bottom;
                if (!clickedInDialog) {
                  dialog.close();
                }
              });
            });

            document.addEventListener('fullscreenchange', () => {
              if (!document.fullscreenElement) {
                this.root.classList.remove('dt-fullscreen');
              }
            });
          }

          applySettingsClasses() {
            this.root.classList.toggle('dt-high-contrast', !!this.state.settings.highContrast);
            this.root.classList.toggle('dt-large-text', !!this.state.settings.largeText);

            if (this.elements.autoSpeak) this.elements.autoSpeak.checked = !!this.state.settings.autoSpeak;
            if (this.elements.largeText) this.elements.largeText.checked = !!this.state.settings.largeText;
            if (this.elements.highContrast) this.elements.highContrast.checked = !!this.state.settings.highContrast;
            if (this.elements.speechRate) this.elements.speechRate.value = String(this.state.settings.speechRate);
            this.updateSpeechRateText();
          }

          updateSpeechRateText() {
            if (this.elements.speechRateValue) {
              this.elements.speechRateValue.textContent = `Current speed: ${Number(this.state.settings.speechRate).toFixed(2)}x`;
            }
          }

          toast(message, type = 'info') {
            const item = document.createElement('div');
            item.className = `dt-toast ${type}`;
            item.textContent = message;
            this.elements.toastWrap.appendChild(item);
            setTimeout(() => item.remove(), 2600);
          }

          getFavouriteItems() {
            const out = [];
            const seen = new Set();

            this.state.categories.forEach((cat, catIndex) => {
              cat.items.forEach((item, itemIndex) => {
                if (!item.favourite) return;
                const key = `${item.name.toLowerCase()}|${item.emoji}|${item.imageUrl}`;
                if (seen.has(key)) return;
                seen.add(key);
                out.push({ item, catIndex, itemIndex });
              });
            });

            return out;
          }

          goHome() {
            this.currentView = 'main';
            this.currentCategoryIndex = null;
            this.searchTerm = '';
            if (this.elements.search) {
              this.elements.search.value = '';
            }
            this.render();
          }

          goBack() {
            this.goHome();
          }

          render() {
            this.root.classList.toggle('dt-editing', this.editMode);
            this.renderBreadcrumbs();
            this.renderView();
            this.renderSentence();
            this.elements.exportBtn.classList.toggle('dt-hidden', !this.editMode);
            this.elements.importBtn.classList.toggle('dt-hidden', !this.editMode);
            this.elements.backBtn.classList.toggle('dt-hidden', this.currentView === 'main');
            this.elements.editBtn.textContent = this.editMode ? 'Edit Mode On' : 'Edit';
          }

          renderBreadcrumbs() {
            let html = `<button class="dt-chip" type="button" id="dtBreadcrumbHome">&#x1f3e0; Home</button>`;

            if (this.currentView === 'category' && this.state.categories[this.currentCategoryIndex]) {
              const cat = this.state.categories[this.currentCategoryIndex];
              html += `<span>›</span><span class="dt-chip">${this.escape(cat.emoji)} ${this.escape(cat.name)}</span>`;
            }

            this.elements.breadcrumbs.innerHTML = html;

            const homeChip = this.root.querySelector('#dtBreadcrumbHome');
            if (homeChip) {
              homeChip.addEventListener('click', () => this.goHome());
            }
          }

          renderView() {
            if (this.currentView === 'main') {
              this.renderMain();
            } else {
              this.renderCategory();
            }
          }

          renderFavouriteCard(entry) {
            const item = entry.item;
            return `
              <button
                class="dt-fav-card"
                type="button"
                data-card-type="item"
                data-index="${entry.itemIndex}"
                data-category-index="${entry.catIndex}"
              >
                ${
                  item.imageUrl
                    ? `<div class="img-wrap"><img decoding="async" src="${this.escape(item.imageUrl)}" alt="${this.escape(item.name)}"></div>`
                    : `<div class="emoji">${this.escape(item.emoji)}</div>`
                }
                <div class="label">${this.escape(item.name)}</div>
              </button>
            `;
          }

          renderMain() {
            const filtered = this.state.categories.filter(cat => {
              if (!this.searchTerm) return true;
              if (cat.name.toLowerCase().includes(this.searchTerm)) return true;
              return cat.items.some(item => item.name.toLowerCase().includes(this.searchTerm));
            });

            const favourites = this.getFavouriteItems();

            let html = '';

            html += `
              <div class="dt-panel">
                <div class="dt-title-row">
                  <h2>Favourites</h2>
                  <div class="dt-form-help">${favourites.length} quick button${favourites.length === 1 ? '' : 's'}</div>
                </div>
            `;

            if (!favourites.length) {
              html += `<div class="dt-empty">Mark favourite items with the star button in edit mode.</div>`;
            } else {
              html += `<div class="dt-favourites-grid">`;
              favourites.forEach(entry => {
                html += this.renderFavouriteCard(entry);
              });
              html += `</div>`;
            }

            html += `</div>`;

            html += `
              <div class="dt-panel">
                <div class="dt-title-row">
                  <h2>Quick Access</h2>
                  <div class="dt-form-help">Fast actions for routines and everyday use</div>
                </div>
                <div style="display:flex;gap:10px;flex-wrap:wrap;">
                  <button class="dt-btn dt-btn-primary" type="button" data-toggle-routines="1">
                    ${this.showRoutines ? 'Hide Routines' : 'Show Routines'}
                  </button>
                </div>
              </div>
            `;

            if (this.showRoutines) {
              html += `
                <div class="dt-panel">
                  <div class="dt-title-row">
                    <h2>Routines</h2>
                    <div class="dt-form-help">${this.state.routines.length} saved</div>
                  </div>
              `;

              if (!this.state.routines.length) {
                html += `<div class="dt-empty">Build a sentence, then save it as a routine.</div>`;
              } else {
                html += `<div class="dt-routine-list">`;
                this.state.routines.forEach((routine, index) => {
                  html += `
                    <div class="dt-routine-card">
                      <button class="dt-routine-card-main" type="button" data-routine-index="${index}">
                        <span class="e">${this.escape(routine.emoji)}</span>
                        <span class="t">${this.escape(routine.name)}</span>
                        <span class="s">${routine.items.length} step${routine.items.length === 1 ? '' : 's'}</span>
                      </button>

                      ${this.editMode ? `
                        <div class="dt-routine-actions">
                          <button class="dt-mini-btn move" type="button" data-routine-move="-1" data-routine-index="${index}">←</button>
                          <button class="dt-mini-btn move" type="button" data-routine-move="1" data-routine-index="${index}">→</button>
                          <button class="dt-mini-btn edit" type="button" data-edit-routine="${index}">Edit</button>
                          <button class="dt-mini-btn copy" type="button" data-duplicate-routine="${index}">Copy</button>
                          <button class="dt-mini-btn delete" type="button" data-delete-routine="${index}">Delete</button>
                        </div>
                      ` : ''}
                    </div>
                  `;
                });
                html += `</div>`;
              }

              html += `</div>`;
            }

            html += `
              <div class="dt-panel">
                <div class="dt-title-row">
                  <h2>Quick Add Templates</h2>
                  <div class="dt-form-help">${this.editMode ? 'Tap a pack to add ready-made buttons' : 'Turn on edit mode to add packs'}</div>
                </div>
                <div class="dt-template-list">
                  ${TEMPLATE_PACKS.map((pack, idx) => `
                    <button class="dt-template-chip ${!this.editMode ? 'is-disabled' : ''}" type="button" data-template-pack="${idx}">
                      ${this.escape(pack.name)} (${pack.items.length})
                    </button>
                  `).join('')}
                </div>
              </div>
            `;

            html += `
              <div class="dt-panel">
                <div class="dt-title-row">
                  <h2>Categories</h2>
                  <div class="dt-form-help">${filtered.length} shown</div>
                </div>
            `;

            html += `<div class="dt-grid">`;

            if (!filtered.length) {
              html += `<div class="dt-empty" style="grid-column:1/-1;">No categories match your search.</div>`;
            } else {
              filtered.forEach(cat => {
                const actualIndex = this.state.categories.indexOf(cat);
                html += this.cardHtml({
                  type: 'category',
                  index: actualIndex,
                  data: cat,
                  subtitle: `${cat.items.length} item${cat.items.length === 1 ? '' : 's'}`
                });
              });
            }

            if (this.editMode) {
              html += this.addCardHtml('category');
            }

            html += `</div>`;
            html += `</div>`;

            this.elements.view.innerHTML = html;
          }

          renderCategory() {
            const category = this.state.categories[this.currentCategoryIndex];
            if (!category) {
              this.currentView = 'main';
              this.currentCategoryIndex = null;
              return this.render();
            }

            const filteredItems = category.items.filter(item => {
              if (!this.searchTerm) return true;
              return item.name.toLowerCase().includes(this.searchTerm);
            });

            let html = `
              <div class="dt-panel">
                <div class="dt-title-row">
                  <h2>${this.escape(category.emoji)} ${this.escape(category.name)}</h2>
                  <div class="dt-form-help">${filteredItems.length} shown</div>
                </div>
            `;

            html += `<div class="dt-grid">`;

            if (!filteredItems.length) {
              html += `<div class="dt-empty" style="grid-column:1/-1;">No items match your search in this category.</div>`;
            } else {
              filteredItems.forEach(item => {
                const actualIndex = category.items.indexOf(item);
                html += this.cardHtml({
                  type: 'item',
                  index: actualIndex,
                  categoryIndex: this.currentCategoryIndex,
                  data: item,
                  subtitle: 'Tap to say and add'
                });
              });
            }

            if (this.editMode) {
              html += this.addCardHtml('item', this.currentCategoryIndex);
            }

            html += `</div>`;
            html += `</div>`;
            this.elements.view.innerHTML = html;
          }

          renderSentence() {
            const sentence = this.state.sentence;
            if (!sentence.length) {
              this.elements.sentenceStrip.innerHTML = `<div class="dt-empty">Tap tiles to build a sentence.</div>`;
              return;
            }

            this.elements.sentenceStrip.innerHTML = `
              <div class="dt-sentence-strip">
                ${sentence.map((item, i) => `
                  <div class="dt-sentence-token" data-sentence-index="${i}">
                    <button class="dt-sentence-token-remove" type="button" data-remove-sentence-index="${i}" aria-label="Remove ${this.escape(item.name)}">×</button>
                    ${item.imageUrl ? `<div class="img-wrap"><img decoding="async" src="${this.escape(item.imageUrl)}" alt="${this.escape(item.name)}"></div>` : `<span class="emoji">${this.escape(item.emoji)}</span>`}
                    <span>${this.escape(item.name)}</span>
                  </div>
                `).join('')}
              </div>
            `;
          }

          cardHtml({ type, index, categoryIndex = '', data, subtitle }) {
            const display = this.getDisplayParts(data);
            const classes = ['dt-card'];

            if (data.style) {
              classes.push(`style-${this.escape(data.style)}`);
            }

            if (display.showImage && !display.showText && !display.showEmojiBadge && !display.showEmojiMain) {
              classes.push('has-image-only');
            }

            return `
              <button
                class="${classes.join(' ')}"
                type="button"
                data-card-type="${this.escape(type)}"
                data-index="${index}"
                ${type === 'item' ? `data-category-index="${categoryIndex}"` : ''}
              >
                ${this.editMode ? this.cardActionsHtml(type, index, categoryIndex, data) : ''}
                <div class="dt-card-media">
                  ${display.mediaHtml}
                  ${display.showEmojiBadge ? `<div class="dt-card-emoji-badge">${this.escape(data.emoji)}</div>` : ''}
                </div>
                ${display.showText ? `<div class="dt-card-title">${this.escape(type === 'category' ? data.name.toUpperCase() : data.name)}</div>` : `<div></div>`}
                ${display.showText && subtitle ? `<div class="dt-card-sub">${this.escape(subtitle)}</div>` : `<div></div>`}
              </button>
            `;
          }

          getDisplayParts(data) {
            const hasImage = !!data.imageUrl;
            let mode = data.displayMode || 'emoji_text';

            if (hasImage && mode === 'emoji_text') {
              mode = 'image_text';
            }

            let showImage = false;
            let showEmojiMain = false;
            let showEmojiBadge = false;
            let showText = true;

            if (mode === 'emoji_text') {
              showEmojiMain = true;
              showText = true;
            } else if (mode === 'image_text') {
              showImage = hasImage;
              showText = true;
              if (!hasImage) showEmojiMain = true;
            } else if (mode === 'image_only') {
              showImage = hasImage;
              showText = false;
              if (!hasImage) {
                showEmojiMain = true;
                showText = true;
              }
            } else if (mode === 'image_emoji_text') {
              showImage = hasImage;
              showText = true;
              if (hasImage) {
                showEmojiBadge = true;
              } else {
                showEmojiMain = true;
              }
            }

            let mediaHtml = '';

            if (showImage && hasImage) {
              mediaHtml += `
                <div class="dt-card-image-wrap">
                  <img decoding="async" class="dt-card-image" src="${this.escape(data.imageUrl)}" alt="${this.escape(data.name)}">
                </div>
              `;
            } else if (showEmojiMain || (!hasImage && !showImage)) {
              mediaHtml += `<div class="dt-card-emoji">${this.escape(data.emoji || '&#x2753;')}</div>`;
            }

            return { mediaHtml, showImage, showEmojiMain, showEmojiBadge, showText };
          }

          addCardHtml(type, categoryIndex = '') {
            return `
              <button
                class="dt-card is-add"
                type="button"
                data-action="add"
                data-add-type="${this.escape(type)}"
                ${type === 'item' ? `data-category-index="${categoryIndex}"` : ''}
              >
                <div class="dt-card-media">
                  <div class="dt-card-emoji">&#x2795;</div>
                </div>
                <div class="dt-card-title">${type === 'category' ? 'ADD CATEGORY' : 'ADD ITEM'}</div>
                <div class="dt-card-sub">Create a new tile</div>
              </button>
            `;
          }

          cardActionsHtml(type, index, categoryIndex, data) {
            return `
              <div class="dt-card-actions">
                <div class="dt-card-actions-left">
                  <button class="dt-action-btn dt-action-move" type="button" data-action="move-left" data-card-type="${this.escape(type)}" data-index="${index}" ${type === 'item' ? `data-category-index="${categoryIndex}"` : ''}>←</button>
                  <button class="dt-action-btn dt-action-move" type="button" data-action="move-right" data-card-type="${this.escape(type)}" data-index="${index}" ${type === 'item' ? `data-category-index="${categoryIndex}"` : ''}>→</button>
                </div>
                <div class="dt-card-actions-right">
                  ${type === 'item' ? `<button class="dt-action-btn dt-action-star" type="button" data-action="toggle-favourite" data-card-type="${this.escape(type)}" data-index="${index}" data-category-index="${categoryIndex}">${data.favourite ? '★' : '☆'}</button>` : ''}
                  <button class="dt-action-btn dt-action-edit" type="button" data-action="edit" data-card-type="${this.escape(type)}" data-index="${index}" ${type === 'item' ? `data-category-index="${categoryIndex}"` : ''}>Edit</button>
                  <button class="dt-action-btn dt-action-delete" type="button" data-action="delete" data-card-type="${this.escape(type)}" data-index="${index}" ${type === 'item' ? `data-category-index="${categoryIndex}"` : ''}>✕</button>
                </div>
              </div>
            `;
          }

          handleViewClick(e) {
            const toggleRoutinesBtn = e.target.closest('[data-toggle-routines]');
            if (toggleRoutinesBtn) {
              e.preventDefault();
              e.stopPropagation();
              this.showRoutines = !this.showRoutines;
              this.queueSave();
              this.render();
              return;
            }

            const templateBtn = e.target.closest('[data-template-pack]');
            if (templateBtn) {
              e.preventDefault();
              e.stopPropagation();
              this.applyTemplatePack(Number(templateBtn.dataset.templatePack));
              return;
            }

            const routineMove = e.target.closest('[data-routine-move]');
            if (routineMove) {
              e.preventDefault();
              e.stopPropagation();
              this.moveRoutine(
                Number(routineMove.dataset.routineIndex),
                Number(routineMove.dataset.routineMove)
              );
              return;
            }

            const routineEdit = e.target.closest('[data-edit-routine]');
            if (routineEdit) {
              e.preventDefault();
              e.stopPropagation();
              this.openRoutineEditModal(Number(routineEdit.dataset.editRoutine));
              return;
            }

            const routineDuplicate = e.target.closest('[data-duplicate-routine]');
            if (routineDuplicate) {
              e.preventDefault();
              e.stopPropagation();
              this.duplicateRoutine(Number(routineDuplicate.dataset.duplicateRoutine));
              return;
            }

            const routineDelete = e.target.closest('[data-delete-routine]');
            if (routineDelete) {
              e.preventDefault();
              e.stopPropagation();
              this.askDelete({ type: 'routine', index: Number(routineDelete.dataset.deleteRoutine) });
              return;
            }

            const routineBtn = e.target.closest('[data-routine-index]');
            if (routineBtn) {
              e.preventDefault();
              e.stopPropagation();
              this.applyRoutine(Number(routineBtn.dataset.routineIndex));
              return;
            }

            const actionBtn = e.target.closest('[data-action]');
            if (actionBtn) {
              e.preventDefault();
              e.stopPropagation();

              const action = actionBtn.dataset.action;
              const type = actionBtn.dataset.cardType;
              const index = Number(actionBtn.dataset.index);
              const categoryIndex = actionBtn.dataset.categoryIndex !== undefined ? Number(actionBtn.dataset.categoryIndex) : null;

              if (action === 'add') {
                const addType = actionBtn.dataset.addType;
                const addCatIndex = actionBtn.dataset.categoryIndex !== undefined ? Number(actionBtn.dataset.categoryIndex) : null;
                this.openTileModal({ mode: 'add', type: addType, categoryIndex: addCatIndex });
                return;
              }

              if (action === 'edit') {
                this.openTileModal({ mode: 'edit', type, index, categoryIndex });
                return;
              }

              if (action === 'delete') {
                this.askDelete({ type, index, categoryIndex });
                return;
              }

              if (action === 'move-left' || action === 'move-right') {
                this.moveTile({ type, index, categoryIndex, direction: action === 'move-left' ? -1 : 1 });
                return;
              }

              if (action === 'toggle-favourite') {
                this.toggleFavourite(categoryIndex, index);
                return;
              }
            }

            const card = e.target.closest('.dt-card, .dt-fav-card');
            if (!card) return;

            const addType = card.dataset.addType;
            if (addType) {
              const addCatIndex = card.dataset.categoryIndex !== undefined ? Number(card.dataset.categoryIndex) : null;
              this.openTileModal({ mode: 'add', type: addType, categoryIndex: addCatIndex });
              return;
            }

            const type = card.dataset.cardType;
            const index = Number(card.dataset.index);
            const categoryIndex = card.dataset.categoryIndex !== undefined ? Number(card.dataset.categoryIndex) : null;

            this.root.querySelectorAll('.dt-card, .dt-fav-card').forEach(el => el.classList.remove(ACTIVE_CLASS));
            card.classList.add(ACTIVE_CLASS);
            setTimeout(() => card.classList.remove(ACTIVE_CLASS), 700);

            if (type === 'category') {
              this.currentView = 'category';
              this.currentCategoryIndex = index;
              this.render();
              return;
            }

            if (type === 'item' && categoryIndex !== null) {
              const item = this.state.categories[categoryIndex]?.items[index];
              if (!item) return;
              this.state.sentence.push(this.normaliseItem(item));
              this.state.sentence = this.state.sentence.slice(-20);
              this.renderSentence();
              this.queueSave();

              if (this.state.settings.autoSpeak) {
                this.speak(item.name);
              }
            }
          }

          applyTemplatePack(packIndex) {
            if (!this.editMode) {
              this.toast('Turn on edit mode to add template buttons', 'info');
              return;
            }

            const pack = TEMPLATE_PACKS[packIndex];
            if (!pack) return;

            let targetCategoryIndex = this.currentCategoryIndex;

            if (targetCategoryIndex === null || targetCategoryIndex === undefined) {
              const existingIndex = this.state.categories.findIndex(
                c => c.name.toLowerCase() === pack.name.toLowerCase()
              );

              if (existingIndex >= 0) {
                targetCategoryIndex = existingIndex;
              } else {
                this.state.categories.push({
                  name: pack.name,
                  emoji: pack.items[0]?.emoji || '&#x1f4c1;',
                  imageId: 0,
                  imageUrl: '',
                  displayMode: 'emoji_text',
                  items: []
                });
                targetCategoryIndex = this.state.categories.length - 1;
              }
            }

            const targetCategory = this.state.categories[targetCategoryIndex];
            if (!targetCategory) return;

            let added = 0;

            pack.items.forEach(templateItem => {
              const exists = targetCategory.items.some(
                item => item.name.toLowerCase() === templateItem.name.toLowerCase()
              );

              if (!exists) {
                targetCategory.items.push(this.normaliseItem({
                  ...templateItem,
                  imageId: 0,
                  imageUrl: '',
                  displayMode: 'emoji_text'
                }));
                added++;
              }
            });

            this.currentView = 'category';
            this.currentCategoryIndex = targetCategoryIndex;
            this.queueSave();
            this.render();
            this.toast(`${added} template buttons added to ${targetCategory.name}`, 'success');
          }

          toggleFullscreen() {
            const onMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);

            if (onMobile) {
              this.root.classList.toggle('dt-fullscreen');
              return;
            }

            if (!document.fullscreenElement) {
              this.root.classList.add('dt-fullscreen');
              this.root.requestFullscreen?.().catch(() => {
                this.root.classList.remove('dt-fullscreen');
              });
            } else {
              document.exitFullscreen?.();
            }
          }

          toggleEditMode() {
            if (this.editMode) {
              this.editMode = false;
              this.render();
              this.toast('Edit mode disabled', 'info');
              return;
            }

            if (!this.parentCode) {
              const code = window.prompt('Set a parent code for edit mode');
              if (!code || !String(code).trim()) return;
              this.parentCode = String(code).trim();
              sessionStorage.setItem('dotty_parent_code', this.parentCode);
            }

            const entered = window.prompt('Enter parent code');
            if (!entered) return;

            if (String(entered).trim() !== this.parentCode) {
              this.toast('Wrong parent code', 'error');
              return;
            }

            this.editMode = true;
            this.render();
            this.toast('Edit mode enabled', 'success');
          }

          openTileModal(context) {
            this.editingContext = {
              ...context,
              tempImageId: 0,
              tempImageUrl: ''
            };

            this.emojiSearchTerm = '';
            this.elements.emojiSearch.value = '';

            let current = null;

            if (context.mode === 'edit') {
              if (context.type === 'category') {
                current = this.state.categories[context.index];
              } else {
                current = this.state.categories[context.categoryIndex]?.items[context.index];
              }
            }

            this.elements.tileModalTitle.textContent =
              context.mode === 'add'
                ? (context.type === 'category' ? 'Add Category' : 'Add Item')
                : (context.type === 'category' ? 'Edit Category' : 'Edit Item');

            this.elements.tileName.value = current?.name || '';
            this.elements.tileEmoji.value = current?.emoji || '';
            this.elements.tileDisplayMode.value = current?.displayMode || 'emoji_text';
            this.elements.tileFavourite.checked = !!current?.favourite;
            this.elements.favouriteWrap.classList.toggle('dt-hidden', context.type === 'category');

            this.editingContext.tempImageId = Number(current?.imageId || 0) || 0;
            this.editingContext.tempImageUrl = current?.imageUrl || '';

            this.renderEmojiPicker();
            this.renderImagePreview();
            this.updateImageButtons();

            this.elements.tileModal.showModal();
            setTimeout(() => this.elements.tileName.focus(), 30);
          }

          closeTileModal() {
            this.editingContext = null;
            this.elements.tileModal.close();
          }

          renderEmojiPicker() {
            const search = this.emojiSearchTerm;
            const currentEmoji = this.elements.tileEmoji.value.trim();

            const list = EMOJIS.filter(item => {
              if (!search) return true;
              return item.name.toLowerCase().includes(search);
            }).slice(0, 140);

            this.elements.emojiGrid.innerHTML = list.map(item => `
              <button class="dt-emoji-option ${currentEmoji === item.emoji ? 'is-selected' : ''}" type="button" data-pick-emoji="${this.escape(item.emoji)}" data-pick-name="${this.escape(item.name)}">
                <span class="e">${this.escape(item.emoji)}</span>
                <span class="n">${this.escape(item.name)}</span>
              </button>
            `).join('');

            this.elements.emojiGrid.querySelectorAll('[data-pick-emoji]').forEach(btn => {
              btn.addEventListener('click', () => {
                const emoji = btn.dataset.pickEmoji;
                const name = btn.dataset.pickName;
                this.elements.tileEmoji.value = emoji;
                if (!this.elements.tileName.value.trim()) {
                  this.elements.tileName.value = name;
                }
                this.renderEmojiPicker();
              });
            });
          }

          renderImagePreview() {
            const url = this.editingContext?.tempImageUrl || '';
            if (!url) {
              this.elements.imagePreview.innerHTML = `<div class="dt-image-preview-empty">No image</div>`;
              return;
            }
            this.elements.imagePreview.innerHTML = `<img decoding="async" src="${this.escape(url)}" alt="Selected image">`;
          }

          updateImageButtons() {
            const canUpload = !!window.DottyToolConfig?.canUpload && !!window.wp?.media;
            this.elements.pickImageBtn.disabled = !canUpload;
            this.elements.cameraImageBtn.disabled = !window.DottyToolConfig?.canUpload;
            this.elements.pickImageBtn.textContent = canUpload ? 'Choose Image' : 'Upload unavailable';
          }

          pickImage() {
            if (!window.DottyToolConfig?.canUpload || !window.wp?.media) {
              this.toast('Image upload needs a logged-in user with media access', 'error');
              return;
            }

            const frame = window.wp.media({
              title: 'Choose tile image',
              button: { text: 'Use this image' },
              multiple: false,
              library: { type: ['image'] }
            });

            frame.on('select', () => {
              const attachment = frame.state().get('selection').first().toJSON();
              this.editingContext.tempImageId = Number(attachment.id || 0);
              this.editingContext.tempImageUrl = attachment.sizes?.medium?.url || attachment.url || '';
              this.renderImagePreview();

              if (this.elements.tileDisplayMode.value === 'emoji_text') {
                this.elements.tileDisplayMode.value = 'image_text';
              }
            });

            frame.open();
          }

          pickCameraImage() {
            if (!window.DottyToolConfig?.canUpload) {
              this.toast('Camera upload needs a logged-in user with upload access', 'error');
              return;
            }
            this.elements.cameraFile.click();
          }

          async handleCameraUpload(e) {
            const file = e.target.files?.[0];
            if (!file) return;

            const formData = new FormData();
            formData.append('action', 'dotty_upload_image');
            formData.append('nonce', window.DottyToolConfig.nonce);
            formData.append('image', file);

            try {
              const res = await fetch(window.DottyToolConfig.ajaxUrl, {
                method: 'POST',
                credentials: 'same-origin',
                body: formData
              });

              const json = await res.json();

              if (json?.success && json?.data) {
                this.editingContext.tempImageId = Number(json.data.id || 0);
                this.editingContext.tempImageUrl = json.data.url || '';
                this.renderImagePreview();
                if (this.elements.tileDisplayMode.value === 'emoji_text') {
                  this.elements.tileDisplayMode.value = 'image_text';
                }
                this.toast('Camera image uploaded', 'success');
              } else {
                this.toast(json?.data?.message || 'Upload failed', 'error');
              }
            } catch (err) {
              this.toast('Upload failed', 'error');
            } finally {
              e.target.value = '';
            }
          }

          removeModalImage() {
            if (!this.editingContext) return;
            this.editingContext.tempImageId = 0;
            this.editingContext.tempImageUrl = '';
            this.renderImagePreview();

            if (this.elements.tileDisplayMode.value !== 'emoji_text') {
              this.elements.tileDisplayMode.value = 'emoji_text';
            }
          }

          saveTileModal() {
            if (!this.editingContext) return;

            const name = this.cleanText(this.elements.tileName.value, 'Untitled');
            const emoji = this.cleanEmoji(this.elements.tileEmoji.value, '&#x2753;');
            const displayMode = this.cleanDisplayMode(this.elements.tileDisplayMode.value);
            const imageId = Number(this.editingContext.tempImageId || 0) || 0;
            const imageUrl = this.cleanImageUrl(this.editingContext.tempImageUrl || '');
            const favourite = !!this.elements.tileFavourite.checked;

            const payload = {
              name,
              emoji,
              imageId,
              imageUrl,
              displayMode,
              favourite,
              style: this.editingContext?.mode === 'edit'
                ? (this.editingContext.type === 'item'
                    ? (this.state.categories[this.editingContext.categoryIndex]?.items[this.editingContext.index]?.style || '')
                    : '')
                : ''
            };

            const ctx = this.editingContext;

            if (ctx.mode === 'add') {
              if (ctx.type === 'category') {
                this.state.categories.push({ ...payload, items: [] });
                this.currentView = 'main';
              } else {
                this.state.categories[ctx.categoryIndex]?.items.push(payload);
                this.currentView = 'category';
                this.currentCategoryIndex = ctx.categoryIndex;
              }
              this.toast('Tile added', 'success');
            } else {
              if (ctx.type === 'category') {
                const cat = this.state.categories[ctx.index];
                if (cat) {
                  cat.name = payload.name;
                  cat.emoji = payload.emoji;
                  cat.imageId = payload.imageId;
                  cat.imageUrl = payload.imageUrl;
                  cat.displayMode = payload.displayMode;
                }
              } else {
                const item = this.state.categories[ctx.categoryIndex]?.items[ctx.index];
                if (item) {
                  item.name = payload.name;
                  item.emoji = payload.emoji;
                  item.imageId = payload.imageId;
                  item.imageUrl = payload.imageUrl;
                  item.displayMode = payload.displayMode;
                  item.favourite = payload.favourite;
                }
              }
              this.toast('Tile updated', 'success');
            }

            this.queueSave();
            this.closeTileModal();
            this.render();
          }

          toggleFavourite(categoryIndex, index) {
            const item = this.state.categories[categoryIndex]?.items[index];
            if (!item) return;
            item.favourite = !item.favourite;
            this.queueSave();
            this.render();
          }

          openRoutineModal() {
            if (!this.state.sentence.length) {
              this.toast('Build a sentence first', 'info');
              return;
            }

            this.elements.routineName.value = '';
            this.elements.routineEmoji.value = '&#x2b50;';
            this.elements.routinePreview.innerHTML = this.state.sentence.map(i => this.escape(i.name)).join(' → ');
            this.elements.routineModal.showModal();
          }

          saveRoutine() {
            const name = this.cleanText(this.elements.routineName.value, 'Routine');
            const emoji = this.cleanEmoji(this.elements.routineEmoji.value, '&#x2b50;');

            if (!this.state.sentence.length) {
              this.toast('Sentence is empty', 'error');
              return;
            }

            this.state.routines.push({
              name,
              emoji,
              items: this.state.sentence.map(item => this.normaliseItem(item))
            });

            this.state.routines = this.state.routines.slice(-20);
            this.queueSave();
            this.elements.routineModal.close();
            this.render();
            this.toast('Routine saved', 'success');
          }

          openRoutineEditModal(index) {
            const routine = this.state.routines[index];
            if (!routine) return;

            this.editingRoutineIndex = index;
            this.elements.routineEditName.value = routine.name || '';
            this.elements.routineEditEmoji.value = routine.emoji || '&#x2b50;';
            this.renderRoutineEditPreview();
            this.elements.routineEditModal.showModal();
          }

          closeRoutineEditModal() {
            this.editingRoutineIndex = null;
            this.elements.routineEditModal.close();
          }

          renderRoutineEditPreview() {
            const index = this.editingRoutineIndex;
            const routine = this.state.routines[index];
            if (!routine || !Array.isArray(routine.items) || !routine.items.length) {
              this.elements.routineEditPreview.innerHTML = 'No items in this routine yet.';
              return;
            }

            this.elements.routineEditPreview.innerHTML = routine.items
              .map(item => this.escape(item.name))
              .join(' → ');
          }

          saveRoutineEdit() {
            const index = this.editingRoutineIndex;
            const routine = this.state.routines[index];
            if (!routine) return;

            routine.name = this.cleanText(this.elements.routineEditName.value, 'Routine');
            routine.emoji = this.cleanEmoji(this.elements.routineEditEmoji.value, '&#x2b50;');

            this.queueSave();
            this.render();
            this.closeRoutineEditModal();
            this.toast('Routine updated', 'success');
          }

          overwriteRoutineFromSentence() {
            const index = this.editingRoutineIndex;
            const routine = this.state.routines[index];
            if (!routine) return;

            if (!this.state.sentence.length) {
              this.toast('Build a sentence first', 'info');
              return;
            }

            routine.items = this.state.sentence.map(item => this.normaliseItem(item)).slice(0, 30);
            this.renderRoutineEditPreview();
            this.queueSave();
            this.render();
            this.toast('Routine replaced with current sentence', 'success');
          }

          duplicateRoutine(index) {
            const routine = this.state.routines[index];
            if (!routine) return;

            const copy = {
              name: this.cleanText(`${routine.name} Copy`, 'Routine Copy'),
              emoji: routine.emoji || '&#x2b50;',
              items: routine.items.map(item => this.normaliseItem(item))
            };

            this.state.routines.splice(index + 1, 0, copy);
            this.state.routines = this.state.routines.slice(0, 20);
            this.queueSave();
            this.render();
            this.toast('Routine copied', 'success');
          }

          moveRoutine(index, direction) {
            const target = index + direction;
            if (target < 0 || target >= this.state.routines.length) return;

            const arr = this.state.routines;
            [arr[index], arr[target]] = [arr[target], arr[index]];
            this.queueSave();
            this.render();
          }

          applyRoutine(index) {
            const routine = this.state.routines[index];
            if (!routine || !Array.isArray(routine.items) || !routine.items.length) {
              this.toast('Routine is empty', 'info');
              return;
            }

            this.state.sentence = [...routine.items.map(i => this.normaliseItem(i))].slice(-20);
            this.render();
            this.queueSave();

            if (this.state.settings.autoSpeak) {
              this.speak(routine.items.map(i => i.name).join(' '));
            }
          }

          askDelete(context) {
            this.pendingDelete = context;
            let label = 'item';
            if (context.type === 'category') label = 'category';
            if (context.type === 'routine') label = 'routine';
            this.elements.confirmText.textContent = `Delete this ${label}?`;
            this.elements.confirmModal.showModal();
          }

          confirmDelete() {
            const ctx = this.pendingDelete;
            if (!ctx) return;

            if (ctx.type === 'category') {
              this.state.categories.splice(ctx.index, 1);
              this.currentView = 'main';
              this.currentCategoryIndex = null;
            } else if (ctx.type === 'routine') {
              this.state.routines.splice(ctx.index, 1);
            } else {
              this.state.categories[ctx.categoryIndex]?.items.splice(ctx.index, 1);
              this.currentView = 'category';
              this.currentCategoryIndex = ctx.categoryIndex;
            }

            this.pendingDelete = null;
            this.elements.confirmModal.close();
            this.queueSave();
            this.render();
            this.toast('Deleted', 'success');
          }

          moveTile({ type, index, categoryIndex, direction }) {
            if (type === 'category') {
              const target = index + direction;
              if (target < 0 || target >= this.state.categories.length) return;
              const arr = this.state.categories;
              [arr[index], arr[target]] = [arr[target], arr[index]];
              this.queueSave();
              this.render();
              return;
            }

            const items = this.state.categories[categoryIndex]?.items;
            if (!items) return;
            const target = index + direction;
            if (target < 0 || target >= items.length) return;
            [items[index], items[target]] = [items[target], items[index]];
            this.queueSave();
            this.render();
          }

          removeSentenceToken(index) {
            if (index < 0 || index >= this.state.sentence.length) return;
            this.state.sentence.splice(index, 1);
            this.renderSentence();
            this.queueSave();
          }

          speak(text) {
            if (!('speechSynthesis' in window)) {
              this.toast('Speech is not supported on this browser', 'error');
              return;
            }

            try {
              window.speechSynthesis.cancel();
            } catch (_) {}

            const utterance = new SpeechSynthesisUtterance(text);
            utterance.lang = 'en-GB';
            utterance.rate = this.state.settings.speechRate;

            const voices = window.speechSynthesis.getVoices();
            const voice =
              voices.find(v => v.lang === 'en-GB' && /female|susan|libby|hazel|google uk english female/i.test(v.name)) ||
              voices.find(v => v.lang === 'en-GB') ||
              voices[0];

            if (voice) utterance.voice = voice;
            window.speechSynthesis.speak(utterance);
          }

          speakSentence() {
            if (!this.state.sentence.length) {
              this.toast('Sentence is empty', 'info');
              return;
            }
            this.speak(this.state.sentence.map(x => x.name).join(' '));
          }

          undoSentence() {
            if (!this.state.sentence.length) return;
            this.state.sentence.pop();
            this.renderSentence();
            this.queueSave();
          }

          clearSentence() {
            this.state.sentence = [];
            this.renderSentence();
            this.queueSave();
          }

          exportJsonFile() {
            const blob = new Blob([JSON.stringify(this.state, null, 2)], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'dotty-learning-tool-backup.json';
            document.body.appendChild(a);
            a.click();
            a.remove();
            URL.revokeObjectURL(url);
            this.toast('Backup exported', 'success');
          }

          async importJsonFile(e) {
            const file = e.target.files?.[0];
            if (!file) return;

            try {
              const text = await file.text();
              const parsed = JSON.parse(text);
              this.state = this.normaliseData(parsed);
              this.showRoutines = !!this.state.ui?.showRoutines;
              this.applySettingsClasses();
              this.queueSave();
              this.render();
              this.toast('Backup loaded', 'success');
            } catch (err) {
              this.toast('Invalid JSON file', 'error');
            } finally {
              e.target.value = '';
            }
          }

          escape(str) {
            return String(str)
              .replace(/&/g, '&amp;')
              .replace(/</g, '&lt;')
              .replace(/>/g, '&gt;')
              .replace(/"/g, '&quot;')
              .replace(/'/g, '&#039;');
          }
        }

        document.addEventListener('DOMContentLoaded', () => {
          const root = document.getElementById('dotty-app-root');
          if (!root) return;
          const app = new DottyTool(root);
          app.init();
          window.dottyTool = app;
        });
      })();
    </script>
    </p>



<p>Supporting a child with additional needs can feel like navigating a world without a map — especially when it comes to communication. That’s why we’ve created a free, digital set of SEN visual and emoji learning cards, designed specifically for children with autism, global developmental delay (GDD), and speech and language difficulties. These tools aren’t just useful — they’re essential for families travelling, learning, or living life in full colour.</p>



<p><strong>What Are SEN Visual &amp; Emoji Learning Cards?</strong></p>



<p>Our digital SEN cards are a modern take on classic communication tools. Think Picture Exchange Communication System (PECS) — but upgraded for the digital age. We’ve blended clear visuals with emojis children recognise from daily life, especially screens and devices they already love our Emoji Learning Tool.</p>



<p>Each card represents a common need, emotion, action, or choice. From “I’m hungry” to “I need a break,” or from “I feel happy” to “I’m scared,” these digital cards give children a safe, friendly way to express themselves without needing words.</p>



<p>They work brilliantly on phones, tablets, or touchscreen laptops — no printing required, no cutting out, no fuss. Just tap, show, and connect.</p>



<p><strong>Why Use Emoji-Based SEN Tools?</strong></p>



<p>Emoji learning taps into how children with SEN already engage with the world. Emojis are:</p>



<ul class="wp-block-list">
<li>Visually consistent across platforms</li>



<li>Emotionally expressive</li>



<li>Familiar from messaging apps and games</li>



<li>Easy to understand, even for non-readers</li>
</ul>



<p>For neurodivergent children, especially those on the autism spectrum, consistency and clarity are everything. Emojis offer both. That’s why we’ve included emoji options on many of the cards — side-by-side with custom visuals — to create context-rich tools that support comprehension and build emotional awareness.</p>



<p>totally free!</p>



<p>if your planning a trip try our free route planner!<br><a href="https://tantrummingtrailblazers.com/tools/make-my-drive-fun/">Make My Drive Fun, Share Road Trips, Attractions, Stops &amp; More!</a></p>



<p></p>
<p>The post <a href="https://tantrummingtrailblazers.com/ai/sen-visual-communication-cards/">Digital Emoji Learning Tool SEN Visual Communication Cards </a> appeared first on <a href="https://tantrummingtrailblazers.com">Tantrumming Trailblazers</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://tantrummingtrailblazers.com/ai/sen-visual-communication-cards/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4083</post-id>	</item>
	</channel>
</rss>
