<?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>Educational Archives - Tantrumming Trailblazers</title>
	<atom:link href="https://tantrummingtrailblazers.com/tag/educational/feed/" rel="self" type="application/rss+xml" />
	<link>https://tantrummingtrailblazers.com/tag/educational/</link>
	<description>Where Travel and Tantrums Collide in Epic Journeys!</description>
	<lastBuildDate>Sun, 03 May 2026 09:56:51 +0000</lastBuildDate>
	<language>en-GB</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</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>Educational Archives - Tantrumming Trailblazers</title>
	<link>https://tantrummingtrailblazers.com/tag/educational/</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 Emoji PECs system using emojis that&#8230;</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 class="wp-block-paragraph">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 class="wp-block-paragraph">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: "ec375263d7",
        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 class="wp-block-paragraph">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 class="wp-block-paragraph"><strong>What Are SEN Visual &amp; Emoji Learning Cards?</strong></p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">They work brilliantly on phones, tablets, or touchscreen laptops — no printing required, no cutting out, no fuss. Just tap, show, and connect.</p>



<p class="wp-block-paragraph"><strong>Why Use Emoji-Based SEN Tools?</strong></p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">totally free!</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph"></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>
		<item>
		<title>Goodbye Brno, Hello Prague: A Day of Science and New Adventures</title>
		<link>https://tantrummingtrailblazers.com/czech-republic/goodbye-brno-hello-prague-a-day-of-science-and-new-adventures/</link>
					<comments>https://tantrummingtrailblazers.com/czech-republic/goodbye-brno-hello-prague-a-day-of-science-and-new-adventures/#respond</comments>
		
		<dc:creator><![CDATA[Dennis]]></dc:creator>
		<pubDate>Thu, 09 Jan 2025 06:46:52 +0000</pubDate>
				<category><![CDATA[Czech Republic]]></category>
		<category><![CDATA[Centre]]></category>
		<category><![CDATA[Educational]]></category>
		<category><![CDATA[Exploration]]></category>
		<category><![CDATA[Family]]></category>
		<category><![CDATA[Prague]]></category>
		<category><![CDATA[Science]]></category>
		<category><![CDATA[travel]]></category>
		<guid isPermaLink="false">https://tantrummingtrailblazers.com/czech-republic/goodbye-brno-hello-prague-a-day-of-science-and-new-adventures/</guid>

					<description><![CDATA[<p>Family travel adventures As we said goodbye to Brno and hit the road to Prague, we reflected on a day packed with discovery and&#8230;</p>
<p>The post <a href="https://tantrummingtrailblazers.com/czech-republic/goodbye-brno-hello-prague-a-day-of-science-and-new-adventures/">Goodbye Brno, Hello Prague: A Day of Science and New Adventures</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%2Fczech-republic%2Fgoodbye-brno-hello-prague-a-day-of-science-and-new-adventures%2F&amp;linkname=Goodbye%20Brno%2C%20Hello%20Prague%3A%20A%20Day%20of%20Science%20and%20New%20Adventures" 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%2Fczech-republic%2Fgoodbye-brno-hello-prague-a-day-of-science-and-new-adventures%2F&amp;linkname=Goodbye%20Brno%2C%20Hello%20Prague%3A%20A%20Day%20of%20Science%20and%20New%20Adventures" 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%2Fczech-republic%2Fgoodbye-brno-hello-prague-a-day-of-science-and-new-adventures%2F&amp;linkname=Goodbye%20Brno%2C%20Hello%20Prague%3A%20A%20Day%20of%20Science%20and%20New%20Adventures" 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%2Fczech-republic%2Fgoodbye-brno-hello-prague-a-day-of-science-and-new-adventures%2F&#038;title=Goodbye%20Brno%2C%20Hello%20Prague%3A%20A%20Day%20of%20Science%20and%20New%20Adventures" data-a2a-url="https://tantrummingtrailblazers.com/czech-republic/goodbye-brno-hello-prague-a-day-of-science-and-new-adventures/" data-a2a-title="Goodbye Brno, Hello Prague: A Day of Science and New Adventures"></a></p><strong>Family travel adventures</strong> <p class="p1 wp-block-paragraph">As we said goodbye to Brno and hit the road to Prague, we reflected on a day packed with discovery and fun. Brno had impressed us with its castles and Christmas lights, but it was our visit to the VIDA! Science Centre that wrapped it up.</p>



<p class="p1 wp-block-paragraph">We weren&rsquo;t sure what to expect from VIDA!, but it turned out to be a perfect way to spend the afternoon. Dotty was completely absorbed by the water table, playing with lock gates playing with the flow of water. Given her condition hands-on activities bring out her best smile, and this one really delivered.</p>



<p class="p1 wp-block-paragraph">Georgia, always curious, spent her time exploring exhibits on renewable energy and how wind power works. Watching her excitement as she pieced together how these things connect to everyday life was a bit of a proud parent moment for us. Ali and I even got stuck into a few of the activities ourselves (Ali sillily went in to a spinning machine and had to leave, she nearly passed out from labyrinthitis) even so there&rsquo;s something about playing with science that makes you forget you&rsquo;re supposed to be the grown up.</p>



<p class="p1 wp-block-paragraph">VIDA! Seems to sneak in education without making it feel like a lecture. We left feeling like we&rsquo;d all learned something new while having fun at the same time.</p>



<p class="p3 wp-block-paragraph">Onward to Prague</p>



<p class="p1 wp-block-paragraph">The drive to Prague wasn&rsquo;t long, and as we arrived, the city greeted us with its lively energy and busy roads. We&rsquo;re staying just outside the centre, so it&rsquo;s a short tram ride into the heart of the action. The kids are already looking forward to exploring tomorrow Georgia is determined to see Prague Castle, while Dotty will just be excited on the tram ride itself (it&rsquo;s the little things that make her day).</p>



<p class="p1 wp-block-paragraph">For now, though, we&rsquo;re settling in and getting ready for another full day of adventures. Brno gave us plenty of memorable moments, and we&rsquo;re excited to see what Prague has in store.</p>



<p class="p1 wp-block-paragraph">Traveling as a family isn&rsquo;t always smooth sailing, but it&rsquo;s moments like today that make it all worthwhile. If you&rsquo;re looking for tips, tricks, or just a bit of inspiration for your own family travels, check out our <a href="https://tantrummingtrailblazers.com/travel-tips/">Travel Tips Page</a>. We&rsquo;re learning as we go, and we&rsquo;d love to share what works (and what doesn&rsquo;t) with you.</p>


<p>Learn more about <strong>Family travel adventures</strong>!</p>

<p>Learn more about <strong>Family travel adventures</strong>!</p><p>The post <a href="https://tantrummingtrailblazers.com/czech-republic/goodbye-brno-hello-prague-a-day-of-science-and-new-adventures/">Goodbye Brno, Hello Prague: A Day of Science and New Adventures</a> appeared first on <a href="https://tantrummingtrailblazers.com">Tantrumming Trailblazers</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://tantrummingtrailblazers.com/czech-republic/goodbye-brno-hello-prague-a-day-of-science-and-new-adventures/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3344</post-id>	</item>
		<item>
		<title>🌟Favourite Finds</title>
		<link>https://tantrummingtrailblazers.com/favourite-finds/</link>
		
		<dc:creator><![CDATA[Dennis]]></dc:creator>
		<pubDate>Wed, 14 Feb 2024 20:59:50 +0000</pubDate>
				<category><![CDATA[Baby]]></category>
		<category><![CDATA[Educational]]></category>
		<category><![CDATA[Family]]></category>
		<category><![CDATA[Gear]]></category>
		<category><![CDATA[Kids]]></category>
		<category><![CDATA[Toys]]></category>
		<category><![CDATA[travel]]></category>
		<guid isPermaLink="false">http://cashbak.uk/Blog/?page_id=122</guid>

					<description><![CDATA[<p>Family travel products Favourite finds. Useful kit for real family adventures. Welcome to our family travel product hub. These are the bags, swimming bits,&#8230;</p>
<p>The post <a href="https://tantrummingtrailblazers.com/favourite-finds/">🌟Favourite Finds</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%2Ffavourite-finds%2F&amp;linkname=%F0%9F%8C%9FFavourite%20Finds" 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%2Ffavourite-finds%2F&amp;linkname=%F0%9F%8C%9FFavourite%20Finds" 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%2Ffavourite-finds%2F&amp;linkname=%F0%9F%8C%9FFavourite%20Finds" 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%2Ffavourite-finds%2F&#038;title=%F0%9F%8C%9FFavourite%20Finds" data-a2a-url="https://tantrummingtrailblazers.com/favourite-finds/" data-a2a-title="&#x1f31f;Favourite Finds"></a></p>
<section class="ttb-products-page" aria-labelledby="ttb-products-title">
  <style>
    .ttb-products-page {
      --ttb-ink: #1d1712;
      --ttb-soft-ink: #5f554d;
      --ttb-muted: #8d8176;
      --ttb-cream: #f8f3e8;
      --ttb-paper: #fffdf8;
      --ttb-blue: #e8f2f7;
      --ttb-rust: #c84f1a;
      --ttb-orange: #ff7a32;
      --ttb-border: rgba(29, 23, 18, 0.12);
      --ttb-shadow: 0 18px 45px rgba(29, 23, 18, 0.10);

      background: var(--ttb-cream);
      color: var(--ttb-ink);
      font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      line-height: 1.65;
      overflow: hidden;
    }

    .ttb-products-page * {
      box-sizing: border-box;
    }

    .ttb-products-wrap {
      max-width: 1180px;
      margin: 0 auto;
      padding: clamp(28px, 5vw, 72px) clamp(18px, 4vw, 38px);
    }

    .ttb-products-hero {
      display: grid;
      grid-template-columns: minmax(0, 1.12fr) minmax(280px, 0.88fr);
      gap: clamp(20px, 4vw, 34px);
      align-items: stretch;
      margin-bottom: clamp(34px, 6vw, 58px);
    }

    .ttb-products-panel {
      position: relative;
      padding: clamp(26px, 5vw, 52px);
      border-radius: 34px;
      background:
        radial-gradient(circle at top right, rgba(255, 122, 50, 0.22), transparent 34%),
        linear-gradient(135deg, #fffdf8, #eef6f9);
      border: 1px solid var(--ttb-border);
      box-shadow: var(--ttb-shadow);
      overflow: hidden;
    }

    .ttb-products-kicker {
      margin: 0 0 12px;
      color: var(--ttb-rust);
      font-size: 0.78rem;
      font-weight: 900;
      letter-spacing: 0.32em;
      text-transform: uppercase;
    }

    .ttb-products-panel h1,
    .ttb-section-head h2,
    .ttb-cta h2 {
      margin: 0;
      font-size: clamp(2.6rem, 7vw, 5.8rem);
      line-height: 0.88;
      letter-spacing: -0.07em;
      font-weight: 950;
    }

    .ttb-script-dark {
      display: block;
      color: var(--ttb-rust);
      font-family: Georgia, "Times New Roman", serif;
      font-size: clamp(1.5rem, 4vw, 3rem);
      font-style: italic;
      font-weight: 400;
      letter-spacing: 0;
      line-height: 1.05;
      margin-top: 10px;
    }

    .ttb-products-lede,
    .ttb-lede {
      max-width: 860px;
      margin: 20px 0 0;
      color: var(--ttb-soft-ink);
      font-size: clamp(1rem, 2vw, 1.18rem);
    }

    .ttb-disclosure-card {
      padding: 24px;
      border-radius: 28px;
      background: #1d1712;
      color: #fff;
      box-shadow: var(--ttb-shadow);
    }

    .ttb-disclosure-card h2 {
      margin: 0 0 10px;
      color: #fff;
      font-size: 1.4rem;
      line-height: 1.15;
      letter-spacing: -0.03em;
    }

    .ttb-disclosure-card p {
      margin: 0;
      color: rgba(255,255,255,0.8);
    }

    .ttb-trust-strip {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 12px;
      margin-top: 18px;
    }

    .ttb-trust-strip span {
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 54px;
      padding: 12px;
      border-radius: 18px;
      background: rgba(255,255,255,0.08);
      border: 1px solid rgba(255,255,255,0.12);
      color: rgba(255,255,255,0.9);
      font-size: 0.9rem;
      font-weight: 800;
      text-align: center;
    }

    .ttb-section-head {
      margin: clamp(42px, 7vw, 72px) 0 clamp(22px, 4vw, 34px);
    }

    .ttb-section-kicker {
      margin: 0 0 10px;
      color: var(--ttb-rust);
      font-size: 0.78rem;
      font-weight: 900;
      letter-spacing: 0.32em;
      text-transform: uppercase;
    }

    .ttb-category-grid {
      display: grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      gap: 18px;
    }

    .ttb-category-card {
      position: relative;
      min-height: 100%;
      padding: 24px;
      border: 1px solid var(--ttb-border);
      border-radius: 26px;
      background: var(--ttb-paper);
      box-shadow: 0 10px 26px rgba(29, 23, 18, 0.06);
      overflow: hidden;
    }

    .ttb-category-card::before {
      content: "";
      position: absolute;
      inset: 0 auto 0 0;
      width: 5px;
      background: var(--ttb-rust);
      opacity: 0.88;
    }

    .ttb-icon {
      width: 46px;
      height: 46px;
      display: grid;
      place-items: center;
      margin-bottom: 16px;
      border-radius: 16px;
      background: var(--ttb-blue);
      font-size: 1.45rem;
    }

    .ttb-category-card h3 {
      margin: 0 0 10px;
      font-size: 1.32rem;
      line-height: 1.2;
      letter-spacing: -0.02em;
    }

    .ttb-category-card p {
      margin: 0;
      color: var(--ttb-soft-ink);
    }

    .ttb-card-link {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      margin-top: 18px;
      min-height: 42px;
      padding: 10px 14px;
      border-radius: 999px;
      background: var(--ttb-rust);
      color: #fff;
      font-weight: 900;
      text-decoration: none;
      box-shadow: 0 10px 22px rgba(200,79,26,0.18);
    }

    .ttb-card-link:hover {
      transform: translateY(-2px);
      color: #fff;
    }

    .ttb-shop-section {
      margin-top: clamp(38px, 7vw, 70px);
      padding: clamp(22px, 4vw, 34px);
      border-radius: 34px;
      background: var(--ttb-paper);
      border: 1px solid var(--ttb-border);
      box-shadow: var(--ttb-shadow);
      scroll-margin-top: 90px;
    }

    .ttb-shop-head {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      gap: 18px;
      align-items: end;
      margin-bottom: 22px;
    }

    .ttb-shop-head h2 {
      margin: 0;
      font-size: clamp(2rem, 5vw, 4rem);
      line-height: 0.9;
      letter-spacing: -0.06em;
      font-weight: 950;
    }

    .ttb-shop-head p {
      margin: 10px 0 0;
      max-width: 720px;
      color: var(--ttb-soft-ink);
    }

    .ttb-back-top {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-height: 42px;
      padding: 10px 14px;
      border-radius: 999px;
      background: #fff4ea;
      color: var(--ttb-rust);
      font-weight: 900;
      text-decoration: none;
      white-space: nowrap;
    }

    .ttb-shortcode-wrap {
      padding: 18px;
      border-radius: 24px;
      background: #f8f3e8;
      border: 1px solid var(--ttb-border);
    }

    .ttb-shortcode-wrap .shop-page-wp-grid,
    .ttb-shortcode-wrap .spwp-grid,
    .ttb-shortcode-wrap .products {
      gap: 18px !important;
    }

    .ttb-shortcode-wrap img {
      border-radius: 18px;
    }

    .ttb-amazon-search {
      margin-top: clamp(38px, 7vw, 70px);
      padding: clamp(26px, 5vw, 48px);
      border-radius: 34px;
      background:
        radial-gradient(circle at top left, rgba(255, 122, 50, 0.22), transparent 34%),
        linear-gradient(135deg, #1d1712, #38251d);
      color: #fff;
      box-shadow: var(--ttb-shadow);
    }

    .ttb-amazon-grid {
      display: grid;
      grid-template-columns: minmax(0, 0.9fr) minmax(280px, 1.1fr);
      gap: clamp(20px, 4vw, 34px);
      align-items: center;
    }

    .ttb-amazon-search h2 {
      margin: 0;
      color: #fff;
      font-size: clamp(2.2rem, 6vw, 4.8rem);
      line-height: 0.9;
      letter-spacing: -0.06em;
      font-weight: 950;
    }

    .ttb-amazon-search p {
      margin: 16px 0 0;
      color: rgba(255,255,255,0.8);
    }

    .ttb-search-box {
      padding: 22px;
      border-radius: 26px;
      background: rgba(255,255,255,0.1);
      border: 1px solid rgba(255,255,255,0.16);
    }

    .ttb-search-box label {
      display: block;
      margin-bottom: 10px;
      color: rgba(255,255,255,0.84);
      font-weight: 900;
    }

    .ttb-search-row {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      gap: 10px;
    }

    .ttb-search-row input[type="text"] {
      width: 100%;
      min-height: 50px;
      padding: 12px 16px;
      border: 1px solid rgba(255,255,255,0.22);
      border-radius: 999px;
      background: #fff;
      color: var(--ttb-ink);
      font-size: 1rem;
      outline: none;
    }

    .ttb-search-row input[type="text"]:focus {
      border-color: var(--ttb-orange);
      box-shadow: 0 0 0 4px rgba(255,122,50,0.18);
    }

    .ttb-search-row button {
      min-height: 50px;
      padding: 12px 20px;
      border: 0;
      border-radius: 999px;
      background: var(--ttb-orange);
      color: #1d1712;
      font-weight: 950;
      cursor: pointer;
      white-space: nowrap;
    }

    .ttb-search-row button:hover {
      transform: translateY(-2px);
    }

    .ttb-search-note {
      margin-top: 12px !important;
      color: rgba(255,255,255,0.68) !important;
      font-size: 0.88rem;
    }

    .ttb-wide-panel {
      display: grid;
      grid-template-columns: minmax(0, 0.82fr) minmax(0, 1.18fr);
      gap: clamp(18px, 3vw, 34px);
      align-items: start;
      margin-top: clamp(34px, 6vw, 58px);
      padding: clamp(24px, 4vw, 40px);
      border-radius: 34px;
      background: #1d1712;
      color: #fff;
      box-shadow: var(--ttb-shadow);
    }

    .ttb-wide-panel h2 {
      margin: 0;
      color: #fff;
      font-size: clamp(2.2rem, 6vw, 4.8rem);
      line-height: 0.9;
      letter-spacing: -0.06em;
      font-weight: 950;
    }

    .ttb-wide-panel p {
      margin: 14px 0 0;
      color: rgba(255,255,255,0.78);
    }

    .ttb-checklist {
      display: grid;
      gap: 12px;
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .ttb-checklist li {
      display: grid;
      grid-template-columns: auto 1fr;
      gap: 12px;
      align-items: start;
      padding: 15px 16px;
      border: 1px solid rgba(255,255,255,0.12);
      border-radius: 18px;
      background: rgba(255,255,255,0.07);
      color: rgba(255,255,255,0.9);
    }

    .ttb-check {
      display: grid;
      place-items: center;
      width: 24px;
      height: 24px;
      border-radius: 999px;
      background: var(--ttb-orange);
      color: #1d1712;
      font-weight: 950;
      line-height: 1;
    }

    .ttb-note {
      margin-top: 18px;
      padding: 22px;
      border-left: 6px solid var(--ttb-orange);
      border-radius: 22px;
      background: #fff4ea;
      color: var(--ttb-soft-ink);
    }

    .ttb-note strong {
      color: var(--ttb-ink);
    }

    .ttb-cta {
      margin-top: clamp(36px, 6vw, 62px);
      padding: clamp(28px, 5vw, 54px);
      border-radius: 34px;
      background:
        radial-gradient(circle at top left, rgba(255,122,50,0.22), transparent 34%),
        linear-gradient(135deg, #fffdf8, #eef6f9);
      border: 1px solid var(--ttb-border);
      box-shadow: var(--ttb-shadow);
      text-align: center;
    }

    .ttb-cta p {
      max-width: 760px;
      margin: 18px auto 0;
      color: var(--ttb-soft-ink);
      font-size: 1.08rem;
    }

    .ttb-button-row {
      display: flex;
      justify-content: center;
      flex-wrap: wrap;
      gap: 12px;
      margin-top: 24px;
    }

    .ttb-button {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-height: 48px;
      padding: 13px 20px;
      border-radius: 999px;
      font-weight: 900;
      text-decoration: none;
      letter-spacing: 0.03em;
      transition: transform 0.2s ease, box-shadow 0.2s ease;
    }

    .ttb-button:hover {
      transform: translateY(-2px);
    }

    .ttb-button-primary {
      background: var(--ttb-rust);
      color: #fff;
      box-shadow: 0 12px 26px rgba(200,79,26,0.22);
    }

    .ttb-button-primary:hover {
      color: #fff;
    }

    .ttb-button-secondary {
      border: 1px solid var(--ttb-border);
      background: #fff;
      color: var(--ttb-ink);
    }

    .ttb-small-print {
      margin-top: 18px;
      color: var(--ttb-muted);
      font-size: 0.92rem;
    }

    @media (max-width: 1020px) {
      .ttb-category-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }
    }

    @media (max-width: 900px) {
      .ttb-products-hero,
      .ttb-amazon-grid,
      .ttb-wide-panel {
        grid-template-columns: 1fr;
      }

      .ttb-shop-head {
        grid-template-columns: 1fr;
      }

      .ttb-back-top {
        justify-self: start;
      }
    }

    @media (max-width: 620px) {
      .ttb-category-grid,
      .ttb-trust-strip {
        grid-template-columns: 1fr;
      }

      .ttb-search-row {
        grid-template-columns: 1fr;
      }

      .ttb-search-row button {
        width: 100%;
      }
    }

    @media (max-width: 560px) {
      .ttb-products-wrap {
        padding-left: 16px;
        padding-right: 16px;
      }

      .ttb-products-panel,
      .ttb-disclosure-card,
      .ttb-category-card,
      .ttb-shop-section,
      .ttb-amazon-search,
      .ttb-wide-panel,
      .ttb-cta {
        border-radius: 24px;
      }

      .ttb-products-panel h1,
      .ttb-section-head h2,
      .ttb-shop-head h2,
      .ttb-amazon-search h2,
      .ttb-wide-panel h2,
      .ttb-cta h2 {
        font-size: clamp(2.5rem, 13vw, 4rem);
      }

      .ttb-button-row .ttb-button {
        width: 100%;
      }
    }
  </style>

  <div id="top" class="ttb-products-wrap">

    <header class="ttb-products-hero">
      <div class="ttb-products-panel">
        <p class="ttb-products-kicker">Family travel products</p>
        <h1 id="ttb-products-title">
          Favourite finds.
          <span class="ttb-script-dark">Useful kit for real family adventures.</span>
        </h1>
        <p class="ttb-products-lede">
          Welcome to our family travel product hub. These are the bags, swimming bits, toys,
          gadgets and practical finds that can make travelling with children easier, calmer
          and slightly less like wrestling an octopus into a suitcase.
        </p>
        <p class="ttb-products-lede">
          We focus on things that solve real problems: packing chaos, long travel days,
          bored kids, wet swim gear, dead batteries and the eternal mystery of where the charger went.
        </p>
      </div>

      <aside class="ttb-disclosure-card">
        <h2>Affiliate note</h2>
        <p>
          Some links on this page may be affiliate links. As an Amazon Associate, we may earn from qualifying purchases.
          This does not cost you extra. We recommend products because they are useful for family travel,
          not because the internet threw a shiny commission carrot at us.
        </p>

        <div class="ttb-trust-strip">
          <span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Family-focused</span>
          <span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9f3.png" alt="🧳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Travel useful</span>
          <span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f50c.png" alt="🔌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Problem solving</span>
          <span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f49b.png" alt="💛" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Worth considering</span>
        </div>
      </aside>
    </header>

    <section class="ttb-section-head">
      <p class="ttb-section-kicker">Browse the shelves</p>
      <h2>
        Start with the problem.
        <span class="ttb-script-dark">Then find the kit.</span>
      </h2>
      <p class="ttb-lede">
        The best travel products are not always the flashiest. They are the ones that save space,
        keep kids entertained, dry things faster, charge things properly, or stop your bag becoming
        a portable junk drawer with handles.
      </p>
    </section>

    <nav class="ttb-category-grid" aria-label="Product categories">
      <article class="ttb-category-card">
        <div class="ttb-icon"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f392.png" alt="🎒" class="wp-smiley" style="height: 1em; max-height: 1em;" /></div>
        <h3>Rucksacks and bags</h3>
        <p>
          Travel bags, packing helpers and luggage bits that keep the family circus organised.
        </p>
        <a class="ttb-card-link" href="#rucksacks-bags">Discover more</a>
      </article>

      <article class="ttb-category-card">
        <div class="ttb-icon"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3ca.png" alt="🏊" class="wp-smiley" style="height: 1em; max-height: 1em;" /></div>
        <h3>Swimming products</h3>
        <p>
          Pool, beach and swim-day gear for splashy days, wet bags and sun-soaked chaos.
        </p>
        <a class="ttb-card-link" href="#swimming-products">Explore here</a>
      </article>

      <article class="ttb-category-card">
        <div class="ttb-icon"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9f8.png" alt="🧸" class="wp-smiley" style="height: 1em; max-height: 1em;" /></div>
        <h3>Toys</h3>
        <p>
          Small travel-friendly toys and activities for flights, restaurants, hotels and queues.
        </p>
        <a class="ttb-card-link" href="#travel-toys">Check it out</a>
      </article>

      <article class="ttb-category-card">
        <div class="ttb-icon"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f50c.png" alt="🔌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></div>
        <h3>Gadgets</h3>
        <p>
          Chargers, adapters, organisers and useful tech for keeping everyone powered and sane.
        </p>
        <a class="ttb-card-link" href="#travel-gadgets">Take a look</a>
      </article>
    </nav>

    <section class="ttb-wide-panel">
      <div>
        <h2>What makes the list?</h2>
        <p>
          We are not here to fill your house with travel tat. These are the rules for anything that earns
          a place in a family bag.
        </p>
      </div>

      <ul class="ttb-checklist">
        <li>
          <span class="ttb-check">✓</span>
          <span>It solves a real travel problem, not an imaginary one invented by a product photo.</span>
        </li>
        <li>
          <span class="ttb-check">✓</span>
          <span>It works for families, not just minimalist backpackers with one shirt and suspicious calm.</span>
        </li>
        <li>
          <span class="ttb-check">✓</span>
          <span>It earns the space it takes in the suitcase, car, backpack or cabin bag.</span>
        </li>
        <li>
          <span class="ttb-check">✓</span>
          <span>It helps on actual travel days, when everyone is tired, hungry and looking for the charger.</span>
        </li>
      </ul>
    </section>

    <section id="rucksacks-bags" class="ttb-shop-section">
      <div class="ttb-shop-head">
        <div>
          <p class="ttb-section-kicker">Category</p>
          <h2>Rucksacks and bags</h2>
          <p>
            Travel bags, packing cubes, organisers and storage helpers for keeping family luggage under control.
          </p>
        </div>
        <a class="ttb-back-top" href="#top">Back to top</a>
      </div>

      <div class="ttb-shortcode-wrap">
                <div class="shop-page-wp-grid">
			            <a target="_blank" href="https://amzn.to/4dR0zqd" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/04/Packing-Cubes-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Packing Cubes for Suitcase</h3>
            </div>
						                <div class="shop-page-wp-description">
								£25.99 at time of publish.
This versatile packing cube set is perfectly suited for carry-on suitcases, backpacks, duffel bags, totes, and weekenders.
You can also use this to store off season cloths at home too!                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Check out these Packing Cubes!</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/44YMWkt" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/04/Cable-tidy-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Travel Cable Organiser Bag</h3>
            </div>
						                <div class="shop-page-wp-description">
								£9.58 at time of publich.
This thoughtfully designed cable tidy bag is ideal for travellers seeking to streamline their packing. By consolidating cables, electronic devices, and even cosmetics in one convenient location.                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Take a look at the cable tidy bag!</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/3UZ1lIT" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/04/roll-up-storage-bags-1-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Roll Up Storage Bags for Travel</h3>
            </div>
						                <div class="shop-page-wp-description">
								£10.99 at time of publish.
There is no pump or vacuum required: When you're travelling and want the most space, these storage bags are perfect. No need for a hand pump or vacuum to use, just simply roll up and seal.                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Click to get your roll up bag here!</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/4bB8pT1" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/04/Osprey-Unisex-farpoin-65-1-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Osprey Europe Farpoint With Wheels</h3>
            </div>
						                <div class="shop-page-wp-description">
								The Osprey Farpoint 36 Wheeled travel pack carry-on gives the choice between energy-saving wheels or a capable backpack when the path forward gets rough!                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Link to Osprey Bag!</span>               </div>
						            </a>			        </div>
		
      </div>
    </section>

    <section id="swimming-products" class="ttb-shop-section">
      <div class="ttb-shop-head">
        <div>
          <p class="ttb-section-kicker">Category</p>
          <h2>Swimming</h2>
          <p>
            Pool and beach finds for swim days, wet clothes, splash play and sunny family adventures.
          </p>
        </div>
        <a class="ttb-back-top" href="#top">Back to top</a>
      </div>

      <div class="ttb-shortcode-wrap">
                <div class="shop-page-wp-grid">
			            <a target="_blank" href="https://amzn.to/3KiMcND" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/05/diving-toy-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Diving Sticks</h3>
            </div>
						                <div class="shop-page-wp-description">
								£15.00 at time of publishing.

Used these locally and on holiday and great fun for all the family in the pool. Good value for money and the sticks stand up on the floor of the pool so children can dive to collect them. Excellent for encouraging little ones to swim and play underwater.                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Get dive sticks here!</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/3wT7vCo" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/05/snorkling-mask-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Kids Snorkel Mask Full Face</h3>
            </div>
						                <div class="shop-page-wp-description">
								£26.99 at time of publishing.
We had a snorkel like this one, it is one of the most amazing swimming devices we have ever used. This Kids Snorkel Mask Full Face  is designed for 3-8 years old swimming beginners. 
It offers an anti-fog and dry top snorkel set with an easy breathing system, making it ideal for pool use and snorkelling adventures.                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Click here for snorkle!</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/3yCCgMo" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/05/zoggs-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Dual Buoyancy Arm Bands</h3>
            </div>
						                <div class="shop-page-wp-description">
								£18.52 at time of publishing.
Other parents approached us asking where these armbands were from. They are worth every single penny. They are clearly comfortable to wear and provide incredible buoyancy. We felt that our daughter was safe and happy with these on- they are a little bit more expensive than the basic arm bands but in our opinion, absolutely worth it.                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Get your armbands here!</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/4bRHnqk" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/04/Aquaspear-Swim-Vest-1-190x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Aquasphere Swim Vest</h3>
            </div>
						                <div class="shop-page-wp-description">
								£26.35 at time of publish.
This swimming vest is ideal for Swimming, we have found it amazing for Dotty, she feels safe, thanks to the strap positioned at the crotch it does not ride up and the handle on the back is a god send, we have not found another vest like this.                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Link to Swim Vest!</span>               </div>
						            </a>			        </div>
		
      </div>
    </section>

    <section id="travel-toys" class="ttb-shop-section">
      <div class="ttb-shop-head">
        <div>
          <p class="ttb-section-kicker">Category</p>
          <h2>Toys</h2>
          <p>
            Travel-friendly toys and activities for keeping little hands busy during long waits and longer journeys.
          </p>
        </div>
        <a class="ttb-back-top" href="#top">Back to top</a>
      </div>

      <div class="ttb-shortcode-wrap">
                <div class="shop-page-wp-grid">
			            <a target="_blank" href="https://amzn.to/3yCA73m" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/05/buckle-pillow-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Buckle Pillow</h3>
            </div>
						                <div class="shop-page-wp-description">
								£13.99 at time of publishing...
perfect for quiet playtime. it is an engaging way for toddlers to relax and improved focus. Made of fluffy polyester not hard foam                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Get your sensory pillow</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/3Vj7GjY" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/05/Spinners-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Suction Cup Spinner Toys</h3>
            </div>
						                <div class="shop-page-wp-description">
								£8.99 at time of publishing.
We have installed one on the window of the car to amuse the little one and stop them falling asleep. It certainly works and you’ll have a happy child, we also have in the bath but you can also stick on a plane tray etc.                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Buy Now</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/3X041c2" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/05/suction-toy-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Suction Cup Toys</h3>
            </div>
						                <div class="shop-page-wp-description">
								£13.98 at time of publishing. Stick these to the side of the bath and my little one loves to bat them and feel them and also have a little chew. Actually perfect for putting 1 or 2 in travel bag too for when out eating as they can stick to highchair                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Get your suction toy!</span>               </div>
						            </a>			        </div>
		
      </div>
    </section>

    <section id="travel-gadgets" class="ttb-shop-section">
      <div class="ttb-shop-head">
        <div>
          <p class="ttb-section-kicker">Category</p>
          <h2>Gadgets</h2>
          <p>
            Useful travel tech, charging kit and little gadget wins for keeping the journey moving.
          </p>
        </div>
        <a class="ttb-back-top" href="#top">Back to top</a>
      </div>

      <div class="ttb-shortcode-wrap">
                <div class="shop-page-wp-grid">
			            <a target="_blank" href="https://amzn.to/482ksYN" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/10/aleca-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>The Fire TV Stick 4K</h3>
            </div>
						                <div class="shop-page-wp-description">
								£39.99 at time of publishing.  The Fire TV Stick 4K has been a game-changer for us as we prep for our travels. It keeps the kids entertained with their favorite shows, and it’s super easy to set up on any TV. Plus, with Xbox Game Pass Ultimate, I’ve been sneaking in some gaming time after the kids are in bed no need for a console anymore, cloud gaming!                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Buy Now</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/4bBpono" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/07/bedguard-e1720364411413-200x250.jpeg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>BANBALOO Bed Bumper Guard</h3>
            </div>
						                <div class="shop-page-wp-description">
								£29.99 at the time of publishing.

BANBALOO Bed Bumper for Toddlers Inflatable Model Baby and Toddler Anti Fall Barrier Lightweight Bed Bumper for Twin Double Queen and King Size Bed Guard for Kids Bed Protector                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Get the Guard Here!</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/3UR3y96" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/05/AirTags-200x185.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Apple AirTag, Track your luggage!</h3>
            </div>
						                <div class="shop-page-wp-description">
								£28 at time of publishing, track and find your keys, wallet, luggage, backpack and more. 
Apple AirTags are small, lightweight tracking devices that can be used to track lost items. They are compatible with all iPhones and iPads running iOS 14.5 or later. AirTags use a combination of Bluetooth, Ultra Wideband, and the Find My network to help you locate your lost items.

To use an AirTag, simply attach it to the item you want to track and add it to the Find My app                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Get your Airtag!</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/4e18WQ3" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/05/dji-mini-2-se-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>DJI Mini 2 SE,  Mini Camera Drone</h3>
            </div>
						                <div class="shop-page-wp-description">
								£269.00 at time of plubshing. 
All our drone videos have been taken with this and its fantasitc. The camera on this thing is amazing, its so easy to fly and along with the crisp stabilization of the video feed creating magic moments is simple.
The only downside to having a pocket drone is the wind. But even with that being said it still flies pretty well in 10-15+mph winds.                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Check the drone out here!</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/3yK5kBy" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/05/noise-machine-186x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>White Noise Machine &#8211; Portable</h3>
            </div>
						                <div class="shop-page-wp-description">
								£24.99 at time of publishing, My first impression was how something so tiny could be effective. It really is small! It can fit in the palm of your hand and so is fantastic for travelling!

Although it’s tiny, that doesn’t compromise on the sound quality and it’s as good as our much larger white noise machine we have - it runs for hours off mains or a battery charger but overnight id plug it in!                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Get your white noise machine here!</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/3R4pdd7" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/05/torch-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Keyring Torch with Clip</h3>
            </div>
						                <div class="shop-page-wp-description">
								£18.99 at time of publishing. 
Keyring Torch, what can i saym this is a great little torch and very bright especially with both front lights turned on.
The tiny strobe light at front will no doubt have use at some point.
Holds charge well and is very robust.                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Buy Now</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/3R4RUGJ" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/05/instant-camera-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Instant Print Camera</h3>
            </div>
						                <div class="shop-page-wp-description">
								£38.88 at time of posing.
This is a fun instant print camera enables you to to take photos and print out instantly to the paper roll, it has no ink just thermal printer paper which is super cheap, Georgia thinks its amazing fun and so do we!                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Get the camera here!</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/3R1fap0" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/05/LCD-Writing-Tablet-8.5-Inch-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>LCD Writing Tablet, 8.5 Inch</h3>
            </div>
						                <div class="shop-page-wp-description">
								£6.99 at time of Publish!
This is a great toy for messy little artists! Very easy to use, and works absolutely fine. After a month of crazy use (its been stepped on, thrown at the wall and splashed with water) it’s still in a great condition. It comes with two packs of stickers, extra batteries and a little screw driver. I would say it’s a great value for money!                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">LCD Writing Tablet Link!</span>               </div>
						            </a>			            <a target="_blank" href="https://amzn.to/4bVmgn3" class="shop-page-wp-item spwp-has-link spwp-one-fourth" rel="nofollow">
            <div class="shop-page-wp-image">
                <img decoding="async" src="https://tantrummingtrailblazers.com/wp-content/uploads/2024/04/kindle-2-200x250.jpg" alt=""/>
            </div>
            <div class="shop-page-wp-title">
                <h3>Amazon Fire HD 8 Kids Pro tablet</h3>
            </div>
						                <div class="shop-page-wp-description">
								£149.99 at time of publishing.

This can give up to 13 hours of battery life, Easy-to-use parental controls so that children can request content from a digital shop where they can request games and books | you can approve purchases and downloads using the online Parent Dashboard.                </div>
												               <div class="shop-page-wp-link">
                <span class="buy-link">Check out this Kindle!</span>               </div>
						            </a>			        </div>
		
      </div>
    </section>

    <section class="ttb-amazon-search" aria-labelledby="ttb-amazon-title">
      <div class="ttb-amazon-grid">
        <div>
          <h2 id="ttb-amazon-title">
            Search Amazon.
            <span class="ttb-script-dark">Find your own travel fix.</span>
          </h2>
          <p>
            Looking for something specific? Use this search to look for family travel products,
            packing helpers, toys, swimming gear or gadgets.
          </p>
        </div>

        <form class="ttb-search-box" action="https://www.amazon.co.uk/s" method="get" target="_blank" rel="nofollow sponsored noopener">
          <label for="ttb-amazon-query">What are you looking for?</label>
          <div class="ttb-search-row">
            <input
              id="ttb-amazon-query"
              name="k"
              type="text"
              value="family travel essentials"
              placeholder="Search family travel products..."
              aria-label="Search Amazon for family travel products"
            >
            <input type="hidden" name="tag" value="tantrummingtra-21">
            <button type="submit">Search</button>
          </div>
          <p class="ttb-search-note">
            As an Amazon Associate, we may earn from qualifying purchases.
          </p>
        </form>
      </div>
    </section>

    <div class="ttb-note">
      <strong>Quick honest note:</strong>
      prices, availability, product details and age suitability can change. Always check the current product page,
      safety guidance, sizing, delivery location and airline rules before buying or packing anything for a trip.
    </div>

    <section class="ttb-cta">
      <h2>Family. Chaos. Better kit.</h2>
      <p>
        The right product will not make travel perfect. Nothing can. But it can make a journey easier,
        a bag calmer, a child happier, or a tired parent slightly less likely to mutter into a suitcase.
      </p>

      <div class="ttb-button-row">
        <a class="ttb-button ttb-button-primary" href="/travel-tips/">
          Read more travel tips
        </a>
        <a class="ttb-button ttb-button-secondary" href="/tools/make-my-drive-fun/">
          Plan a better family drive
        </a>
      </div>

      <p class="ttb-small-print">
        Built from real family travel, overpacked bags, missing chargers and the noble quest to make travel days easier.
      </p>
    </section>

  </div>

  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "CollectionPage",
    "name": "Family Travel Products",
    "description": "Family travel product recommendations including rucksacks and bags, swimming products, toys and gadgets for travelling with children.",
    "url": "https://tantrummingtrailblazers.com/favourite-finds/",
    "isPartOf": {
      "@type": "WebSite",
      "name": "Tantrumming Trailblazers",
      "url": "https://tantrummingtrailblazers.com/"
    },
    "about": [
      "Family travel products",
      "Rucksacks and bags",
      "Swimming products",
      "Travel toys",
      "Travel gadgets"
    ]
  }
  </script>
</section>
<p>The post <a href="https://tantrummingtrailblazers.com/favourite-finds/">🌟Favourite Finds</a> appeared first on <a href="https://tantrummingtrailblazers.com">Tantrumming Trailblazers</a>.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">122</post-id>	</item>
	</channel>
</rss>
