/*
 * ================================================================
 * まじん式スライド  v4.4b（書籍版）
 * 『秒速で仕事が片付く まじん式生成AI資料作成術』（KADOKAWA）専用
 * (c) Majincraft（マジクラ）/ まじん  - All rights reserved.
 *
 * ■ このコードについて
 *  本コードは、本書のために専用に作り直したものです。
 *  note で配布しているものとは中身が異なります。
 *  本書の手順で作業される場合は、必ず本書の書誌ページ（→6ページ）から
 *  ダウンロードしたファイルをお使いください。
 *
 * ■ ご利用の範囲
 *  ・本コードのライセンスは、本書1冊のご所有と一体です。1冊につき1名さまが
 *    ご利用いただけます。
 *  ・複数の方でお使いになる場合は、ご利用人数分の書籍のご購入が必要です。
 *  ・本書を売却・譲渡された場合、本コードをご利用いただく権利も失われます。
 *    以後、お手元のコードはご利用いただけません。
 *  ・法人・団体でのご利用は、ご利用人数分の書籍をご購入いただくか、
 *    マジクラ法人プランへのご加入が必要です。
 *      マジクラ（法人プラン）: https://note.com/majin_108/membership/join
 *
 * ■ ここに書かれていない事項
 *  本書および本ヘッダーに記載のない事項については、下記の記事内の
 *  利用に関する記述が適用されます。
 *      https://note.com/majin_108/n/n1f5577c4e02d
 * ================================================================
 */

function hexToRgb(hex) {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? {
    r: parseInt(result[1], 16),
    g: parseInt(result[2], 16),
    b: parseInt(result[3], 16)
  } : null;
}

function rgbToHsl(r, g, b) {
  r /= 255;
  g /= 255;
  b /= 255;
  const max = Math.max(r, g, b),
    min = Math.min(r, g, b);
  let h, s, l = (max + min) / 2;
  if (max === min) {
    h = s = 0;
  } else {
    const d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    switch (max) {
      case r:
        h = (g - b) / d + (g < b ? 6 : 0);
        break;
      case g:
        h = (b - r) / d + 2;
        break;
      case b:
        h = (r - g) / d + 4;
        break;
    }
    h /= 6;
  }
  return {
    h: h * 360,
    s: s * 100,
    l: l * 100
  };
}

function hslToHex(h, s, l) {
  s /= 100;
  l /= 100;
  const c = (1 - Math.abs(2 * l - 1)) * s,
    x = c * (1 - Math.abs((h / 60) % 2 - 1)),
    m = l - c / 2;
  let r = 0,
    g = 0,
    b = 0;
  if (0 <= h && h < 60) {
    r = c;
    g = x;
    b = 0;
  } else if (60 <= h && h < 120) {
    r = x;
    g = c;
    b = 0;
  } else if (120 <= h && h < 180) {
    r = 0;
    g = c;
    b = x;
  } else if (180 <= h && h < 240) {
    r = 0;
    g = x;
    b = c;
  } else if (240 <= h && h < 300) {
    r = x;
    g = 0;
    b = c;
  } else if (300 <= h && h < 360) {
    r = c;
    g = 0;
    b = x;
  }
  r = Math.round((r + m) * 255);
  g = Math.round((g + m) * 255);
  b = Math.round((b + m) * 255);
  return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
}

function generateTintedGray(tintColorHex, saturation, lightness) {
  const rgb = hexToRgb(tintColorHex);
  if (!rgb) return '#F8F9FA';
  const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
  return hslToHex(hsl.h, saturation, lightness);
}


function generatePyramidColors(baseColor, levels) {
  const colors = [];
  for (let i = 0; i < levels; i++) {
    
    const lightenAmount = (i / Math.max(1, levels - 1)) * 0.6;
    colors.push(lightenColor(baseColor, lightenAmount));
  }
  return colors;
}


function lightenColor(color, amount) {
  const rgb = hexToRgb(color);
  if (!rgb) return color;
  
  const lighten = (c) => Math.min(255, Math.round(c + (255 - c) * amount));
  const newR = lighten(rgb.r);
  const newG = lighten(rgb.g);
  const newB = lighten(rgb.b);
  
  return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
}


function darkenColor(color, amount) {
  const rgb = hexToRgb(color);
  if (!rgb) return color;
  
  const darken = (c) => Math.max(0, Math.round(c * (1 - amount)));
  const newR = darken(rgb.r);
  const newG = darken(rgb.g);
  const newB = darken(rgb.b);
  
  return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
}


function generateStepUpColors(baseColor, steps) {
  const colors = [];
  for (let i = 0; i < steps; i++) {
    
    const lightenAmount = 0.6 * (1 - (i / Math.max(1, steps - 1)));
    colors.push(lightenColor(baseColor, lightenAmount));
  }
  return colors;
}


function generateProcessColors(baseColor, steps) {
  const colors = [];
  for (let i = 0; i < steps; i++) {
    
    const lightenAmount = 0.5 * (1 - (i / Math.max(1, steps - 1)));
    colors.push(lightenColor(baseColor, lightenAmount));
  }
  return colors;
}


function generateTimelineCardColors(baseColor, milestones) {
  const colors = [];
  for (let i = 0; i < milestones; i++) {
    
    const lightenAmount = 0.4 * (1 - (i / Math.max(1, milestones - 1)));
    colors.push(lightenColor(baseColor, lightenAmount));
  }
  return colors;
}


function generateCompareColors(baseColor) {
  return {
    left: darkenColor(baseColor, 0.3),   
    right: baseColor                     
  };
}




const CONFIG = {
  BASE_PX: {
    W: 960,
    H: 540
  },
  BACKGROUND_IMAGES: {
    title: '',
    closing: '',
    section: '',
    main: ''
  },
  POS_PX: {
    titleSlide: {
      logo: {
        left: 55,
        top: 60,    
        width: 135
      },
      title: {
        left: 50,
        top: 200,
        width: 830,
        height: 90
      },
      date: {
        left: 50,
        top: 450,
        width: 250,
        height: 40
      }
    },
    contentSlide: {
      headerLogo: {
        right: 20,
        top: 20,
        width: 75
      },
      title: {
        left: 25,
        top: 20,
        width: 830,
        height: 65
      },
      titleUnderline: {
        left: 25,
        top: 80,
        width: 260,
        height: 4
      },
      subhead: {
        left: 25,
        top: 90,
        width: 910,
        height: 40
      },
      body: {
        left: 25,
        top: 132,
        width: 910,
        height: 330
      },
      twoColLeft: {
        left: 25,
        top: 132,
        width: 440,
        height: 330
      },
      twoColRight: {
        left: 495,
        top: 132,
        width: 440,
        height: 330
      }
    },
    compareSlide: {
      headerLogo: {
        right: 20,
        top: 20,
        width: 75
      },
      title: {
        left: 25,
        top: 20,
        width: 830,
        height: 65
      },
      titleUnderline: {
        left: 25,
        top: 80,
        width: 260,
        height: 4
      },
      subhead: {
        left: 25,
        top: 90,
        width: 910,
        height: 40
      },
      leftBox: {
        left: 25,
        top: 112,
        width: 445,
        height: 350
      },
      rightBox: {
        left: 490,
        top: 112,
        width: 445,
        height: 350
      }
    },
    processSlide: {
      headerLogo: {
        right: 20,
        top: 20,
        width: 75
      },
      title: {
        left: 25,
        top: 20,
        width: 830,
        height: 65
      },
      titleUnderline: {
        left: 25,
        top: 80,
        width: 260,
        height: 4
      },
      subhead: {
        left: 25,
        top: 90,
        width: 910,
        height: 40
      },
      area: {
        left: 25,
        top: 132,
        width: 910,
        height: 330
      }
    },
    timelineSlide: {
      headerLogo: {
        right: 20,
        top: 20,
        width: 75
      },
      title: {
        left: 25,
        top: 20,
        width: 830,
        height: 65
      },
      titleUnderline: {
        left: 25,
        top: 80,
        width: 260,
        height: 4
      },
      subhead: {
        left: 25,
        top: 90,
        width: 910,
        height: 40
      },
      area: {
        left: 25,
        top: 132,
        width: 910,
        height: 330
      }
    },
    diagramSlide: {
      headerLogo: {
        right: 20,
        top: 20,
        width: 75
      },
      title: {
        left: 25,
        top: 20,
        width: 830,
        height: 65
      },
      titleUnderline: {
        left: 25,
        top: 80,
        width: 260,
        height: 4
      },
      subhead: {
        left: 25,
        top: 90,
        width: 910,
        height: 40
      },
      lanesArea: {
        left: 25,
        top: 132,
        width: 910,
        height: 330
      }
    },
    cardsSlide: {
      headerLogo: {
        right: 20,
        top: 20,
        width: 75
      },
      title: {
        left: 25,
        top: 20,
        width: 830,
        height: 65
      },
      titleUnderline: {
        left: 25,
        top: 80,
        width: 260,
        height: 4
      },
      subhead: {
        left: 25,
        top: 90,
        width: 910,
        height: 40
      },
      gridArea: {
        left: 25,
        top: 120,
        width: 910,
        height: 340
      }
    },
    tableSlide: {
      headerLogo: {
        right: 20,
        top: 20,
        width: 75
      },
      title: {
        left: 25,
        top: 20,
        width: 830,
        height: 65
      },
      titleUnderline: {
        left: 25,
        top: 80,
        width: 260,
        height: 4
      },
      subhead: {
        left: 25,
        top: 90,
        width: 910,
        height: 40
      },
      area: {
        left: 25,
        top: 130,
        width: 910,
        height: 330
      }
    },
    progressSlide: {
      headerLogo: {
        right: 20,
        top: 20,
        width: 75
      },
      title: {
        left: 25,
        top: 20,
        width: 830,
        height: 65
      },
      titleUnderline: {
        left: 25,
        top: 80,
        width: 260,
        height: 4
      },
      subhead: {
        left: 25,
        top: 90,
        width: 910,
        height: 40
      },
      area: {
        left: 25,
        top: 132,
        width: 910,
        height: 330
      }
    },
    quoteSlide: {
      headerLogo: {
        right: 20,
        top: 20,
        width: 75
      },
      title: {
        left: 25,
        top: 20,
        width: 830,
        height: 65
      },
      titleUnderline: {
        left: 25,
        top: 80,
        width: 260,
        height: 4
      },
      subhead: {
        left: 25,
        top: 100,
        width: 910,
        height: 40
      }
    },
    kpiSlide: {
      headerLogo: {
        right: 20,
        top: 20,
        width: 75
      },
      title: {
        left: 25,
        top: 20,
        width: 830,
        height: 65
      },
      titleUnderline: {
        left: 25,
        top: 80,
        width: 260,
        height: 4
      },
      subhead: {
        left: 25,
        top: 90,
        width: 910,
        height: 40
      },
      gridArea: {
        left: 25,
        top: 132,
        width: 910,
        height: 330
      }
    },
    triangleSlide: {
      headerLogo: {
        right: 20,
        top: 20,
        width: 75
      },
      title: {
        left: 25,
        top: 20,
        width: 830,
        height: 65
      },
      titleUnderline: {
        left: 25,
        top: 80,
        width: 260,
        height: 4
      },
      subhead: {
        left: 25,
        top: 90,
        width: 910,
        height: 40
      },
      area: {
        left: 25,
        top: 110,
        width: 910,
        height: 350
      }
    },
    flowChartSlide: {
      headerLogo: {
        right: 20,
        top: 20,
        width: 75
      },
      title: {
        left: 25,
        top: 20,
        width: 830,
        height: 65
      },
      titleUnderline: {
        left: 25,
        top: 80,
        width: 260,
        height: 4
      },
      subhead: {
        left: 25,
        top: 90,
        width: 910,
        height: 40
      },
      singleRow: {
        left: 25,
        top: 160,
        width: 910,
        height: 180
      },
      upperRow: {
        left: 25,
        top: 150,
        width: 910,
        height: 120
      },
      lowerRow: {
        left: 25,
        top: 290,
        width: 910,
        height: 120
      }
    },
    stepUpSlide: {
      headerLogo: {
        right: 20,
        top: 20,
        width: 75
      },
      title: {
        left: 25,
        top: 20,
        width: 830,
        height: 65
      },
      titleUnderline: {
        left: 25,
        top: 80,
        width: 260,
        height: 4
      },
      subhead: {
        left: 25,
        top: 90,
        width: 910,
        height: 40
      },
      stepArea: {
        left: 25,
        top: 130,
        width: 910,
        height: 330
      }
    },
    imageTextSlide: {
      headerLogo: {
        right: 20,
        top: 20,
        width: 75
      },
      title: {
        left: 25,
        top: 20,
        width: 830,
        height: 65
      },
      titleUnderline: {
        left: 25,
        top: 80,
        width: 260,
        height: 4
      },
      subhead: {
        left: 25,
        top: 90,
        width: 910,
        height: 40
      },
      leftImage: {
        left: 25,
        top: 140,
        width: 440,
        height: 310  
      },
      leftImageCaption: {
        left: 25,
        top: 470,
        width: 440,
        height: 30
      },
      rightText: {
        left: 485,
        top: 140,
        width: 450,
        height: 310
      },
      leftText: {
        left: 25,
        top: 140,
        width: 450,
        height: 310
      },
      rightImage: {
        left: 495,
        top: 140,
        width: 440,
        height: 310  
      },
      rightImageCaption: {
        left: 495,
        top: 430,
        width: 470,
        height: 30
      }
    },
      pyramidSlide: {
        headerLogo: {
          right: 20,
          top: 20,
          width: 75
        },
        title: {
          left: 25,
          top: 20,
          width: 830,
          height: 65
        },
        titleUnderline: {
          left: 25,
          top: 80,
          width: 260,
          height: 4
        },
        subhead: {
          left: 25,
          top: 90,
          width: 910,
          height: 40
        },
        pyramidArea: {
          left: 25,
          top: 120,
          width: 910,
          height: 360
        }
      },
    sectionSlide: {
      title: {
        left: 55,
        top: 230,
        width: 840,
        height: 80
      },
      ghostNum: {
        left: 35,
        top: 120,
        width: 400,
        height: 200
      }
    },
    footer: {
      leftText: {
        left: 15,
        top: 511,
        width: 250,
        height: 20
      },
      creditImage: {
        right: 55, 
        top: 518,  
        width: 150, 
        height: 10  
      },
      rightPage: {
        right: 15,
        top: 511,
        width: 50,
        height: 20
      }
    },
    bottomBar: {
      left: 0,
      top: 534,
      width: 960,
      height: 6
    }
  },
  FONTS: {
    family: 'Noto Sans JP',
    sizes: {
      title: 41,
      date: 16,
      sectionTitle: 38,
      contentTitle: 24,
      subhead: 16,
      body: 14,
      footer: 9,
      chip: 11,
      laneTitle: 13,
      small: 10,
      processStep: 14,
      axis: 12,
      ghostNum: 180
    }
  },
  COLORS: {
    primary_color: '#4285F4',
    text_primary: '#333333',
    text_small_font: '#1F2937',
    background_white: '#FFFFFF',
    card_bg: '#f6e9f0',
    background_gray: '',
    faint_gray: '',
    ghost_gray: '',
    table_header_bg: '',
    lane_border: '',
    card_border: '',
    neutral_gray: '',
    process_arrow: ''
  },
  DIAGRAM: {
    laneGap_px: 24,
    lanePad_px: 10,
    laneTitle_h_px: 30,
    cardGap_px: 12,
    cardMin_h_px: 48,
    cardMax_h_px: 70,
    arrow_h_px: 10,
    arrowGap_px: 8
  },
  LOGOS: {
    header: '',
    closing: ''
  },
  FOOTER_TEXT: `© ${new Date().getFullYear()} Your Company`
};






const APP_VERSION = 'v4.4'; 
const APP_TITLE = 'まじん式 ' + APP_VERSION;

function doGet(e) {
  
  
  if (e && e.parameter && e.parameter.e2e === 'run') {
    return handleE2ERun(e);
  }
  
  
  if (e && e.parameter && e.parameter.pptxconv) {
    return ContentService.createTextOutput(JSON.stringify(testConvertDrivePptx(e.parameter.pptxconv, !!(e.parameter.keep))))
      .setMimeType(ContentService.MimeType.JSON);
  }
  
  if (e && e.parameter && e.parameter.mastertest) {
    return ContentService.createTextOutput(JSON.stringify(testStandardMasterCopy()))
      .setMimeType(ContentService.MimeType.JSON);
  }
  
  if (e && e.parameter && e.parameter.deckmaster) {
    var __dm = { ok: false };
    try {
      var __p = SlidesApp.openById(e.parameter.deckmaster);
      __dm.masters = __p.getMasters().length;
      __dm.layouts = __p.getLayouts().length;
      __dm.layoutNames = __p.getLayouts().map(function (l) { try { return l.getLayoutName(); } catch (e2) { return '?'; } });
      
      try {
        if (typeof Slides !== 'undefined' && Slides && Slides.Presentations) {
          var __rp = Slides.Presentations.get(e.parameter.deckmaster);
          __dm.masterNames = (__rp.masters || []).map(function (m) { return (m.masterProperties && m.masterProperties.displayName) || m.objectId; });
        }
      } catch (eMN) { __dm.masterNamesErr = (eMN && eMN.message) || String(eMN); }
      __dm.slides = __p.getSlides().length;
      __dm.elementsPerSlide = __p.getSlides().map(function (s) { return s.getPageElements().length; });
      __dm.ok = true;
    } catch (e3) { __dm.error = (e3 && e3.message) || String(e3); }
    return ContentService.createTextOutput(JSON.stringify(__dm)).setMimeType(ContentService.MimeType.JSON);
  }
  
  if (e && e.parameter && e.parameter.newslideprobe) {
    return ContentService.createTextOutput(JSON.stringify(probeNewSlideMaster()))
      .setMimeType(ContentService.MimeType.JSON);
  }
  
  if (e && e.parameter && e.parameter.setupseed) {
    return ContentService.createTextOutput(JSON.stringify(setupNoTemplateSeedDeck()))
      .setMimeType(ContentService.MimeType.JSON);
  }
  
  
  if (e && e.parameter && e.parameter.tplwarm) {
    var __tw = { ok: false };
    try {
      var __twId = (typeof _extractTemplateIdNative === 'function') ? _extractTemplateIdNative(e.parameter.tplwarm) : null;
      
      
      if (__twId && e.parameter.metaonly) {
        PropertiesService.getScriptProperties().deleteProperty('majin_tplcache_' + __twId);
        __tw.metaCleared = true;
      }
      
      if (__twId && e.parameter.list && typeof _findTplCacheFilesInDrive_ === 'function') {
        try { __tw.driveFiles = _findTplCacheFilesInDrive_(__twId); } catch (eLs) { __tw.driveFilesError = (eLs && eLs.message) || String(eLs); }
      }
      if (__twId && e.parameter.force) {
        var __twMeta = (typeof _readTplCacheMeta_ === 'function') ? _readTplCacheMeta_(__twId) : null;
        if (__twMeta && __twMeta.fileId && typeof _deleteDriveFileHard_ === 'function') _deleteDriveFileHard_(__twMeta.fileId);
        PropertiesService.getScriptProperties().deleteProperty('majin_tplcache_' + __twId);
        __tw.forced = true;
      }
      
      if (e.parameter.deleteonly) { __tw.ok = true; __tw.status = 'deleted'; return ContentService.createTextOutput(JSON.stringify(__tw)).setMimeType(ContentService.MimeType.JSON); }
      if (e.parameter.listonly) { __tw.ok = true; __tw.status = 'listed'; return ContentService.createTextOutput(JSON.stringify(__tw)).setMimeType(ContentService.MimeType.JSON); }
      __tw = Object.assign(__tw, warmTemplateCache(e.parameter.tplwarm));
      try {
        if (__tw.cacheFileId) { var __cf = DriveApp.getFileById(__tw.cacheFileId); __tw.cacheInTrash = __cf.isTrashed(); __tw.cacheKB = Math.round(__cf.getSize() / 1024); }
      } catch (eCf) { __tw.cacheFileError = (eCf && eCf.message) || String(eCf); }
    } catch (eTw) { __tw.error = (eTw && eTw.message) || String(eTw); }
    return ContentService.createTextOutput(JSON.stringify(__tw)).setMimeType(ContentService.MimeType.JSON);
  }
  
  if (e && e.parameter && e.parameter.seeddiag) {
    var __sd = { ok: false };
    try {
      var __sid = PropertiesService.getScriptProperties().getProperty('noTemplateSeedDeckId') || '';
      __sd.seedDeckId = __sid;
      __sd.openable = false;
      if (__sid) {
        try {
          var __sdeck = SlidesApp.openById(__sid);
          __sd.openable = true;
          __sd.masters = __sdeck.getMasters().length;
          __sd.layouts = __sdeck.getLayouts().length;
          __sd.slides = __sdeck.getSlides().length;
          if (typeof Slides !== 'undefined' && Slides && Slides.Presentations) {
            var __srp = Slides.Presentations.get(__sid);
            __sd.masterDisplayNames = (__srp.masters || []).map(function (m) { return (m.masterProperties && m.masterProperties.displayName) || m.objectId; });
          }
        } catch (eOpen2) { __sd.openError = (eOpen2 && eOpen2.message) || String(eOpen2); }
      }
      __sd.ok = true;
    } catch (eD) { __sd.error = (eD && eD.message) || String(eD); }
    return ContentService.createTextOutput(JSON.stringify(__sd)).setMimeType(ContentService.MimeType.JSON);
  }
  
  if (e && e.parameter && e.parameter.tplprobe) {
    return ContentService.createTextOutput(JSON.stringify(probeTemplateOoxml(e.parameter.tplprobe)))
      .setMimeType(ContentService.MimeType.JSON);
  }
  
  if (e && e.parameter && e.parameter.tpltest) {
    return ContentService.createTextOutput(JSON.stringify(tplTestNativeOnTemplate(e.parameter.tpltest, !!(e.parameter.keep))))
      .setMimeType(ContentService.MimeType.JSON);
  }
  
  if (e && e.parameter && e.parameter.e2e === 'perf') {
    return ContentService.createTextOutput(JSON.stringify({
      perf: PropertiesService.getScriptProperties().getProperty('LAST_PERF_LOG') || '',
      lastError: PropertiesService.getScriptProperties().getProperty('LAST_ERROR') || '',
      lastRestError: PropertiesService.getScriptProperties().getProperty('LAST_REST_ERROR') || '',
      lastNativeError: PropertiesService.getScriptProperties().getProperty('LAST_NATIVE_ERROR') || '',
      lastFastpathError: PropertiesService.getScriptProperties().getProperty('LAST_FASTPATH_ERROR') || ''
    })).setMimeType(ContentService.MimeType.JSON);
  }
  const activationData = checkUserActivation();
  const htmlTemplate = HtmlService.createTemplateFromFile('無題.html');
  const baseSettings = loadSettings();
  
  htmlTemplate.settings = baseSettings;
  htmlTemplate.activationData = activationData;
  
  
  htmlTemplate.appVersion = APP_VERSION;

  
  htmlTemplate.devMode = !!(e && e.parameter && (e.parameter.dev || e.parameter.develop));

  
  return htmlTemplate.evaluate()
    .setTitle(APP_TITLE) 
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.DEFAULT);
}


function handleE2ERun(e) {
  const out = { ok: false, ts: new Date().toISOString() };
  try {
    const settings = loadSettings();
    try { PropertiesService.getScriptProperties().deleteProperty('LAST_REST_FLUSH_FALLBACK'); } catch (_) {} 
    
    
    const P = (e && e.parameter) || {};
    const _b = (v) => (v === '1' || v === 'true');
    if (P.rest !== undefined)    settings.restTextMode    = _b(P.rest);
    if (P.decor !== undefined)   settings.nativeDecorMode  = _b(P.decor);
    if (P.section !== undefined) settings.nativeSectionMode = _b(P.section);
    if (P.chart !== undefined)   settings.nativeChartMode  = _b(P.chart);
    
    
    if (P.single !== undefined)  settings.nativeSingleMaster = _b(P.single);
    if (P.themename !== undefined && String(P.themename).trim()) settings.nativeThemeName = String(P.themename).trim();
    
    if (P.nativetpl !== undefined) settings.nativeTemplateMode = _b(P.nativetpl);
    if (P.tplurl !== undefined && String(P.tplurl).trim()) settings.templateUrl = String(P.tplurl).trim();
    
    
    if (P.logo !== undefined) settings.headerLogoUrl = String(P.logo);
    
    if (P.gtop !== undefined && !isNaN(parseFloat(P.gtop))) settings.ghostTopAlpha = parseFloat(P.gtop);
    if (P.gbot !== undefined && !isNaN(parseFloat(P.gbot))) settings.ghostBottomAlpha = parseFloat(P.gbot);
    
    __FORCE_REST_FAIL = (P.flushfail !== undefined) ? _b(P.flushfail) : false;
    out.flags = { restTextMode: !!settings.restTextMode, nativeDecorMode: !!settings.nativeDecorMode, nativeSectionMode: !!settings.nativeSectionMode, nativeChartMode: !!settings.nativeChartMode, nativeSingleMaster: !!settings.nativeSingleMaster, nativeTemplateMode: !!settings.nativeTemplateMode, templateUrl: settings.templateUrl || '', forceRestFail: __FORCE_REST_FAIL };
    
    if (!(e && e.parameter && e.parameter.nocredit)) {
      settings.creditText = 'Change the world. majin';
    }
    const slideDataString = buildE2ETestData();
    
    
    const __e2ePid = (e && e.parameter && e.parameter.deckId) ? e.parameter.deckId : null;
    const url = generateSlidesFromWebApp(slideDataString, settings, __e2ePid, 'update');
    out.ok = true;
    out.url = url;
    out.inputDeckId = __e2ePid;
    out.sameId = __e2ePid ? (String(url).indexOf(__e2ePid) >= 0) : null;
    out.perf = PropertiesService.getScriptProperties().getProperty('LAST_PERF_LOG') || '';
    out.lastFallback = PropertiesService.getScriptProperties().getProperty('LAST_REST_FLUSH_FALLBACK') || ''; 
    
    if (!(e && e.parameter && e.parameter.keep)) {
      try {
        const m = url.match(/\/d\/([-\w]{25,})/) || url.match(/[-\w]{25,}/);
        const fileId = m ? (m[1] || m[0]) : null;
        if (fileId) DriveApp.getFileById(fileId).setTrashed(true);
        out.trashed = !!fileId;
      } catch (cleanupErr) {
        out.cleanupError = String(cleanupErr && cleanupErr.message || cleanupErr);
      }
    }
  } catch (err) {
    out.error = String(err && err.message || err);
    out.perf = PropertiesService.getScriptProperties().getProperty('LAST_PERF_LOG') || '';
    out.lastError = PropertiesService.getScriptProperties().getProperty('LAST_ERROR') || '';
    out.lastRestError = PropertiesService.getScriptProperties().getProperty('LAST_REST_ERROR') || '';
  }
  __FORCE_REST_FAIL = false; 
  return ContentService.createTextOutput(JSON.stringify(out)).setMimeType(ContentService.MimeType.JSON);
}



const E2E_IMAGE_B64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAA0ElEQVR42u3RMREAAAjEMPzrYUYEhsDHX4YaaKpnT7mVCYAFWIAFWIAFWIABC7AAC7AAC7AAAxZgARZgARZgARZgwAIswAIswAIswIAFWIAFWIAFWIABC7AAC7AAC7AACzBgARZgARZgARZgwAIswAIswAIswIBNACzAAizAAizAAgxYgAVYgAVYgAUYsAALsAALsAALsAADFmABFmABFmABBizAAizAAizAAgxYgAVYgAVYgAVYgAELsAALsAALsAADFmABFmABFmABBmxCcg/WxoN62krevAAAAABJRU5ErkJggg==';
function buildE2ETestData() {
  const ghost = E2E_IMAGE_B64;
  const data = [
    { type: 'title', title: 'パフォーマンステスト用スライド', date: '2026.06.15' },
    { type: 'agenda', title: '本日のアジェンダ', items: ['市場の現状分析', '主要KPIの確認', '競合との比較', '成長戦略の提案'] },
    { type: 'section', title: '市場の現状分析', sectionNo: 1, ghostImageBase64: ghost },
    { type: 'kpi', title: '主要指標サマリー', columns: 3, items: [{ label: '売上高', value: '1.2億円', change: '+15%', status: 'good' }, { label: '解約率', value: '3.2%', change: '-0.5%', status: 'good' }, { label: '新規顧客', value: '320件', change: '+8%', status: 'good' }] },
    { type: 'triangle', title: '成功の3要素', items: [{ title: 'スピード', desc: '迅速な意思決定' }, { title: '品質', desc: '高い**完成度**' }, { title: 'コスト', desc: '効率的な運用' }] },
    { type: 'section', title: '主要KPIの確認', sectionNo: 2, ghostImageBase64: ghost },
    { type: 'statsCompare', title: '前年比較', leftTitle: '昨年度', rightTitle: '今年度', stats: [{ label: '売上', leftValue: '1.0億', rightValue: '1.2億', trend: 'up' }, { label: '利益[[率]]', leftValue: '12%', rightValue: '18%', trend: 'up' }, { label: '顧客数', leftValue: '280', rightValue: '320', trend: 'up' }] },
    { type: 'compare', title: '自社と競合の比較', leftTitle: '自社', rightTitle: '競合A', leftItems: ['24時間サポート', '低価格プラン', '柔軟なカスタマイズ'], rightItems: ['実績豊富', '大規模対応', 'ブランド力'] },
    { type: 'section', title: '成長戦略の提案', sectionNo: 3, ghostImageBase64: ghost },
    { type: 'stepUp', title: '成長のステップ', items: [{ title: '基盤構築（準備）', desc: '体制を整える' }, { title: '拡大', desc: '市場を広げる' }, { title: '最適化(改善)', desc: '効率を高める' }] },
    { type: 'flowChart', title: '処理の流れ', steps: ['受付', '審査と[[承認]]', '処理', '完了'] },
    { type: 'content', title: '本文(1カラム)', points: ['市場が**拡大**している', '需要は[[二倍]]に成長見込み', '競争環境も激化する'] },
    { type: 'content', title: '本文(2カラム)', columns: [['強み: 低コスト', '強み: 高速対応'], ['課題: 認知度', '課題: [[体制]]整備']] },
    { type: 'headerCards', title: '重点施策', columns: 3, items: [{ title: '営業強化', desc: '新規開拓を加速' }, { title: '製品改善', desc: '機能を拡充' }, { title: '顧客満足', desc: 'サポート充実' }] },
    { type: 'bulletCards', title: '今後の展望', items: [{ title: '短期', desc: '売上1.5億円達成' }, { title: '中期', desc: '市場シェア拡大' }, { title: '長期', desc: '業界リーダーへ' }] },
    { type: 'cards', title: '主要サービス', columns: 3, items: [{ title: '分析', desc: 'データを可視化する' }, { title: '分析と[[予測]]', desc: 'データを**可視化**する' }, { title: '実行', desc: 'PDCAを高速で回す' }] },
    { type: 'progress', title: 'プロジェクト進捗', items: [{ label: '設計フェーズ', percent: 100 }, { label: '開発と[[テスト]]', percent: 65 }, { label: 'リリース準備', percent: 20 }] },
    { type: 'process', title: '導入プロセス', steps: ['要件をヒアリングする', '設計と[[試作]]を行う', '本番環境へ展開する'] },
    { type: 'cycle', title: '改善サイクル', centerText: 'PDCA', items: [{ subLabel: 'Plan', label: '計画する' }, { subLabel: 'Do', label: '実行する' }, { subLabel: 'Check', label: '評価する' }, { subLabel: 'Act', label: '改善する' }] },
    { type: 'diagram', title: '処理フロー', lanes: [{ title: '入力', items: ['データ受信', 'バリデーション', '正規化'] }, { title: '処理', items: ['集計', '判定'] }, { title: '出力', items: ['レポート生成', '通知', '保存'] }] },
    { type: 'pyramid', title: '価値の階層', levels: [{ title: 'ビジョン', description: '目指す姿' }, { title: '戦略', description: '実現の[[道筋]]' }, { title: '戦術', description: '具体的な施策' }, { title: '実行', description: '日々の**行動**' }] },
    { type: 'timeline', title: 'ロードマップ', milestones: [{ date: '2024 (Q1)', label: '企画開始' }, { date: '2024（Q3）', label: 'β版をリリースし初期顧客を獲得する取り組みを本格化' }, { date: '2025 Q1', label: '正式版' }, { date: '2025 Q4', label: '海外展開を視野に拡大' }] },
    { type: 'imageText', title: 'SVG図形(編集可能)', imagePosition: 'right', imageCaption: 'サンプルSVG図(caption付)', points: ['この図はSVGコードを', 'PNGではなく編集可能な', 'ネイティブ図形として配置'], image: { info: 'svg_code', data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', nativeSvg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 130"><rect x="6" y="6" width="188" height="118" fill="#E6F1FB" stroke="#378ADD" stroke-width="2"/><circle cx="55" cy="60" r="28" fill="#D85A30"/><rect x="105" y="32" width="80" height="56" rx="6" fill="#1D9E75"/><polyline points="20,110 70,80 120,95 180,55" fill="none" stroke="#534AB7" stroke-width="3"/><text x="14" y="120" font-size="13" fill="#042C53">編集可能SVG</text></svg>' } },
    { type: 'imageText', title: 'グラフ(凡例/タイトル/影)', imagePosition: 'left', points: ['タイトル中央寄せ', '凡例チップと文字の上下整合', 'チップに影(feDropShadow)'], image: { info: 'chart', data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', nativeSvg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 360"><defs><linearGradient id="barg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#616ad8"/><stop offset="100%" stop-color="#5059d1"/></linearGradient><filter id="shadow"><feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2"/></filter></defs><style>@import url(\'https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&amp;display=swap\');.bar{fill:url(#barg)}.axis{stroke:#999999;stroke-width:1}.ctitle{font-size:22px;fill:#5f6368;text-anchor:middle;font-weight:500}.clabel{font-size:13px;fill:#5f6368;text-anchor:middle}.legend-item{font-size:13px;fill:#202124}</style><text class="ctitle" x="300" y="34">名目GDPと消費者物価指数の推移</text><g transform="translate(70,70)"><rect x="0" y="-6" width="12" height="12" rx="2" fill="#616ad8" filter="url(#shadow)"/><text x="18" y="0" class="legend-item" dominant-baseline="middle">名目GDP</text></g><g transform="translate(420,70)"><line x1="0" y1="0" x2="-12" y2="0" stroke="#e68a9c" stroke-width="2"/><circle cx="-6" cy="0" r="3" fill="#e68a9c" stroke="white" stroke-width="1"/><text x="-20" y="0" class="legend-item" dominant-baseline="middle" text-anchor="end">消費者物価指数</text></g><line class="axis" x1="50" y1="300" x2="560" y2="300"/><rect class="bar" x="90" y="170" width="60" height="130" filter="url(#shadow)"/><rect class="bar" x="200" y="150" width="60" height="150" filter="url(#shadow)"/><rect class="bar" x="310" y="120" width="60" height="180" filter="url(#shadow)"/><rect class="bar" x="420" y="100" width="60" height="200" filter="url(#shadow)"/><text class="clabel" x="120" y="320">2021</text><text class="clabel" x="230" y="320">2022</text><text class="clabel" x="340" y="320">2023</text><text class="clabel" x="450" y="320">2024</text></svg>' } },
    { type: 'quote', title: '私たちの信念', text: 'クオリティとスピードは両立できる。', author: 'majin' },
    { type: 'closing' }
  ];
  return JSON.stringify(data);
}

function saveSettings(settings) {
  try {
    const storableSettings = Object.assign({}, settings);
    
    storableSettings.showTitleUnderline = String(storableSettings.showTitleUnderline);
    storableSettings.showBottomBar = String(storableSettings.showBottomBar);
    storableSettings.showDateColumn = String(storableSettings.showDateColumn);
    storableSettings.enableGradient = String(storableSettings.enableGradient);
    storableSettings.showPageNumber = String(storableSettings.showPageNumber);
    
    if (storableSettings.nativeSectionMode !== undefined) {
      storableSettings.nativeSectionMode = String(storableSettings.nativeSectionMode);
    }
    if (storableSettings.nativeDecorMode !== undefined) {
      storableSettings.nativeDecorMode = String(storableSettings.nativeDecorMode);
    }
    
    if (storableSettings.restTextMode !== undefined) {
      storableSettings.restTextMode = String(storableSettings.restTextMode);
    }
    
    if (storableSettings.nativeTemplateMode !== undefined) {
      storableSettings.nativeTemplateMode = String(storableSettings.nativeTemplateMode);
    }
    
    if (storableSettings.ghostTopAlpha !== undefined) {
      storableSettings.ghostTopAlpha = String(storableSettings.ghostTopAlpha);
    }
    if (storableSettings.ghostBottomAlpha !== undefined) {
      storableSettings.ghostBottomAlpha = String(storableSettings.ghostBottomAlpha);
    }


    

    PropertiesService.getUserProperties().setProperties(storableSettings, false);
    return {
      status: 'success',
      message: '設定を保存しました。'
    };
  } catch (e) {
    Logger.log(`設定の保存エラー: ${e.message}`);
    return {
      status: 'error',
      message: `設定の保存中にエラーが発生しました: ${e.message}`
    };
  }
}

function saveSelectedPreset(presetName) {
  try {
    PropertiesService.getUserProperties().setProperty('selectedPreset', presetName);
    return {
      status: 'success',
      message: 'プリセット選択を保存しました。'
    };
  } catch (e) {
    Logger.log(`プリセット保存エラー: ${e.message}`);
    return {
      status: 'error',
      message: `プリセットの保存中にエラーが発生しました: ${e.message}`
    };
  }
}

function loadSettings() {
  const properties = PropertiesService.getUserProperties().getProperties();
  const scriptProperties = PropertiesService.getScriptProperties().getProperties();
  const DEFAULT_DRIVE_URL = "https://drive.google.com/drive/my-drive";
  const driveFolderUrl = (properties.driveFolderUrl === undefined || properties.driveFolderUrl === "") ?
    DEFAULT_DRIVE_URL : properties.driveFolderUrl;

  return {
    primaryColor: properties.primaryColor || '#4285F4',
    
    largeFontColor: properties.largeFontColor || '#333333', 
    smallFontColor: properties.smallFontColor || '#1F2937', 
    backgroundColor: properties.backgroundColor || '#FFFFFF', 
    
    gradientStart: properties.gradientStart || '#4285F4',
    gradientEnd: properties.gradientEnd || '#ff52df',
    fontFamily: properties.fontFamily || 'Noto Sans JP',
    showTitleUnderline: properties.showTitleUnderline === 'false' ? false : true,
    showBottomBar: properties.showBottomBar === 'false' ? false : true,
    showDateColumn: properties.showDateColumn === 'false' ? false : true,
    showPageNumber: properties.showPageNumber === 'false' ? false : true,
    enableGradient: properties.enableGradient === 'false' ? false : true,
    
    nativeSectionMode: properties.nativeSectionMode === 'false' ? false : true,
    
    nativeDecorMode: properties.nativeDecorMode === 'false' ? false : true,
    
    restTextMode: properties.restTextMode === 'false' ? false : true,
    
    nativeChartMode: properties.nativeChartMode === 'false' ? false : true,
    
    
    nativeSingleMaster: properties.nativeSingleMaster === 'false' ? false : true,
    
    nativeTemplateMode: properties.nativeTemplateMode === 'true' ? true : false,
    
    ghostTopAlpha: (properties.ghostTopAlpha !== undefined && !isNaN(parseFloat(properties.ghostTopAlpha))) ? parseFloat(properties.ghostTopAlpha) : 0.30,
    ghostBottomAlpha: (properties.ghostBottomAlpha !== undefined && !isNaN(parseFloat(properties.ghostBottomAlpha))) ? parseFloat(properties.ghostBottomAlpha) : 0.10,
    
    useNotebookLmSplit: properties.useNotebookLmSplit === 'false' ? false : true,
    
    newVideoLogic: properties.newVideoLogic === 'false' ? false : true,
    
    animationFormat: (properties.animationFormat === 'gif') ? 'gif' : 'apng',
    footerText: (properties.footerText === undefined) ? '© Google Inc.' : properties.footerText,
  headerLogoUrl: (properties.headerLogoUrl === undefined) ? 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8a/Google_Gemini_logo.svg/1280px-Google_Gemini_logo.svg.png' : properties.headerLogoUrl,
    closingLogoUrl: (properties.closingLogoUrl === undefined) ? 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8a/Google_Gemini_logo.svg/1280px-Google_Gemini_logo.svg.png' : properties.closingLogoUrl,
    titleBgUrl: properties.titleBgUrl || '',
    sectionBgUrl: properties.sectionBgUrl || '',
    mainBgUrl: properties.mainBgUrl || '',
    closingBgUrl: properties.closingBgUrl || '',
    
    templateUrl: properties.templateUrl || '',
    
    marginTop:    properties.marginTop    || '',
    marginBottom: properties.marginBottom || '',
    marginLeft:   properties.marginLeft   || '',
    marginRight:  properties.marginRight  || '',
    driveFolderUrl: properties.driveFolderUrl || driveFolderUrl,
    selectedPreset: properties.selectedPreset || 'default',
    geminiGemUrl: scriptProperties.geminiGemUrl || null,
    
    graphColorTheme: properties.graphColorTheme || 'primary',
  };
}


function createBlankPresentation(slideData, settings) {
  try {
    
    
    const rawTitle = (slideData[0] && slideData[0].title) ? String(slideData[0].title) : 'Google Slide Generator Presentation';
    const singleLineTitle = rawTitle.replace(/\r?\n/g, ' ').replace(/\s+/g, ' ').trim();
    let finalName;
    if (settings.showDateColumn) {
      const dateStr = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy.MM.dd');
      finalName = singleLineTitle ? (singleLineTitle + ' ' + dateStr) : ('Google Slide Generator Presentation ' + dateStr);
    } else {
      finalName = singleLineTitle || 'Google Slide Generator Presentation';
    }

    
    
    
    
    const presentation = SlidesApp.create(finalName);
    const defaultSlide = presentation.getSlides()[0];
    if (defaultSlide) {
      defaultSlide.remove();
    }

    let extractedId = null;
    const url = settings.driveFolderUrl; 

    if (url) {
      
      const drivePatterns = [
        /\/folders\/([a-zA-Z0-9_-]+)/,
        /\?id=([a-zA-Z0-9_-]+)/
      ];
      for (let i = 0; i < drivePatterns.length; i++) {
        const match = url.match(drivePatterns[i]);
        if (match && match[1]) {
          extractedId = match[1];
          break;
        }
      }
      
      if (!extractedId && /^[a-zA-Z0-9_-]{25,}$/.test(url)) {
        extractedId = url;
      }
    }

    
    if (extractedId && extractedId.trim()) {
      try {
        
        DriveApp.getFileById(presentation.getId()).moveTo(DriveApp.getFolderById(extractedId.trim()));
      } catch (e) {
        Logger.log(`(Blank) フォルダ移動エラー: ${e.message}`);
        
      }
    }


    Logger.log(`(Blank) 新規プレゼンテーションを作成しました。 Name: ${finalName}`);
    
    addToUserHistory(presentation.getId());

    
    return presentation.getUrl();

  } catch (e) {
    Logger.log(`Error in createBlankPresentation: ${e.message}\nStack: ${e.stack}`);
    throw new Error(`Server error in createBlankPresentation: ${e.message}`);
  }
}







function updateDynamicColors(settings) {
  const primary = settings.primaryColor;
  CONFIG.COLORS.background_gray = generateTintedGray(primary, 10, 99.5); 
  CONFIG.COLORS.faint_gray = generateTintedGray(primary, 10, 93);
  CONFIG.COLORS.ghost_gray = generateTintedGray(primary, 38, 88);
  CONFIG.COLORS.table_header_bg = generateTintedGray(primary, 20, 94);
  CONFIG.COLORS.lane_border = generateTintedGray(primary, 15, 92);
  CONFIG.COLORS.card_border = generateTintedGray(primary, 15, 88);
  CONFIG.COLORS.neutral_gray = generateTintedGray(primary, 5, 62);
  CONFIG.COLORS.process_arrow = CONFIG.COLORS.ghost_gray;
}



function getDriveUploadToken() {
  return { token: ScriptApp.getOAuthToken() };
}


function checkSlidesApiAvailable() {
  try {
    var url = 'https://slides.googleapis.com/v1/presentations/__majin_probe_nonexistent__?fields=slides.objectId';
    var resp = UrlFetchApp.fetch(url, {
      method: 'get',
      headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
      muteHttpExceptions: true
    });
    var code = resp.getResponseCode();
    var body = '';
    try { body = resp.getContentText() || ''; } catch (e) {}
    if (code === 403 && /SERVICE_DISABLED|accessNotConfigured|has not been used in project|it is disabled/i.test(body)) {
      return { available: false, code: code, reason: 'SERVICE_DISABLED' };
    }
    return { available: true, code: code, reason: '' };
  } catch (e) {
    return { available: true, code: 0, reason: 'probe_error:' + ((e && e.message) ? e.message : String(e)) };
  }
}









function _resolveDriveImagesInSlideData(slideData) {
  if (!Array.isArray(slideData)) return [];
  var PREFIX = 'DRIVE_IMAGE:';
  var keys = ['image', 'backgroundImage', 'foregroundImage'];
  var tempFileIds = [];
  var resolveVal = function (v) {
    if (typeof v === 'string' && v.indexOf(PREFIX) === 0) {
      var id = v.slice(PREFIX.length);
      tempFileIds.push(id);
      return id; 
    }
    if (v && typeof v === 'object' && typeof v.data === 'string' && v.data.indexOf(PREFIX) === 0) {
      var id2 = v.data.slice(PREFIX.length);
      tempFileIds.push(id2);
      v.data = id2;
      return v;
    }
    return v;
  };
  
  var resolveStr = function (s) {
    if (typeof s === 'string' && s.indexOf(PREFIX) === 0) {
      var fid = s.slice(PREFIX.length);
      tempFileIds.push(fid);
      return fid;
    }
    return s;
  };
  slideData.forEach(function (slide) {
    if (!slide || typeof slide !== 'object') return;
    keys.forEach(function (k) { if (slide[k] != null) slide[k] = resolveVal(slide[k]); });
    
    if (slide.type === 'splitImage' && slide.image && typeof slide.image === 'object') {
      var im = slide.image;
      if (im.backgroundImage != null) im.backgroundImage = resolveStr(im.backgroundImage);
      if (Array.isArray(im.layers)) {
        im.layers.forEach(function (l) {
          if (l && l.type !== 'text' && l.key != null) l.key = resolveStr(l.key);
        });
      }
    }
  });
  return tempFileIds;
}





function _trashDriveTempFiles(fileIds) {
  if (!fileIds || !fileIds.length) return;
  fileIds.forEach(function (id) {
    if (!id) return;
    try {
      Drive.Files.remove(id); 
    } catch (e) {
      
      try { DriveApp.getFileById(id).setTrashed(true); } catch (e2) {}
      Logger.log('[Method A] 一時ファイル完全削除に失敗→ゴミ箱へ ' + id + ': ' + (e && e.message));
    }
  });
}

function generateSlidesFromWebApp(slideDataString, settings, presentationId = null, imageUpdateOption = 'update') {
  Logger.log('[Server Debug] generateSlidesFromWebApp called.');

  
  
  let __driveTempFileIds = [];
  try {
    const slideData = JSON.parse(slideDataString);
    
    
    __driveTempFileIds = _resolveDriveImagesInSlideData(slideData);

    
    const url = createPresentation(slideData, settings, presentationId, imageUpdateOption);
    Logger.log('[Server Debug] Presentation URL created: ' + url);

    
    
    
    const __urlIdMatch = url ? url.match(/\/d\/([a-zA-Z0-9_-]+)|[?&]id=([a-zA-Z0-9_-]+)/) : null;
    const actualId = (__urlIdMatch && (__urlIdMatch[1] || __urlIdMatch[2])) ? (__urlIdMatch[1] || __urlIdMatch[2]) : presentationId;

    if (actualId) {
      addToUserHistory(actualId);
    }

    
    try {
      let targetId = actualId;

      if (targetId) {
        Logger.log('[Server Debug] ID extracted for metadata: ' + targetId);
        
        
        saveGenerationState(targetId, slideData, settings);
        
      } else {
        Logger.log('[Server Debug] ❌ Could not extract ID from URL: ' + url);
      }

    } catch (e) {
      Logger.log("[Server Debug] ❌ Metadata save error: " + e.message);
    }
    

    return url;
  } catch (e) {
    Logger.log(`Error: ${e.message}\nStack: ${e.stack}`);
    
    try {
      PropertiesService.getScriptProperties().setProperty(
        'LAST_ERROR',
        `${new Date().toISOString()}\n[generateSlidesFromWebApp]\n${e.message}\n${e.stack || ''}`
      );
    } catch (_) {}
    throw new Error(`Server error: ${e.message}`);
  } finally {
    
    _trashDriveTempFiles(__driveTempFileIds);
    
    try {
      if (__CREDIT_TEMP_FILE_ID) {
        DriveApp.getFileById(__CREDIT_TEMP_FILE_ID).setTrashed(true);
        __CREDIT_TEMP_FILE_ID = null;
      }
    } catch (eCr) {}
    
    _sweepOrphanTempFiles_();
  }
}


function _sweepOrphanTempFiles_() {
  try {
    const props = PropertiesService.getScriptProperties();
    const SWEEP_KEY = 'majin_last_sweep_ms';
    const now = Date.now();
    const last = parseInt(props.getProperty(SWEEP_KEY) || '0', 10) || 0;
    if (now - last < 24 * 60 * 60 * 1000) return; 

    const CUTOFF_MS = now - 24 * 60 * 60 * 1000;

    
    
    
    const __selfEmail = (function () { try { return Session.getEffectiveUser().getEmail(); } catch (e) { return ''; } })();
    const __isOwnedBySelf = function (item) {
      try {
        const o = item.getOwner();
        return !!(o && __selfEmail && o.getEmail() === __selfEmail);
      } catch (eOwn) { return false; }
    };

    
    let removedFolders = 0;
    try {
      const folders = DriveApp.searchFolders('title contains "temp_images_"');
      while (folders.hasNext() && removedFolders < 20) {
        const f = folders.next();
        try {
          if (/^temp_images_\d+$/.test(f.getName()) && f.getDateCreated().getTime() < CUTOFF_MS && __isOwnedBySelf(f)) {
            f.setTrashed(true);
            removedFolders++;
          }
        } catch (eF) {}
      }
    } catch (eFolders) {
      Logger.log('[_sweepOrphanTempFiles_] フォルダ走査失敗: ' + (eFolders && eFolders.message));
    }

    
    let removedFiles = 0;
    try {
      const files = DriveApp.getFilesByName('majin_credit_temp.png');
      while (files.hasNext()) {
        const cf = files.next();
        try {
          if (cf.getDateCreated().getTime() < CUTOFF_MS && __isOwnedBySelf(cf)) {
            cf.setTrashed(true);
            removedFiles++;
          }
        } catch (eCf) {}
      }
    } catch (eFiles) {
      Logger.log('[_sweepOrphanTempFiles_] creditファイル走査失敗: ' + (eFiles && eFiles.message));
    }

    
    props.setProperty(SWEEP_KEY, String(now));
    Logger.log('[_sweepOrphanTempFiles_] 掃除完了: folders=' + removedFolders + ', creditFiles=' + removedFiles);
  } catch (e) {
    Logger.log('[_sweepOrphanTempFiles_] 失敗(無視): ' + (e && e.message));
  }
}


function generateDeckFromClientPptx(pptxRef, settings, presentationId, slideDataString) {
  var MIME_PPTX = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
  var tempIds = [];
  settings = settings || {}; 
  try {
    if (typeof pptxRef !== 'string' || !pptxRef) throw new Error('pptxRef が不正です');
    var pptxBlob;
    if (pptxRef.indexOf('DRIVE_PPTX:') === 0) {
      var fid = pptxRef.slice('DRIVE_PPTX:'.length);
      tempIds.push(fid);
      
      pptxBlob = DriveApp.getFileById(fid).getBlob().setContentType(MIME_PPTX);
    } else {
      pptxBlob = Utilities.newBlob(Utilities.base64Decode(pptxRef), MIME_PPTX, 'majin_deck.pptx');
    }
    var baseName = (settings && (settings.fileName || settings.title)) ? String(settings.fileName || settings.title) : 'まじん式スライド';
    var tz = (typeof Session !== 'undefined' && Session.getScriptTimeZone()) ? Session.getScriptTimeZone() : 'Asia/Tokyo';
    var finalName = baseName + ' ' + Utilities.formatDate(new Date(), tz, 'yyyy.MM.dd');
    
    var file = convertPptxToSlides(pptxBlob, finalName, presentationId || null);
    if (!file || !file.id) throw new Error('PPTX→Googleスライド変換に失敗しました');
    var url = 'https://docs.google.com/presentation/d/' + file.id + '/edit';
    try { addToUserHistory(file.id); } catch (e) {}
    
    if (slideDataString) {
      try { saveGenerationState(file.id, JSON.parse(slideDataString), settings); } catch (e) { Logger.log('[ClientPptx] saveGenerationState skip: ' + (e && e.message)); }
    }
    return url;
  } catch (e) {
    
    try { PropertiesService.getScriptProperties().setProperty('LAST_FASTPATH_ERROR', new Date().toISOString() + '\n[generateDeckFromClientPptx]\n' + (e && e.message) + '\n' + (e && e.stack || '')); } catch (e2) {}
    
    
    throw e;
  } finally {
    try { _trashDriveTempFiles(tempIds); } catch (e3) {}
  }
}


function testConvertDrivePptx(driveFileId, keep) {
  var out = { ok: false };
  try {
    if (!driveFileId) throw new Error('driveFileId 未指定');
    var blob = DriveApp.getFileById(driveFileId).getBlob()
      .setContentType('application/vnd.openxmlformats-officedocument.presentationml.presentation');
    var t0 = Date.now();
    var file = convertPptxToSlides(blob, 'まじんPPTX変換テスト_' + Utilities.formatDate(new Date(), 'Asia/Tokyo', 'HHmmss'), null);
    out.convertMs = Date.now() - t0;
    out.deckId = file.id;
    out.url = 'https://docs.google.com/presentation/d/' + file.id + '/edit';
    out.ok = true;
    
    if (!keep) {
      try { DriveApp.getFileById(file.id).setTrashed(true); out.trashed = true; }
      catch (cleanupErr) { out.cleanupError = String(cleanupErr && cleanupErr.message || cleanupErr); }
    }
  } catch (e) { out.error = (e && e.message) + ' ' + (e && e.stack || ''); }
  Logger.log(JSON.stringify(out, null, 2));
  return out;
}



function probeNewSlideMaster() {
  var out = { ok: false };
  var trash = [];
  function inspect(deckId) {
    var info = { id: deckId };
    try {
      var p = SlidesApp.openById(deckId);
      info.masters = p.getMasters().length;
      info.layouts = p.getLayouts().length;
      info.layoutNames = p.getLayouts().map(function (l) { try { return l.getLayoutName(); } catch (e) { return '?'; } });
      info.slides = p.getSlides().length;
    } catch (e) { info.slidesAppErr = (e && e.message) || String(e); }
    try {
      if (typeof Slides !== 'undefined' && Slides && Slides.Presentations) {
        var rp = Slides.Presentations.get(deckId);
        info.masterDisplayNames = (rp.masters || []).map(function (m) { return (m.masterProperties && m.masterProperties.displayName) || m.objectId; });
        info.layoutDisplayNames = (rp.layouts || []).map(function (l) { return (l.layoutProperties && l.layoutProperties.displayName) || l.objectId; });
      }
    } catch (e2) { info.restErr = (e2 && e2.message) || String(e2); }
    
    
    return info;
  }
  try {
    try {
      var pA = SlidesApp.create('まじん診断_SlidesApp_' + new Date().getTime());
      trash.push(pA.getId());
      out.slidesAppCreate = inspect(pA.getId());
    } catch (eA) { out.slidesAppCreate = { error: (eA && eA.message) || String(eA) }; }
    try {
      if (typeof Slides !== 'undefined' && Slides && Slides.Presentations) {
        var pB = Slides.Presentations.create({ title: 'まじん診断_REST_' + new Date().getTime() });
        trash.push(pB.presentationId);
        out.restCreate = inspect(pB.presentationId);
      } else { out.restCreate = { note: 'Slides Advanced Service 未有効' }; }
    } catch (eB) { out.restCreate = { error: (eB && eB.message) || String(eB) }; }
    out.ok = true;
  } catch (e) { out.error = (e && e.message) || String(e); }
  finally {
    trash.forEach(function (id) { try { DriveApp.getFileById(id).setTrashed(true); } catch (eT) {} });
    out.trashed = trash;
  }
  return out;
}


function setupNoTemplateSeedDeck() {
  var out = { ok: false };
  try {
    if (typeof SEED_MASTER_PPTX_B64 !== 'string' || !SEED_MASTER_PPTX_B64) {
      throw new Error('SEED_MASTER_PPTX_B64 が未定義(SeedMasterPptx.gs 未push?)');
    }
    var blob = Utilities.newBlob(
      Utilities.base64Decode(SEED_MASTER_PPTX_B64),
      'application/vnd.openxmlformats-officedocument.presentationml.presentation',
      'まじん_無テンプレ種.pptx'
    );
    
    var existingSeed = PropertiesService.getScriptProperties().getProperty('noTemplateSeedDeckId') || null;
    var existingValid = false;
    if (existingSeed) { try { SlidesApp.openById(existingSeed); existingValid = true; } catch (eEx) { existingValid = false; } }
    var file = convertPptxToSlides(blob, 'まじん 無テンプレ マスター種 (simple-light)', existingValid ? existingSeed : null);
    var id = file.id; 
    PropertiesService.getScriptProperties().setProperty('noTemplateSeedDeckId', id);
    out.ok = true;
    out.seedDeckId = id;
    out.url = 'https://docs.google.com/presentation/d/' + id + '/edit';
    try {
      var deck = SlidesApp.openById(id);
      out.masters = deck.getMasters().length;
      out.layouts = deck.getLayouts().length;
      out.layoutNames = deck.getLayouts().map(function (l) { try { return l.getLayoutName(); } catch (e2) { return '?'; } });
      out.slides = deck.getSlides().length;
      if (typeof Slides !== 'undefined' && Slides && Slides.Presentations) {
        var rp = Slides.Presentations.get(id);
        out.masterDisplayNames = (rp.masters || []).map(function (m) { return (m.masterProperties && m.masterProperties.displayName) || m.objectId; });
      }
    } catch (eP) { out.probeErr = (eP && eP.message) || String(eP); }
  } catch (e) {
    out.error = (e && e.message) || String(e);
    out.stack = (e && e.stack) ? String(e.stack).slice(0, 400) : '';
  }
  return out;
}

function testStandardMasterCopy() {
  var out = { ts: new Date().toISOString() };
  var aId = null, bId = null;
  try {
    
    var A = SlidesApp.create('TEST_master_A_' + Utilities.formatDate(new Date(), 'Asia/Tokyo', 'HHmmss'));
    aId = A.getId();
    out.A_masters_before = A.getMasters().length;
    out.A_layouts_before = A.getLayouts().length;
    out.A_layoutNames_before = A.getLayouts().map(function (l) { try { return l.getLayoutName(); } catch (e) { return '?'; } });
    try { A.getSlides()[0].remove(); } catch (e) {}
    
    var sample = [{ type: 'section', sectionNo: 1, title: '検証セクションA' }, { type: 'section', sectionNo: 2, title: '検証セクションB' }];
    var r = createSectionDeckNative(sample, {});
    if (!r || !r.ok) throw new Error('native B 作成失敗: ' + (r && r.error));
    bId = (r.url.match(/\/d\/([a-zA-Z0-9_-]+)/) || [])[1];
    var B = SlidesApp.openById(bId);
    out.B_masters = B.getMasters().length;
    out.B_layouts = B.getLayouts().length;
    out.B_slides = B.getSlides().length;
    
    var bSlides = B.getSlides();
    var t0 = Date.now();
    for (var i = 0; i < bSlides.length; i++) { A.appendSlide(bSlides[i]); }
    A.saveAndClose();
    out.copyMs = Date.now() - t0;
    var A2 = SlidesApp.openById(aId);
    out.A_masters_after = A2.getMasters().length;
    out.A_layouts_after = A2.getLayouts().length;
    out.A_layoutNames_after = A2.getLayouts().map(function (l) { try { return l.getLayoutName(); } catch (e) { return '?'; } });
    out.A_slides_after = A2.getSlides().length;
    out.standardKept = out.A_layouts_after >= out.A_layouts_before; 
    out.ok = true;
  } catch (e) { out.error = (e && e.message) + ' ' + (e && e.stack || ''); }
  finally {
    try { if (aId) DriveApp.getFileById(aId).setTrashed(true); } catch (e) {}
    try { if (bId) DriveApp.getFileById(bId).setTrashed(true); } catch (e) {}
  }
  Logger.log(JSON.stringify(out, null, 2));
  return out;
}

let __SECTION_COUNTER = 0;
let __SLIDE_DATA_FOR_AGENDA = [];
let __CREDIT_IMAGE_BLOB = null;
let __CACHED_GRADIENT_BOTTOM_BAR = null;
let __CACHED_GRADIENT_UNDERLINE_RECT = null;
let __CACHED_HEADER_LOGO_GROUP = null;

let __SEED_USE_EMBEDDED = false;
let __CACHED_CREDIT_IMAGE_GROUP = null;

let __CREDIT_DIMS = null;

let __TEMPLATE_SLIDES = null;


let __TEMPLATE_BASE_IS_COPY = false;

let __SEED_BASE_ACTIVE = false;


let __SEED_TEMPLATE_ID = '';

let __CURRENT_SLIDE_USES_TEMPLATE = false;



let __TPL_DUP_SLIDE_COUNT = -1;




let __NATIVE_SECTION_ACTIVE = false;


let __NATIVE_DECOR_ACTIVE = false;




let __REST_TEXT_ACTIVE = false;


let __SLIDES_API_OK = false;



let __NATIVE_CHART_ACTIVE = false;

let __IMAGE_BLOB_CACHE = {};



let __PERF = null;
function perfStart() {
  __PERF = { t0: Date.now(), phases: {}, counts: {}, metrics: {} };
}
function perfAdd(label, ms) {
  if (!__PERF) return;
  __PERF.phases[label] = (__PERF.phases[label] || 0) + ms;
  __PERF.counts[label] = (__PERF.counts[label] || 0) + 1;
}


function perfCount(label, n) {
  if (!__PERF) return;
  __PERF.metrics[label] = (__PERF.metrics[label] || 0) + (n || 0);
}

function perfTime(label, fn) {
  if (!__PERF) return fn();
  const _t = Date.now();
  try { return fn(); }
  finally { perfAdd(label, Date.now() - _t); }
}
function perfLog(totalLabel) {
  if (!__PERF) return '';
  const total = Date.now() - __PERF.t0;
  const keys = Object.keys(__PERF.phases).sort((a, b) => __PERF.phases[b] - __PERF.phases[a]);
  const lines = keys.map((k) => {
    const ms = __PERF.phases[k], n = __PERF.counts[k];
    const pct = total > 0 ? Math.round((ms / total) * 100) : 0;
    return `  ${k}: ${ms}ms (${pct}% / ${n}回, 平均${Math.round(ms / Math.max(1, n))}ms)`;
  });
  
  const metricKeys = Object.keys(__PERF.metrics || {});
  const metricLine = metricKeys.length
    ? `\n  [件数(時間ではない)] ${metricKeys.map((k) => `${k}=${__PERF.metrics[k]}`).join(', ')}`
    : '';
  const summary = `[PERF] ${totalLabel} 合計=${total}ms\n${lines.join('\n')}${metricLine}`;
  Logger.log(summary);
  
  try {
    PropertiesService.getScriptProperties().setProperty(
      'LAST_PERF_LOG',
      `${new Date().toISOString()}\n${summary}`
    );
  } catch (e) {  }
  return summary;
}


function getLastPerfLog() {
  try {
    const props = PropertiesService.getScriptProperties();
    const perf = props.getProperty('LAST_PERF_LOG') || '(まだ計測データがありません)';
    const err = props.getProperty('LAST_ERROR');
    const restErr = props.getProperty('LAST_REST_ERROR');
    let out = '';
    if (restErr) out += `=== 直近のRESTエラー(batchUpdate) ===\n${restErr}\n\n`;
    if (err) out += `=== 直近のサーバーエラー ===\n${err}\n\n`;
    out += `=== 直近の計測(成功時) ===\n${perf}`;
    return out;
  } catch (e) {
    return `取得エラー: ${e.message}`;
  }
}


function clearDebugInfo() {
  try {
    const props = PropertiesService.getScriptProperties();
    props.deleteProperty('LAST_ERROR');
    props.deleteProperty('LAST_REST_ERROR');
    return 'クリアしました(LAST_ERROR, LAST_REST_ERROR)。';
  } catch (e) {
    return `クリア失敗: ${e.message}`;
  }
}


function authorizeOnce() {
  let msg = '認可チェック: ';
  try {
    
    const token = ScriptApp.getOAuthToken();
    msg += `OAuthトークン取得OK(len=${token ? token.length : 0}). `;
  } catch (e) {
    msg += `トークン取得エラー: ${e.message}. `;
  }
  try {
    
    const ok = (typeof Slides !== 'undefined' && Slides && Slides.Presentations);
    msg += ok ? 'Slides Advanced Service 利用可能.' : 'Slides 未定義(Advanced Service未有効?).';
  } catch (e) {
    msg += `Slides参照エラー: ${e.message}.`;
  }
  Logger.log(msg);
  return msg;
}


let __REST_REQUESTS = [];
let __REST_OBJ_SEQ = 0;
let __REST_PRESENTATION_ID = null;


let __FORCE_REST_FAIL = false;


let __CREDIT_IMAGE_URL = null;
let __CREDIT_TEMP_FILE_ID = null;
let __CREDIT_REST_REFS = [];


let __CREDIT_TEXT = null;
let __CREDIT_TEXT_COLOR = null;





let __HEADER_LOGO_SRC = null;   
let __CLOSING_LOGO_SRC = null;  
let __REST_IMAGE_TEMP_IDS = [];

function restReset(presentationId) {
  __REST_REQUESTS = [];
  __REST_OBJ_SEQ = 0;
  __REST_PRESENTATION_ID = presentationId || null;
}
function restNewId(prefix) {
  __REST_OBJ_SEQ++;
  return (prefix || 'el') + '_' + Date.now().toString(36) + '_' + __REST_OBJ_SEQ;
}
function restPush(req) { __REST_REQUESTS.push(req); }

function restFlush() {
  if (!__REST_PRESENTATION_ID || __REST_REQUESTS.length === 0) return;
  const reqs = __REST_REQUESTS;
  __REST_REQUESTS = [];
  const _t = Date.now();
  try {
    if (__FORCE_REST_FAIL) throw new Error('forced flush fail (E2E自動フォールバック検証フック: ?flushfail=1)'); 
    
    
    
    
    Slides.Presentations.batchUpdate({ requests: reqs }, __REST_PRESENTATION_ID);
    perfAdd('restFlush', Date.now() - _t);
    perfCount('restReqCount', reqs.length);
  } catch (e) {
    
    try {
      PropertiesService.getScriptProperties().setProperty(
        'LAST_REST_ERROR',
        `${new Date().toISOString()}\n[restFlush] reqs=${reqs.length}\n${e.message}\n${e.stack || ''}`
      );
    } catch (_) {}
    throw e;
  }
}

function hexToRgb01(hex) {
  try {
    const c = String(hex || '').replace('#', '');
    const n = parseInt(c.length === 3 ? c.split('').map(x => x + x).join('') : c, 16);
    return { red: ((n >> 16) & 255) / 255, green: ((n >> 8) & 255) / 255, blue: (n & 255) / 255 };
  } catch (e) { return { red: 0, green: 0, blue: 0 }; }
}

function blendOnWhite(hex, alpha) {
  const rgb = hexToRgb01(hex);
  const a = (typeof alpha === 'number') ? Math.max(0, Math.min(1, alpha)) : 1;
  return { red: rgb.red * a + (1 - a), green: rgb.green * a + (1 - a), blue: rgb.blue * a + (1 - a) };
}

function restAddTextBox(slideId, rect, text, style) {
  style = style || {};
  const id = restNewId('tb');
  restPush({
    createShape: {
      objectId: id,
      shapeType: 'TEXT_BOX',
      elementProperties: {
        pageObjectId: slideId,
        size: {
          width:  { magnitude: Math.max(1, rect.width),  unit: 'PT' },
          height: { magnitude: Math.max(1, rect.height), unit: 'PT' }
        },
        transform: { scaleX: 1, scaleY: 1, translateX: rect.left, translateY: rect.top, unit: 'PT' }
      }
    }
  });
  if (style.contentAlignment) {
    restPush({ updateShapeProperties: { objectId: id, shapeProperties: { contentAlignment: style.contentAlignment }, fields: 'contentAlignment' } });
  }
  if (text != null && String(text) !== '') {
    restPush({ insertText: { objectId: id, text: String(text), insertionIndex: 0 } });
    const textStyle = {};
    const fields = [];
    if (style.size != null)   { textStyle.fontSize = { magnitude: style.size, unit: 'PT' }; fields.push('fontSize'); }
    if (style.bold != null)   { textStyle.bold = !!style.bold; fields.push('bold'); }
    if (style.fontFamily)     { textStyle.fontFamily = style.fontFamily; fields.push('fontFamily'); }
    if (style.colorRgb)       { textStyle.foregroundColor = { opaqueColor: { rgbColor: style.colorRgb } }; fields.push('foregroundColor'); }
    if (fields.length) {
      restPush({ updateTextStyle: { objectId: id, style: textStyle, textRange: { type: 'ALL' }, fields: fields.join(',') } });
    }
    
    const pStyle = {}; const pFields = [];
    if (style.align)             { pStyle.alignment = style.align; pFields.push('alignment'); }
    if (style.lineSpacing != null) { pStyle.lineSpacing = style.lineSpacing; pFields.push('lineSpacing'); }
    if (style.spaceBelow != null)  { pStyle.spaceBelow = { magnitude: style.spaceBelow, unit: 'PT' }; pFields.push('spaceBelow'); }
    if (pFields.length) {
      restPush({ updateParagraphStyle: { objectId: id, style: pStyle, textRange: { type: 'ALL' }, fields: pFields.join(',') } });
    }
    
    if (Array.isArray(style.ranges)) {
      style.ranges.forEach(function (r) {
        if (r == null || r.start == null || r.end == null || r.end <= r.start) return;
        const rs = {}; const rf = [];
        if (r.bold != null) { rs.bold = !!r.bold; rf.push('bold'); }
        const rgb = r.colorRgb || (r.color ? hexToRgb01(r.color) : null);
        if (rgb)            { rs.foregroundColor = { opaqueColor: { rgbColor: rgb } }; rf.push('foregroundColor'); }
        if (r.size != null) { rs.fontSize = { magnitude: r.size, unit: 'PT' }; rf.push('fontSize'); }
        if (rf.length) {
          restPush({ updateTextStyle: { objectId: id, style: rs, textRange: { type: 'FIXED_RANGE', startIndex: r.start, endIndex: r.end }, fields: rf.join(',') } });
        }
      });
    }
    
    if (style.bulletPreset) {
      restPush({ createParagraphBullets: { objectId: id, textRange: { type: 'ALL' }, bulletPreset: (style.bulletPreset === true ? 'BULLET_DISC_CIRCLE_SQUARE' : style.bulletPreset) } });
    }
  }
  return id;
}


function restAddShape(slideId, rect, shapeType, opts) {
  opts = opts || {};
  const id = restNewId('shp');
  
  
  let __transform;
  if (opts.rotation) {
    const __rad = opts.rotation * Math.PI / 180;
    const __cos = Math.cos(__rad), __sin = Math.sin(__rad);
    const __w = Math.max(1, rect.width), __h = Math.max(1, rect.height);
    const __cx = rect.left + __w / 2, __cy = rect.top + __h / 2;
    __transform = {
      scaleX: __cos, scaleY: __cos, shearX: -__sin, shearY: __sin,
      translateX: __cx - __cos * (__w / 2) + __sin * (__h / 2),
      translateY: __cy - __sin * (__w / 2) - __cos * (__h / 2),
      unit: 'PT'
    };
  } else {
    __transform = { scaleX: 1, scaleY: 1, translateX: rect.left, translateY: rect.top, unit: 'PT' };
  }
  restPush({
    createShape: {
      objectId: id,
      shapeType: shapeType,
      elementProperties: {
        pageObjectId: slideId,
        size: {
          width:  { magnitude: Math.max(1, rect.width),  unit: 'PT' },
          height: { magnitude: Math.max(1, rect.height), unit: 'PT' }
        },
        transform: __transform
      }
    }
  });
  const sp = {}; const fields = [];
  if (opts.fillRgb) {
    sp.shapeBackgroundFill = { solidFill: { color: { rgbColor: opts.fillRgb }, alpha: (opts.fillAlpha != null ? opts.fillAlpha : 1) } };
    fields.push('shapeBackgroundFill.solidFill.color', 'shapeBackgroundFill.solidFill.alpha');
  } else if (opts.noFill) {
    sp.shapeBackgroundFill = { propertyState: 'NOT_RENDERED' };
    fields.push('shapeBackgroundFill.propertyState');
  }
  if (opts.outlineRgb) {
    sp.outline = {
      outlineFill: { solidFill: { color: { rgbColor: opts.outlineRgb }, alpha: (opts.outlineAlpha != null ? opts.outlineAlpha : 1) } },
      weight: { magnitude: (opts.outlineWeight != null ? opts.outlineWeight : 1), unit: 'PT' }
    };
    fields.push('outline.outlineFill.solidFill.color', 'outline.outlineFill.solidFill.alpha', 'outline.weight');
  } else if (opts.noOutline) {
    sp.outline = { propertyState: 'NOT_RENDERED' };
    fields.push('outline.propertyState');
  }
  if (opts.contentAlignment) { sp.contentAlignment = opts.contentAlignment; fields.push('contentAlignment'); }
  if (fields.length) {
    restPush({ updateShapeProperties: { objectId: id, shapeProperties: sp, fields: fields.join(',') } });
  }
  return id;
}


function restAddLine(slideId, x1, y1, x2, y2, opts) {
  opts = opts || {};
  const id = restNewId('ln');
  
  
  
  const w = Math.max(1, Math.abs(x2 - x1)), h = Math.max(1, Math.abs(y2 - y1));
  const sx = (x2 < x1) ? -1 : 1;
  const sy = (y2 < y1) ? -1 : 1;
  restPush({
    createLine: {
      objectId: id,
      lineCategory: opts.category || 'STRAIGHT',
      elementProperties: {
        pageObjectId: slideId,
        size: { width: { magnitude: w, unit: 'PT' }, height: { magnitude: h, unit: 'PT' } },
        transform: { scaleX: sx, scaleY: sy, translateX: x1, translateY: y1, unit: 'PT' }
      }
    }
  });
  const lp = {}; const fields = [];
  if (opts.colorRgb) {
    lp.lineFill = { solidFill: { color: { rgbColor: opts.colorRgb }, alpha: (opts.alpha != null ? opts.alpha : 1) } };
    fields.push('lineFill.solidFill.color', 'lineFill.solidFill.alpha');
  }
  if (opts.weight != null) { lp.weight = { magnitude: opts.weight, unit: 'PT' }; fields.push('weight'); }
  if (opts.dashStyle) { lp.dashStyle = opts.dashStyle; fields.push('dashStyle'); }
  if (opts.startArrow) { lp.startArrow = opts.startArrow; fields.push('startArrow'); }
  if (opts.endArrow) { lp.endArrow = opts.endArrow; fields.push('endArrow'); }
  if (fields.length) {
    restPush({ updateLineProperties: { objectId: id, lineProperties: lp, fields: fields.join(',') } });
  }
  return id;
}


function restAddImage(slideId, rect, url) {
  const id = restNewId('img');
  restPush({
    createImage: {
      objectId: id,
      url: url,
      elementProperties: {
        pageObjectId: slideId,
        size: {
          width:  { magnitude: Math.max(1, rect.width),  unit: 'PT' },
          height: { magnitude: Math.max(1, rect.height), unit: 'PT' }
        },
        transform: { scaleX: 1, scaleY: 1, translateX: rect.left, translateY: rect.top, unit: 'PT' }
      }
    }
  });
  return id;
}


function _restReqObjectId(r) {
  if (!r) return null;
  if (r.createShape) return r.createShape.objectId;
  if (r.createImage) return r.createImage.objectId;
  if (r.createLine) return r.createLine.objectId;                 
  if (r.updateLineProperties) return r.updateLineProperties.objectId; 
  if (r.insertText) return r.insertText.objectId;
  if (r.updateTextStyle) return r.updateTextStyle.objectId;
  if (r.updateParagraphStyle) return r.updateParagraphStyle.objectId;
  if (r.updateShapeProperties) return r.updateShapeProperties.objectId;
  if (r.createParagraphBullets) return r.createParagraphBullets.objectId;
  return null;
}

function _restDropRequestsForSlide(slideId) {
  if (!slideId || !__REST_REQUESTS || !__REST_REQUESTS.length) return;
  
  const doomed = {};
  __REST_REQUESTS.forEach(function (r) {
    if (r.createShape && r.createShape.elementProperties && r.createShape.elementProperties.pageObjectId === slideId) doomed[r.createShape.objectId] = true;
    if (r.createImage && r.createImage.elementProperties && r.createImage.elementProperties.pageObjectId === slideId) doomed[r.createImage.objectId] = true;
  });
  
  __REST_REQUESTS = __REST_REQUESTS.filter(function (r) {
    const oid = _restReqObjectId(r);
    return !(oid && doomed[oid]);
  });
  
  try {
    if (__CREDIT_REST_REFS && __CREDIT_REST_REFS.length) {
      __CREDIT_REST_REFS = __CREDIT_REST_REFS.filter(function (ref) {
        try { return !(ref && ref.objectId && doomed[ref.objectId]); } catch (e) { return true; }
      });
    }
  } catch (e) {}
}



function _restParagraphAlign(a) {
  try {
    const s = String(a);
    if (s.indexOf('END') >= 0) return 'END';
    if (s.indexOf('CENTER') >= 0) return 'CENTER';
    return 'START';
  } catch (e) { return 'START'; }
}

function _restContentAlignment(c) {
  try {
    const s = String(c);
    if (s.indexOf('MIDDLE') >= 0) return 'MIDDLE';
    if (s.indexOf('BOTTOM') >= 0) return 'BOTTOM';
    return 'TOP';
  } catch (e) { return 'TOP'; }
}

function _restStyleFromOpt(opt) {
  opt = opt || {};
  
  const defaultColor = (opt.fontType === 'large') ? CONFIG.COLORS.text_primary : CONFIG.COLORS.text_small_font;
  const colorHex = opt.color || defaultColor;
  const style = {
    size: (opt.size != null) ? opt.size : CONFIG.FONTS.sizes.body,
    bold: !!opt.bold,
    fontFamily: CONFIG.FONTS.family,
    colorRgb: hexToRgb01(colorHex)
  };
  if (opt.align) style.align = _restParagraphAlign(opt.align);
  return style;
}

function _insertTextBox(slide, rect, text, opt, contentAlignment) {
  opt = opt || {};
  const txt = (text == null) ? '' : String(text);
  if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
    try {
      const slideId = slide.getObjectId();
      const restStyle = _restStyleFromOpt(opt);
      if (contentAlignment) restStyle.contentAlignment = _restContentAlignment(contentAlignment);
      restAddTextBox(slideId, rect, txt, restStyle);
      perfCount('restText:box', 1);
      return null;
    } catch (e) {
      Logger.log('[_insertTextBox] REST失敗→SlidesAppフォールバック: ' + e.message);
      
    }
  }
  const shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, rect.left, rect.top, rect.width, rect.height);
  shape.getText().setText(txt);
  applyTextStyle(shape.getText(), opt);
  if (contentAlignment) { try { shape.setContentAlignment(contentAlignment); } catch (e) {} }
  return shape;
}


function _restStyledAutofitBox(slide, rect, rawText, baseOpt, contentAlignment, preCalculatedWidthPt, opts) {
  baseOpt = baseOpt || {}; opts = opts || {};
  const parsed = parseInlineStyles(rawText == null ? '' : String(rawText));
  const baseSize = (baseOpt.size != null) ? baseOpt.size : CONFIG.FONTS.sizes.body;
  const spaceBelowPt = opts.spaceBelowPt || 0;
  const preW = (typeof preCalculatedWidthPt === 'number') ? preCalculatedWidthPt : null;
  
  const finalSize = opts.noAutofit ? (Math.floor(baseSize * 2) / 2)
    : computeAutofitFontSize(parsed.output, baseSize, rect.width, rect.height, preW, spaceBelowPt);
  const restStyle = _restStyleFromOpt({ size: finalSize, bold: baseOpt.bold, color: baseOpt.color, fontType: baseOpt.fontType, align: baseOpt.align });
  if (contentAlignment) restStyle.contentAlignment = _restContentAlignment(contentAlignment);
  let __allRanges = [];
  if (parsed.ranges && parsed.ranges.length) {
    __allRanges = parsed.ranges.map(function (r) {
      const o = { start: r.start, end: r.end };
      if (r.bold != null) o.bold = r.bold;
      if (r.color) o.color = r.color;
      return o;
    });
    
    if (opts.boldRunSize && finalSize === Math.floor(baseSize * 2) / 2) __allRanges.forEach(function (r) { if (r.bold) r.size = opts.boldRunSize; });
  }
  
  
  
  
  if (Array.isArray(opts.extraRanges) && opts.extraRanges.length) {
    const __outLen = parsed.output.length;
    opts.extraRanges.forEach(function (r) {
      if (r == null || r.start == null || r.end == null) return;
      if (r.start < 0 || r.end > __outLen || r.end <= r.start) return; 
      const o = { start: r.start, end: r.end };
      if (r.bold != null) o.bold = r.bold;
      if (r.color) o.color = r.color;
      if (r.size != null) o.size = r.size;
      __allRanges.push(o);
    });
  }
  if (__allRanges.length) restStyle.ranges = __allRanges;
  if (opts.lineSpacing != null) restStyle.lineSpacing = opts.lineSpacing;
  if (spaceBelowPt) restStyle.spaceBelow = spaceBelowPt;
  if (opts.bulletPreset) restStyle.bulletPreset = opts.bulletPreset;
  restAddTextBox(slide.getObjectId(), rect, parsed.output, restStyle);
  perfCount('restText:box', 1);
  return finalSize;
}


function _restBulletsAutofitBox(slide, rect, points, baseOpt, opts) {
  baseOpt = baseOpt || {}; opts = opts || {};
  const joiner = '\n\n';
  let combined = ''; const ranges = [];
  (points || []).forEach(function (pt, idx) {
    const parsed = parseInlineStyles(String(pt == null ? '' : pt));
    if (idx > 0) combined += joiner;
    const start = combined.length;
    combined += parsed.output;
    (parsed.ranges || []).forEach(function (r) { ranges.push({ start: start + r.start, end: start + r.end, bold: r.bold, color: r.color }); });
  });
  if (!combined) combined = '—'; 
  const baseSize = (baseOpt.size != null) ? baseOpt.size : CONFIG.FONTS.sizes.body;
  const baseFloored = Math.floor(baseSize * 2) / 2;
  const spaceBelowPt = (opts.spaceBelowPt != null) ? opts.spaceBelowPt : 6;
  const lineSpacing = (opts.lineSpacing != null) ? opts.lineSpacing : 100;
  const finalSize = computeAutofitFontSize(combined, baseSize, rect.width, rect.height, null, spaceBelowPt);
  
  if (opts.boldRunSize && finalSize === baseFloored) {
    ranges.forEach(function (r) { if (r.bold) r.size = opts.boldRunSize; });
  }
  let contentAlignment = opts.contentAlignment || null;
  if (opts.shrinkToMiddleBelow != null) {
    contentAlignment = (finalSize < opts.shrinkToMiddleBelow) ? SlidesApp.ContentAlignment.MIDDLE : SlidesApp.ContentAlignment.TOP;
  }
  const restStyle = _restStyleFromOpt({ size: finalSize, color: baseOpt.color, fontType: baseOpt.fontType, bold: baseOpt.bold, align: baseOpt.align });
  restStyle.ranges = ranges;
  restStyle.lineSpacing = lineSpacing;
  restStyle.spaceBelow = spaceBelowPt;
  if (contentAlignment) restStyle.contentAlignment = _restContentAlignment(contentAlignment);
  restAddTextBox(slide.getObjectId(), rect, combined, restStyle);
  perfCount('restText:box', 1);
  return finalSize;
}


function _restParenthesizedRanges(text, size) {
  const src = (text == null) ? '' : String(text);
  const regex = /(\(|（)([^()（）]+)(\)|）)/g;
  let out = '';
  let last = 0;
  const ranges = [];
  let m;
  while ((m = regex.exec(src)) !== null) {
    out += src.slice(last, m.index);
    const openStr = (m[1] === '（') ? ' (' : m[1];
    const closeStr = (m[3] === '）') ? ') ' : m[3];
    const replacement = openStr + m[2] + closeStr;
    const start = out.length;
    out += replacement;
    ranges.push({ start: start, end: start + replacement.length, size: size });
    last = m.index + m[0].length;
  }
  out += src.slice(last);
  return { text: out, ranges: ranges };
}

const CREDIT_IMAGE_LINK = 'https://note.com/majin_108';


function getTemplateCategoryForType(type) {
  if (type === 'title') return 'title';
  if (type === 'section') return 'section';
  if (type === 'closing') return 'closing';
  return 'content';
}


function getPredefinedLayoutForType(type) {
  
  
  
  
  return SlidesApp.PredefinedLayout.BLANK;
}


function removePlaceholders(slide) {
  try {
    slide.getPageElements().forEach(e => {
      try { e.remove(); } catch (err) {}
    });
  } catch (e) {
    Logger.log(`removePlaceholders error: ${e.message}`);
  }
}


function loadTemplateSlides(settings) {
  __TEMPLATE_SLIDES = null;
  const url = settings && settings.templateUrl ? String(settings.templateUrl).trim() : '';
  if (!url) return;
  try {
    let tplPresentation;
    try {
      tplPresentation = SlidesApp.openByUrl(url);
    } catch (e1) {
      
      const m = url.match(/[-\w]{25,}/);
      if (m) tplPresentation = SlidesApp.openById(m[0]);
      else throw e1;
    }
    const tplSlides = tplPresentation.getSlides();
    if (!tplSlides || tplSlides.length === 0) {
      Logger.log('テンプレートにスライドがありません。テンプレートは無効化されます。');
      return;
    }
    
    __TEMPLATE_SLIDES = {
      title:   tplSlides[0] || null,
      section: tplSlides[1] || null,
      content: tplSlides[2] || null,
      closing: tplSlides[3] || null,
    };
    Logger.log(`テンプレート読込成功: ${tplSlides.length}枚 (タイプ別に割当)`);
  } catch (e) {
    Logger.log(`テンプレート読込失敗: ${e.message}. テンプレートは無効化されます。`);
    __TEMPLATE_SLIDES = null;
  }
}


function checkTemplateUrl(url) {
  try {
    var s = url ? String(url).trim() : '';
    if (!s) return { ok: false, error: 'URLが空です' };
    var p;
    try {
      p = SlidesApp.openByUrl(s);
    } catch (e1) {
      var m = s.match(/[-\w]{25,}/);
      if (!m) return { ok: false, error: 'URLの形式が正しくありません' };
      p = SlidesApp.openById(m[0]);
    }
    var slides = p.getSlides();
    var n = slides ? slides.length : 0;
    if (n === 0) return { ok: false, error: 'スライドが0枚です（4枚構成で作成してください）' };
    return { ok: true, name: p.getName(), slideCount: n };
  } catch (e) {
    var msg = (e && e.message) ? e.message : String(e);
    
    return { ok: false, error: 'このスライドを開けません（存在しないか、アクセス権限がない可能性）' };
  }
}


function createBaseSlide(presentation, type, insertIndex = null) {
  const category = getTemplateCategoryForType(type);
  const tplSlide = (__TEMPLATE_SLIDES && __TEMPLATE_SLIDES[category]) ? __TEMPLATE_SLIDES[category] : null;

  let slide;
  if (tplSlide) {
    
    try {
      if (__TEMPLATE_BASE_IS_COPY && typeof tplSlide.duplicate === 'function') {
        
        slide = tplSlide.duplicate();
        
        
        
        if (__TPL_DUP_SLIDE_COUNT < 0) { __TPL_DUP_SLIDE_COUNT = presentation.getSlides().length; }
        else { __TPL_DUP_SLIDE_COUNT++; }
        const __tot = __TPL_DUP_SLIDE_COUNT;
        slide.move(insertIndex == null ? (__tot - 1) : insertIndex); 
        __CURRENT_SLIDE_USES_TEMPLATE = true;
        return slide;
      }
      
      __TPL_DUP_SLIDE_COUNT = -1;
      slide = (insertIndex == null)
        ? presentation.appendSlide(tplSlide)
        : presentation.insertSlide(insertIndex, tplSlide);
      __CURRENT_SLIDE_USES_TEMPLATE = true;
      return slide;
    } catch (e) {
      Logger.log(`テンプレート複製失敗(${category}): ${e.message}. 標準レイアウトにフォールバックします。`);
      
    }
  }

  
  
  __TPL_DUP_SLIDE_COUNT = -1;
  const predefined = getPredefinedLayoutForType(type);
  slide = (insertIndex == null)
    ? presentation.appendSlide(predefined)
    : presentation.insertSlide(insertIndex, predefined);
  removePlaceholders(slide);
  __CURRENT_SLIDE_USES_TEMPLATE = false;
  return slide;
}


function resetRunState_() {
  __CACHED_GRADIENT_BOTTOM_BAR = null;
  __CACHED_GRADIENT_UNDERLINE_RECT = null;
  __CACHED_HEADER_LOGO_GROUP = null;
  __CACHED_CREDIT_IMAGE_GROUP = null;
  __CREDIT_DIMS = null; 
  __HEADER_LOGO_SRC = null;   
  __CLOSING_LOGO_SRC = null;
  __REST_IMAGE_TEMP_IDS = [];
  __NATIVE_SECTION_ACTIVE = false; 
  __NATIVE_DECOR_ACTIVE = false;    
  __NATIVE_CHART_ACTIVE = false;    
  __TPL_DUP_SLIDE_COUNT = -1;       
  __SEED_USE_EMBEDDED = false;      
  __TPL_MTIME_CACHE = {};           
  __IMAGE_BLOB_CACHE = {}; 
  restReset(null); 
}


function createPresentation(slideData, settings, presentationId = null, imageUpdateOption = 'update', __isFallbackRetry = false) {
  
  
  
  perfStart(); 
  const __RUN_START_MS = Date.now(); 
  resetRunState_(); 
  
  __CREDIT_TEXT = (settings && settings.creditText && String(settings.creditText).trim()) ? String(settings.creditText).trim() : null;
  __CREDIT_TEXT_COLOR = __CREDIT_TEXT ? blendOnWhite((settings && settings.primaryColor) || '#4285F4', 0.7) : null;

  
  
  
  
  __TEMPLATE_BASE_IS_COPY = false;
  
  
  var __TEMPLATE_TRY_NATIVE = !!(settings && settings.templateUrl && String(settings.templateUrl).trim());
  
  
  
  

  
  
  
  
  __SEED_BASE_ACTIVE = false;
  __SEED_TEMPLATE_ID = '';
  __SEED_USE_EMBEDDED = false;
  try {
    if (!__TEMPLATE_BASE_IS_COPY && !(settings && settings.templateUrl && String(settings.templateUrl).trim())) {
      
      
      
      
      if (typeof SEED_MASTER_PPTX_B64 === 'string' && SEED_MASTER_PPTX_B64) {
        __SEED_USE_EMBEDDED = true;
      } else {
        var __seedId = PropertiesService.getScriptProperties().getProperty('noTemplateSeedDeckId') || '';
        
        
        var __seedOk = false;
        if (__seedId) { try { SlidesApp.openById(__seedId); __seedOk = true; } catch (eOpen) { __seedOk = false; } }
        if (!__seedOk) {
          try {
            var __su = setupNoTemplateSeedDeck();
            if (__su && __su.ok && __su.seedDeckId) {
              __seedId = String(__su.seedDeckId); __seedOk = true;
              Logger.log('[無テンプレ] 種デッキを自動セットアップしました: ' + __seedId);
            } else {
              Logger.log('[無テンプレ] 種デッキ自動セットアップ失敗(従来マスター経路へ): ' + ((__su && __su.error) || '不明'));
            }
          } catch (eSetup) {
            Logger.log('[無テンプレ] 種デッキ自動セットアップ例外(従来マスター経路へ): ' + ((eSetup && eSetup.message) || eSetup));
          }
        }
        if (__seedOk && __seedId) { __SEED_TEMPLATE_ID = String(__seedId); }
      }
    }
  } catch (eSeed) {
    __SEED_TEMPLATE_ID = '';
    __SEED_USE_EMBEDDED = false;
    Logger.log('[無テンプレ] 種の用意に失敗→従来方式にフォールバック: ' + ((eSeed && eSeed.message) || eSeed));
  }

  
  
  
  
  if (!__TEMPLATE_TRY_NATIVE && !__TEMPLATE_BASE_IS_COPY) {
    perfTime('templateLoad', () => loadTemplateSlides(settings));
  }
  updateDynamicColors(settings);
  CONFIG.COLORS.primary_color = settings.primaryColor || CONFIG.COLORS.primary_color;
  CONFIG.COLORS.text_primary = settings.largeFontColor || '#333333';
  CONFIG.COLORS.text_small_font = settings.smallFontColor || '#1F2937';
  if (settings.backgroundColor && settings.backgroundColor.toUpperCase() !== '#FFFFFF') {
    CONFIG.COLORS.background_white = settings.backgroundColor;
  } else {
    CONFIG.COLORS.background_white = '#FFFFFF';
  }
  CONFIG.FOOTER_TEXT = settings.footerText;
  CONFIG.FONTS.family = settings.fontFamily || CONFIG.FONTS.family;
  CONFIG.LOGOS.header = settings.headerLogoUrl;
  CONFIG.LOGOS.closing = settings.closingLogoUrl;
  CONFIG.BACKGROUND_IMAGES.title = settings.titleBgUrl;
  CONFIG.BACKGROUND_IMAGES.closing = settings.closingBgUrl;
  CONFIG.BACKGROUND_IMAGES.section = settings.sectionBgUrl;
  CONFIG.BACKGROUND_IMAGES.main = settings.mainBgUrl;

  __SLIDE_DATA_FOR_AGENDA = slideData;

  __CREDIT_IMAGE_BLOB = null; 
  if (settings.creditImageBase64) {
    try {
      
      const parts = settings.creditImageBase64.split(',');
      if (parts.length !== 2) throw new Error('Invalid Base64 format for credit image.');
      const mimeType = parts[0].match(/:(.*?);/)[1];
      const decodedData = Utilities.base64Decode(parts[1]);
      __CREDIT_IMAGE_BLOB = Utilities.newBlob(decodedData, mimeType, 'credit.png');
      Logger.log('[createPresentation] クレジット画像のBlob生成に成功しました。');
    } catch (e) {
      Logger.log(`[createPresentation] クレジット画像のBase64デコードに失敗: ${e.message}`);
    }
  } else {
    Logger.log('[createPresentation] クレジット画像データはありませんでした。');
  }

  
  
    const rawTitle = (slideData[0] && slideData[0].title) ? String(slideData[0].title) : 'Google Slide Generator Presentation';
  const singleLineTitle = rawTitle.replace(/\r?\n/g, ' ').replace(/\s+/g, ' ').trim();
  let finalName;
  if (settings.showDateColumn) {
    const dateStr = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy.MM.dd');
    const baseTitle = singleLineTitle || 'Google Slide Generator Presentation';
    
    
    finalName = baseTitle.endsWith(' ' + dateStr) ? baseTitle : (baseTitle + ' ' + dateStr);
  } else {
    finalName = singleLineTitle || 'Google Slide Generator Presentation';
  }

  
  let presentation;
  let isUpdating = false;
  
  
  
  
  
  var __wantNativeSection = !!(settings && settings.nativeSectionMode === true);
  var __wantNativeDecor = !!(settings && settings.nativeDecorMode === true);
  var __hasSectionForNative = Array.isArray(slideData) && slideData.some(function (d) { return d && d.type === 'section'; });
  var __hasBarForNative = __wantNativeDecor && Array.isArray(slideData)
      && slideData.some(function (d) { return d && (typeof slideHasBottomBarNative === 'function') && slideHasBottomBarNative(d.type, settings); });
  
  var __hasTriangleForNative = __wantNativeDecor && Array.isArray(slideData)
      && slideData.some(function (d) { return d && d.type === 'triangle'; });
  
  var __wantNativeChart = !!(settings && settings.nativeChartMode === true);
  var __hasChartForNative = __wantNativeChart && Array.isArray(slideData)
      && slideData.some(function (d) { return d && d.image && typeof d.image === 'object' && (d.image.info === 'chart' || d.image.info === 'svg_code') && d.image.nativeSvg; });
  
  var __wantNativeTemplate = __TEMPLATE_TRY_NATIVE; 
  
  if ((__wantNativeSection || __wantNativeDecor || __wantNativeChart || __wantNativeTemplate || !!__SEED_TEMPLATE_ID || __SEED_USE_EMBEDDED) && !__TEMPLATE_SLIDES && !__TEMPLATE_BASE_IS_COPY && (typeof Drive !== 'undefined' && !!Drive && !!Drive.Files) && (__hasSectionForNative || __hasBarForNative || __hasTriangleForNative || __hasChartForNative || __wantNativeTemplate || !!__SEED_TEMPLATE_ID || __SEED_USE_EMBEDDED)) {
    try {
      presentation = perfTime('nativeBase(create)', function () {
        
        
        
        var __settingsForNative = __SEED_USE_EMBEDDED
          ? Object.assign({}, settings, { useEmbeddedSeed: true })
          : (__SEED_TEMPLATE_ID ? Object.assign({}, settings, { seedTemplateId: __SEED_TEMPLATE_ID }) : settings);
        return _createNativeBasePresentation(slideData, __settingsForNative, finalName, presentationId);
      });
      __NATIVE_SECTION_ACTIVE = true;            
      __NATIVE_DECOR_ACTIVE = __wantNativeDecor; 
      __NATIVE_CHART_ACTIVE = __wantNativeChart;  
      
      
      if (settings.driveFolderId && settings.driveFolderId.trim()) {
        try { DriveApp.getFileById(presentation.getId()).moveTo(DriveApp.getFolderById(settings.driveFolderId.trim())); }
        catch (e) { Logger.log('フォルダ移動エラー: ' + e.message); }
      }
      Logger.log('[native] ネイティブ装飾土台を新規作成しました。 Name: ' + finalName);
    } catch (eNative) {
      __NATIVE_SECTION_ACTIVE = false;
      __NATIVE_DECOR_ACTIVE = false;
      __NATIVE_CHART_ACTIVE = false;
      presentation = null;
      Logger.log('[native] ネイティブ土台作成に失敗。通常フローにフォールバックします: ' + eNative.message);
      
      try {
        PropertiesService.getScriptProperties().setProperty(
          'LAST_NATIVE_ERROR',
          new Date().toISOString() + '\n[_createNativeBasePresentation]\n' +
          (eNative && eNative.message ? eNative.message : String(eNative)) + '\n' +
          (eNative && eNative.stack ? eNative.stack : '')
        );
      } catch (_) {}
      
      
      if (__TEMPLATE_TRY_NATIVE) {
        try {
          var __fbM = String(settings.templateUrl).trim().match(/[-\w]{25,}/);
          var __fbSrcId = __fbM ? __fbM[0] : null;
          if (__fbSrcId) {
            var __fbCopy = DriveApp.getFileById(__fbSrcId).makeCopy('まじん生成(テンプレ土台/フォールバック)');
            var __fbOldEmpty = presentationId;
            presentationId = __fbCopy.getId();
            __TEMPLATE_BASE_IS_COPY = true;
            if (__fbOldEmpty && __fbOldEmpty !== presentationId) { try { DriveApp.getFileById(__fbOldEmpty).setTrashed(true); } catch (eFbT) {} }
            Logger.log('[native→makeCopy] ネイティブ失敗→テンプレmakeCopy土台へフォールバック: ' + presentationId);
          }
        } catch (eFb) {
          __TEMPLATE_BASE_IS_COPY = false;
          Logger.log('[native→makeCopy] makeCopyフォールバックも失敗→標準レイアウトで生成: ' + ((eFb && eFb.message) || eFb));
        }
      }
    }
  }
  if (!__NATIVE_SECTION_ACTIVE && presentationId) {
    try {
      const file = DriveApp.getFileById(presentationId);
      presentation = SlidesApp.openById(file.getId());
      presentation.setName(finalName);

      
      
      
      if (__TEMPLATE_BASE_IS_COPY) {
        var __copySlides = presentation.getSlides();
        __TEMPLATE_SLIDES = {
          title:   __copySlides[0] || null,
          section: __copySlides[1] || null,
          content: __copySlides[2] || null,
          closing: __copySlides[3] || null,
        };
        Logger.log('[テンプレ] 土台デッキの先頭' + Math.min(4, __copySlides.length) + '枚をタイプ別テンプレに割当');
      }

      
      
      
      if (settings.driveFolderId && settings.driveFolderId.trim()) {
        
        try {
          
          file.moveTo(DriveApp.getFolderById(settings.driveFolderId.trim()));
          Logger.log(`(Update) プレゼンテーションをフォルダ (ID: ${settings.driveFolderId.trim()}) に移動しました。`);
        } catch (moveError) {
          Logger.log(`(Update) 指定フォルダへの移動エラー: ${moveError.message}`);
          
        }
      } else {
        
        try {
          
          file.moveTo(DriveApp.getRootFolder());
          Logger.log(`(Update) プレゼンテーションを "マイドライブ" (ルート) に移動しました。`);
        } catch (moveError) {
          
          Logger.log(`(Update) ルートフォルダへの移動エラー: ${moveError.message}`);
        }
      }
      

      isUpdating = true; 
      Logger.log(`既存のプレゼンテーション (ID: ${presentationId}) を更新します。`);
    } catch (e) {
      Logger.log(`既存スライド(ID: ${presentationId})が開けません。新規作成します。Error: ${e.message}`);
      presentation = SlidesApp.create(finalName);
      
      const defaultSlide = presentation.getSlides()[0];
      if (defaultSlide) {
        defaultSlide.remove();
      }
      
      if (settings.driveFolderId && settings.driveFolderId.trim()) {
        try {
          DriveApp.getFileById(presentation.getId()).moveTo(DriveApp.getFolderById(settings.driveFolderId.trim()));
        } catch (moveError) {
          Logger.log(`フォルダ移動エラー: ${moveError.message}`);
        }
      }
    }
  } else if (!__NATIVE_SECTION_ACTIVE) {
    presentation = SlidesApp.create(finalName);
    const defaultSlide = presentation.getSlides()[0];
    if (defaultSlide) {
      defaultSlide.remove();
    }
    if (settings.driveFolderId && settings.driveFolderId.trim()) {
      try {
        DriveApp.getFileById(presentation.getId()).moveTo(DriveApp.getFolderById(settings.driveFolderId.trim()));
      } catch (e) {
        Logger.log(`フォルダ移動エラー: ${e.message}`);
      }
    }
    Logger.log(`新規プレゼンテーションを作成しました。 Name: ${finalName}`);
  }

  
  __SECTION_COUNTER = 0;
  const layout = createLayoutManager(presentation.getPageWidth(), presentation.getPageHeight(), getContentMarginsPx(settings));
  let pageCounter = 0; 
  let __createdNewSlide = false; 
  let __deckCommittedAndClosed = false; 
  const existingSlides = presentation.getSlides();
  const numExistingSlides = existingSlides.length;
  const numNewSlides = slideData.length;
  __REST_PRESENTATION_ID = presentation.getId(); 
  
  
  
  
  __SLIDES_API_OK = (typeof Slides !== 'undefined' && !!Slides && !!Slides.Presentations) && !__isFallbackRetry;
  __REST_TEXT_ACTIVE = !!(settings && settings.restTextMode === true) && __SLIDES_API_OK;
  perfAdd('restTextMode', __REST_TEXT_ACTIVE ? 1 : 0);

  
  
  
  __CREDIT_IMAGE_URL = null;
  __CREDIT_TEMP_FILE_ID = null;
  __CREDIT_REST_REFS = [];
  if (__CREDIT_IMAGE_BLOB) {
    try {
      
      try { __CREDIT_IMAGE_BLOB.setName('majin_credit_temp.png'); } catch (eNm) {}
      const _cf = DriveApp.createFile(__CREDIT_IMAGE_BLOB);
      _cf.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
      __CREDIT_TEMP_FILE_ID = _cf.getId();
      __CREDIT_IMAGE_URL = 'https://drive.google.com/uc?export=view&id=' + __CREDIT_TEMP_FILE_ID;
      Logger.log('[createPresentation] credit画像をREST用に一時公開URL化: ' + __CREDIT_TEMP_FILE_ID);
    } catch (e) {
      Logger.log('[createPresentation] credit画像のURL化に失敗(SlidesAppにフォールバック): ' + e.message);
      __CREDIT_IMAGE_URL = null;
      __CREDIT_TEMP_FILE_ID = null;
    }
  }

  
  
  if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID && imageUpdateOption === 'update') {
    if (CONFIG.LOGOS.header) __HEADER_LOGO_SRC = prepareRestImageSource(CONFIG.LOGOS.header, 'majin_logo_temp.png');
    if (CONFIG.LOGOS.closing) __CLOSING_LOGO_SRC = prepareRestImageSource(CONFIG.LOGOS.closing, 'majin_logo_closing_temp.png');
    Logger.log('[createPresentation] ロゴのREST化: header=' + (__HEADER_LOGO_SRC ? 'あり' : 'なし')
      + ' closing=' + (__CLOSING_LOGO_SRC ? 'あり' : 'なし'));
  }

  
  
  
  
  const recreateForTemplate = isUpdating && (!!__TEMPLATE_SLIDES || __SEED_BASE_ACTIVE);
  if (recreateForTemplate) {
    Logger.log(`テンプレート設定あり: 既存 ${numExistingSlides} 枚を作り直してテンプレートを反映します。`);
  }

  const __RUN_TIME_LIMIT_MS = 5.2 * 60 * 1000; 
  for (let i = 0; i < numNewSlides; i++) {
    
    if (Date.now() - __RUN_START_MS > __RUN_TIME_LIMIT_MS) {
      try { presentation.saveAndClose(); } catch (eSc) {}
      try { restFlush(); } catch (eFl) {}
      throw new Error('スライド枚数が多く実行時間制限(6分)に近づいたため、' + i + '枚目で中断しました。枚数を減らして再生成してください。');
    }
    const data = slideData[i];
    let slide;
    let slideAction = ''; 

    
    if (data.type !== 'title' && data.type !== 'closing') {
      pageCounter++;
    }

    let generator = slideGenerators[data.type]; 
    
    
    if (!generator) {
      Logger.log(`Warning: No generator found for type "${data.type}". Using fallback slide.`);
      
      generator = createUnsupportedTypeSlide;
    }

    try {
      if (__NATIVE_SECTION_ACTIVE && i < numExistingSlides) {
        
        
        
        slide = existingSlides[i];
        
        __CURRENT_SLIDE_USES_TEMPLATE = !!(settings && settings.templateUrl); 
        slideAction = 'native-reused';
      } else if (isUpdating && !recreateForTemplate && i < numExistingSlides) {
        
        slide = existingSlides[i];
        
        perfTime('slideBase(clear)', () => clearSlideContents(slide, imageUpdateOption));
        
        __CURRENT_SLIDE_USES_TEMPLATE = false;
        slideAction = 'updated';
      } else {
        
        
        slide = perfTime('slideBase(create)', () => createBaseSlide(presentation, data.type));
        slideAction = 'created';
        __createdNewSlide = true; 
      }

      
      perfTime(`gen:${data.type}`, () => generator(slide, data, layout, pageCounter, settings, imageUpdateOption)); 

      
      if (data.notes) {
        perfTime('notes', () => {
          const cleanedNotes = cleanSpeakerNotes(data.notes);
          slide.getNotesPage().getSpeakerNotesShape().getText().setText(cleanedNotes);
        });
      }
      Logger.log(`Slide ${i + 1} (${data.type}) ${slideAction}. Page counter: ${pageCounter}`);

    } catch (e) {
      Logger.log(`Slide generation failed for index ${i} (Type: ${data.type}): ${e.message}\nStack: ${e.stack}`);
      
      if (slide && slideAction === 'created') {
        
        
        
        
        try { _restDropRequestsForSlide(slide.getObjectId()); } catch (e2) {}
        try { slide.remove(); } catch (removeError) {}
        
        __TPL_DUP_SLIDE_COUNT = -1;
      } else if (slide && slideAction === 'updated') {
        
        try {
          clearSlideContents(slide, 'update'); 
          const errorBox = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 50, 50, 400, 100);
          errorBox.getText().setText(`Error generating this slide:\n${e.message}`);
        } catch (errorBoxError) {}
      }
    }
  }

  
  if (recreateForTemplate) {
    
    Logger.log(`テンプレート更新: 元スライド ${numExistingSlides} 枚を削除して作り直します。`);
    for (let di = 0; di < numExistingSlides; di++) {
      try {
        existingSlides[di].remove();
      } catch (removeError) {
        Logger.log(`元スライド ${di + 1} の削除に失敗: ${removeError.message}`);
      }
    }
  } else if (isUpdating && numExistingSlides > numNewSlides) {
    Logger.log(`削除対象のスライド数: ${numExistingSlides - numNewSlides}`);
    for (let i = numExistingSlides - 1; i >= numNewSlides; i--) {
      try {
        existingSlides[i].remove();
        Logger.log(`余分なスライド ${i + 1} を削除しました。`);
      } catch (removeError) {
        Logger.log(`スライド ${i + 1} の削除に失敗: ${removeError.message}`);
      }
    }
  }

  
  
  
  
  
  
  
  
  
  
  
  
  if (__REST_PRESENTATION_ID && __REST_REQUESTS && __REST_REQUESTS.length > 0) {
    try {
      const __ps = Date.now();
      presentation.saveAndClose();
      
      
      __deckCommittedAndClosed = true;
      perfAdd('restCommit(saveAndClose)', Date.now() - __ps);
    } catch (e) {
      Logger.log('[REST] saveAndClose(コミット)失敗、batchUpdateが失敗する可能性: ' + e.message);
    }
  }
  
  
  
  try {
    restFlush();
  } catch (eFlush) {
    Logger.log(`[REST] 最終flush失敗: ${eFlush && eFlush.message}`);
    try {
      PropertiesService.getScriptProperties().setProperty(
        'LAST_REST_FLUSH_FALLBACK',
        `${new Date().toISOString()}\n[restFlush fallback] ${(eFlush && eFlush.message) || eFlush}`
      );
    } catch (_) {}
    if (!__isFallbackRetry && __REST_PRESENTATION_ID) {
      Logger.log('[REST] restFlush失敗 → 純SlidesAppパスで自動フォールバック再生成(同一ID維持・1回限り)');
      
      try { if (__CREDIT_TEMP_FILE_ID) { DriveApp.getFileById(__CREDIT_TEMP_FILE_ID).setTrashed(true); } } catch (_) {}
      __CREDIT_TEMP_FILE_ID = null;
      __CREDIT_IMAGE_URL = null;
      __CREDIT_REST_REFS = [];
      
      (__REST_IMAGE_TEMP_IDS || []).forEach((fid) => { try { DriveApp.getFileById(fid).setTrashed(true); } catch (_) {} });
      __REST_IMAGE_TEMP_IDS = [];
      __HEADER_LOGO_SRC = null;
      __CLOSING_LOGO_SRC = null;
  __HEADER_LOGO_ASPECT = null;
      
      
      const __fbSettings = {};
      for (const __k in settings) { if (Object.prototype.hasOwnProperty.call(settings, __k)) __fbSettings[__k] = settings[__k]; }
      __fbSettings.restTextMode = false;
      __fbSettings.nativeSectionMode = false;
      __fbSettings.nativeDecorMode = false;
      __fbSettings.nativeChartMode = false;
      return createPresentation(slideData, __fbSettings, __REST_PRESENTATION_ID, 'update', true);
    }
    
    throw eFlush;
  }

  
  if (__CREDIT_REST_REFS && __CREDIT_REST_REFS.length > 0) {
    const __lt = Date.now();
    let __linkOk = 0, __linkNull = 0;
    try {
      
      const freshPres = SlidesApp.openById(__REST_PRESENTATION_ID);
      const idsBySlide = {};
      __CREDIT_REST_REFS.forEach((ref) => {
        const sid = ref.slide.getObjectId();
        (idsBySlide[sid] = idsBySlide[sid] || []).push(ref.objectId);
      });
      freshPres.getSlides().forEach((s) => {
        const ids = idsBySlide[s.getObjectId()];
        if (!ids) return;
        ids.forEach((oid) => {
          try {
            const el = s.getPageElementById(oid);
            if (el) { el.asImage().setLinkUrl(CREDIT_IMAGE_LINK); __linkOk++; }
            else { __linkNull++; }
          } catch (e) { __linkNull++; }
        });
      });
    } catch (e) {
      Logger.log('[createPresentation] creditリンク後付け(再open)失敗: ' + e.message);
    }
    perfAdd('credit:link', Date.now() - __lt);
    perfAdd('credit:linkOk', __linkOk);
    perfAdd('credit:linkNull', __linkNull);
    __CREDIT_REST_REFS = [];
  }
  
  if (__CREDIT_TEMP_FILE_ID) {
    try { DriveApp.getFileById(__CREDIT_TEMP_FILE_ID).setTrashed(true); } catch (e) { Logger.log('[createPresentation] credit一時ファイル削除失敗: ' + e.message); }
    __CREDIT_TEMP_FILE_ID = null;
    __CREDIT_IMAGE_URL = null;
  }
  
  if (__REST_IMAGE_TEMP_IDS && __REST_IMAGE_TEMP_IDS.length) {
    __REST_IMAGE_TEMP_IDS.forEach((fid) => {
      try { DriveApp.getFileById(fid).setTrashed(true); } catch (e) { Logger.log('[createPresentation] ロゴ一時ファイル削除失敗: ' + e.message); }
    });
    __REST_IMAGE_TEMP_IDS = [];
    __HEADER_LOGO_SRC = null;
    __CLOSING_LOGO_SRC = null;
  }
  
  
  const url = __deckCommittedAndClosed
    ? ('https://docs.google.com/presentation/d/' + __REST_PRESENTATION_ID + '/edit')
    : perfTime('finalize(getUrl)', () => presentation.getUrl());
  perfLog(`createPresentation (枚数=${numNewSlides}, ${isUpdating ? '更新' : '新規'})`);
  return url;
}


function createUnsupportedTypeSlide(slide, data, layout, pageNum, settings, imageUpdateOption) {
  
  slide.getBackground().setSolidFill('#FFFFFF');

  
  const pageWidth = layout.pageW_pt;
  const pageHeight = layout.pageH_pt;

  
  const titleBox = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 0, 0, pageWidth, pageHeight);
  
  const textRange = titleBox.getText();
  textRange.setText(`⚠️ 未定義のスライドタイプが選択されています\n\n指定されたタイプ: "${data.type}"\nGeminiでスライド構成データを再度作成してください。`);
  
  
  textRange.getTextStyle()
    .setFontSize(24)
    .setForegroundColor('#FF0000') 
    .setBold(true)
    .setFontFamily(settings.fontFamily || 'Noto Sans JP');
    
  
  titleBox.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
  
  
  textRange.getParagraphStyle().setParagraphAlignment(SlidesApp.ParagraphAlignment.CENTER);

  
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}


function clearSlideContents(slide, imageUpdateOption) {
  const pageElements = slide.getPageElements();
  pageElements.forEach(element => {
    try {
      if (imageUpdateOption === 'keep' && element.getPageElementType() === SlidesApp.PageElementType.IMAGE) {
        
        Logger.log(`Keeping image element: ${element.getObjectId()}`);
      } else {
        
        element.remove();
      }
    } catch (e) {
      Logger.log(`Error removing page element ${element.getObjectId()}: ${e.message}`);
    }
  });
   
  try {
    slide.getBackground().setSolidFill('#FFFFFF'); 
  } catch(e) {
    Logger.log(`Error setting slide background: ${e.message}`);
  }
}


function updateSingleSlide(presentationId, slideIndex, singleSlideDataString, settings, imageUpdateOption) {
  if (!presentationId || slideIndex == null || slideIndex < 0 || !singleSlideDataString) {
    return { status: 'error', message: '無効なパラメータが指定されました。' };
  }

  try {
    
    resetRunState_();
    
    
    __CURRENT_SLIDE_USES_TEMPLATE = false;

    
    __CREDIT_IMAGE_BLOB = null;
    if (settings.creditImageBase64) {
      try {
        const parts = settings.creditImageBase64.split(',');
        if (parts.length === 2) {
          const mimeType = parts[0].match(/:(.*?);/)[1];
          const decodedData = Utilities.base64Decode(parts[1]);
          __CREDIT_IMAGE_BLOB = Utilities.newBlob(decodedData, mimeType, 'credit.png');
        }
      } catch (e) {}
    }

    
    let presentation;
    try {
      presentation = SlidesApp.openById(presentationId);
    } catch (e) {
      return { status: 'error', message: `プレゼンテーション (ID: ${presentationId}) を開けませんでした: ${e.message}` };
    }

    const slides = presentation.getSlides();
    if (slideIndex >= slides.length) {
      return { status: 'error', message: `指定されたページ番号 (${slideIndex + 1}) のスライドが存在しません。` };
    }
    const slide = slides[slideIndex];

    
    let newData;
    try {
      newData = JSON.parse(singleSlideDataString);
    } catch (e) {
      return { status: 'error', message: `スライドデータのJSON形式が不正です: ${e.message}` };
    }

    if (!newData || !newData.type) {
       return { status: 'error', message: 'スライドデータに type プロパティが含まれていません。' };
    }

    
    updateDynamicColors(settings);
    
    CONFIG.COLORS.primary_color = settings.primaryColor || CONFIG.COLORS.primary_color;
    CONFIG.COLORS.text_primary = settings.largeFontColor || '#333333';
    CONFIG.COLORS.text_small_font = settings.smallFontColor || '#1F2937';
    CONFIG.COLORS.background_white = (settings.backgroundColor && settings.backgroundColor.toUpperCase() !== '#FFFFFF') ? settings.backgroundColor : '#FFFFFF';
    CONFIG.FOOTER_TEXT = settings.footerText;
    CONFIG.FONTS.family = settings.fontFamily || CONFIG.FONTS.family;
    CONFIG.LOGOS.header = settings.headerLogoUrl;
    CONFIG.LOGOS.closing = settings.closingLogoUrl;
    CONFIG.BACKGROUND_IMAGES.title = settings.titleBgUrl;
    CONFIG.BACKGROUND_IMAGES.closing = settings.closingBgUrl;
    CONFIG.BACKGROUND_IMAGES.section = settings.sectionBgUrl;
    CONFIG.BACKGROUND_IMAGES.main = settings.mainBgUrl;

    const layout = createLayoutManager(presentation.getPageWidth(), presentation.getPageHeight(), getContentMarginsPx(settings));
    restReset(presentation.getId()); 

    
    
    
    
    
    var __b1DecoRec = null;
    try { __b1DecoRec = _loadNativeDecoRecord(presentationId); } catch (eB1Load) { __b1DecoRec = null; }
    
    var __b1HasNativeChart = !!(newData && newData.image && typeof newData.image === 'object'
        && (newData.image.info === 'chart' || newData.image.info === 'svg_code') && newData.image.nativeSvg);
    
    
    var __b1UseDecoPreserve = !!(__b1DecoRec && Array.isArray(__b1DecoRec.d)
        && __b1DecoRec.d.length === slides.length
        && __b1DecoRec.d[slideIndex]
        && typeof __b1DecoRec.dm === 'boolean'
        && !__b1HasNativeChart);

    
    
    function __doClearForUpdate() {
      if (__b1UseDecoPreserve) {
        
        var __b1KeepIds = {};
        (__b1DecoRec.d[slideIndex] || []).forEach(function (oid) { if (oid) __b1KeepIds[String(oid)] = true; });
        slide.getPageElements().forEach(function (element) {
          try {
            var __oid = String(element.getObjectId());
            if (__b1KeepIds[__oid]) return; 
            if (imageUpdateOption === 'keep' && element.getPageElementType() === SlidesApp.PageElementType.IMAGE) {
              Logger.log(`Keeping image element: ${__oid}`); 
              return;
            }
            element.remove();
          } catch (eDel) {
            Logger.log(`Error removing page element (B1 deco-preserve): ${eDel.message}`);
          }
        });
        
        
        __NATIVE_SECTION_ACTIVE = true;
        
        
        __NATIVE_DECOR_ACTIVE = __b1DecoRec.dm === true;
        __NATIVE_CHART_ACTIVE = false; 
        __CURRENT_SLIDE_USES_TEMPLATE = !!(settings && settings.templateUrl && String(settings.templateUrl).trim());
        Logger.log(`[B1] 装飾保存クリアで単一ページ更新(テンプレ/ネイティブ装飾を維持): slideIndex=${slideIndex}`);
      } else {
        
        clearSlideContents(slide, imageUpdateOption);
      }
    }
    __doClearForUpdate();

    const generator = slideGenerators[newData.type];
    if (!generator) {
      return { status: 'error', message: `スライドタイプ "${newData.type}" に対応する生成関数が見つかりません。` };
    }

    let pageCounterForFooter = 0;
    if (newData.type !== 'title' && newData.type !== 'closing') {
        pageCounterForFooter = slideIndex + 1;
    }

    generator(slide, newData, layout, pageCounterForFooter, settings, imageUpdateOption);
    
    try {
      restFlush(); 
    } catch (e) {
      Logger.log(`[REST] flush失敗。SlidesApp経路で再描画を試みます: ${e.message}`);
      try { restReset(presentation.getId()); } catch (eRs) {} 
      
      __SLIDES_API_OK = false;
      __REST_TEXT_ACTIVE = false;
      try {
        __doClearForUpdate(); 
        generator(slide, newData, layout, pageCounterForFooter, settings, imageUpdateOption);
      } catch (e2) {
        Logger.log(`[REST] SlidesApp経路の再描画も失敗: ${e2 && e2.message}`);
        return { status: 'error', message: 'ページ更新に失敗しました: ' + (e2 && e2.message) };
      }
    }

    
    if (newData.notes) {
      const cleanedNotes = cleanSpeakerNotes(newData.notes);
      slide.getNotesPage().getSpeakerNotesShape().getText().setText(cleanedNotes);
    } else {
      slide.getNotesPage().getSpeakerNotesShape().getText().setText('');
    }

    
    
    
    try {
       
       const loadResult = loadLatestGenerationState(presentationId);
       
       let fullSlideDataToSave = [];

       if (loadResult.status === 'success' && loadResult.data && Array.isArray(loadResult.data.slideData)) {
         
         fullSlideDataToSave = JSON.parse(JSON.stringify(loadResult.data.slideData)); 
         
         
         
         if (slideIndex < fullSlideDataToSave.length) {
           fullSlideDataToSave[slideIndex] = newData;
           Logger.log(`[Metadata] Merged new data into index ${slideIndex} of existing history.`);
         } else {
           
           fullSlideDataToSave = [newData]; 
           Logger.log(`[Metadata] Index out of bounds. Fallback to partial save.`);
         }
       } else {
         
         Logger.log(`[Metadata] No history found. Saving partial data.`);
         fullSlideDataToSave = [newData]; 
         
         
       }

       
       saveGenerationState(presentationId, fullSlideDataToSave, settings);

    } catch (e) {
       Logger.log("メタデータ保存呼び出しエラー(Update): " + e.message);
    }
    

    Logger.log(`Slide index ${slideIndex} (Type: ${newData.type}) updated successfully.`);

    addToUserHistory(presentationId);

    return { status: 'success', message: `ページ ${slideIndex + 1} を更新しました。` };

  } catch (e) {
    Logger.log(`Error in updateSingleSlide (Index: ${slideIndex}): ${e.message}\nStack: ${e.stack}`);
    return { status: 'error', message: `ページ ${slideIndex + 1} の更新中にエラーが発生しました: ${e.message}` };
  }
}









const slideGenerators = {
  title: createTitleSlide,
  section: createSectionSlide,
  content: createContentSlide,
  agenda: createAgendaSlide,
  compare: createCompareSlide,
  process: createProcessSlide,
  processList: createProcessListSlide,
  timeline: createTimelineSlide,
  diagram: createDiagramSlide,
  cycle: createCycleSlide,
  cards: createCardsSlide,
  headerCards: createHeaderCardsSlide,
  table: createTableSlide,
  progress: createProgressSlide,
  quote: createQuoteSlide,
  kpi: createKpiSlide,
  closing: createClosingSlide,
  bulletCards: createBulletCardsSlide,
  faq: createFaqSlide,
  statsCompare: createStatsCompareSlide,
  barCompare: createBarCompareSlide,
  triangle: createTriangleSlide,
  pyramid: createPyramidSlide,
  flowChart: createFlowChartSlide,
  stepUp: createStepUpSlide,
  imageText: createImageTextSlide,
  fullImage: createFullImageSlide,
  integratedContent: createIntegratedContentSlide,
  integratedTitle: createIntegratedTitleSlide,
  splitImage: createSplitImageSlide
};





function getContentMarginsPx(settings) {
  const num = (v) => {
    const n = parseFloat(v);
    return (isFinite(n) && n > 0) ? n : 0;
  };
  return {
    top:    num(settings && settings.marginTop),
    bottom: num(settings && settings.marginBottom),
    left:   num(settings && settings.marginLeft),
    right:  num(settings && settings.marginRight),
  };
}

function createLayoutManager(pageW_pt, pageH_pt, marginsPx) {
  const pxToPt = (px) => px * 0.75;
  const baseW_pt = pxToPt(CONFIG.BASE_PX.W),
    baseH_pt = pxToPt(CONFIG.BASE_PX.H);

  
  const m = marginsPx || {};
  const mLeft   = pxToPt(m.left   > 0 ? m.left   : 0);
  const mRight  = pxToPt(m.right  > 0 ? m.right  : 0);
  const mTop    = pxToPt(m.top    > 0 ? m.top    : 0);
  const mBottom = pxToPt(m.bottom > 0 ? m.bottom : 0);

  
  const offsetX = mLeft;
  const offsetY = mTop;
  const contentW = Math.max(1, pageW_pt - mLeft - mRight);
  const contentH = Math.max(1, pageH_pt - mTop - mBottom);

  
  const scaleX = contentW / baseW_pt,
    scaleY = contentH / baseH_pt;
  const getPositionFromPath = (path) => path.split('.').reduce((obj, key) => obj[key], CONFIG.POS_PX);

  return {
    scaleX,
    scaleY,
    pageW_pt,
    pageH_pt,
    pxToPt,
    
    offsetX_pt: offsetX,
    offsetY_pt: offsetY,
    contentW_pt: contentW,
    contentH_pt: contentH,
    contentRight_pt: offsetX + contentW,
    contentBottom_pt: offsetY + contentH,
    contentCenterX_pt: offsetX + contentW / 2,
    contentCenterY_pt: offsetY + contentH / 2,
    getRect: (spec) => {
      const pos = typeof spec === 'string' ? getPositionFromPath(spec) : spec;
      let left_px = pos.left;
      if (pos.right !== undefined && pos.left === undefined) {
        left_px = CONFIG.BASE_PX.W - pos.right - pos.width;
      }

      
      if (left_px === undefined && pos.right === undefined) {
        Logger.log(`Warning: Neither left nor right defined for spec: ${JSON.stringify(spec)}`);
        left_px = 0; 
      }

      return {
        left: left_px !== undefined ? offsetX + pxToPt(left_px) * scaleX : 0,
        top: pos.top !== undefined ? offsetY + pxToPt(pos.top) * scaleY : 0,
        width: pos.width !== undefined ? pxToPt(pos.width) * scaleX : 0,
        height: pos.height !== undefined ? pxToPt(pos.height) * scaleY : 0,
      };
    }
  };
}






function adjustAreaForSubhead(area, subhead, layout) {
  return area;
}


function createContentCushion(slide, area, settings, layout, opts) {
  if (!area || !area.width || !area.height || area.width <= 0 || area.height <= 0) {
    Logger.log(`[Warning] Invalid area for cushion was provided. Skipping creation. Area: ${JSON.stringify(area)}`);
    return;
  }

  
  
  
  
  
  if (opts && opts.rest === true && __REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
    const __cuSid = slide.getObjectId();
    restAddShape(__cuSid, { left: area.left, top: area.top, width: area.width, height: area.height }, 'RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), noOutline: true });
    return;
  }

  
  const cushionColor = CONFIG.COLORS.background_gray;
  const cushion = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, 
    area.left, area.top, area.width, area.height);
  
  cushion.getFill().setSolidFill(cushionColor, 1.0);
  
  
  const border = cushion.getBorder();
  border.setTransparent();
}


function createTitleSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setBackgroundImageFromUrl(slide, layout, CONFIG.BACKGROUND_IMAGES.title, CONFIG.COLORS.background_white, imageUpdateOption);
  if (imageUpdateOption === 'update') { 
    const logoRect = layout.getRect('titleSlide.logo');
    try {
      
      if (CONFIG.LOGOS.header && __HEADER_LOGO_SRC && __REST_PRESENTATION_ID && __SLIDES_API_OK) {
        restAddImage(slide.getObjectId(),
          { left: logoRect.left, top: logoRect.top,
            width: logoRect.width, height: logoRect.width * __HEADER_LOGO_SRC.aspect },
          __HEADER_LOGO_SRC.url);
      } else if (CONFIG.LOGOS.header) {
        const imageData = insertImageFromUrlOrFileId(CONFIG.LOGOS.header);
        if (imageData) {
          const logo = slide.insertImage(imageData);
          const aspect = logo.getHeight() / logo.getWidth();
          logo.setLeft(logoRect.left).setTop(logoRect.top).setWidth(logoRect.width).setHeight(logoRect.width * aspect);
        }
      }
    } catch (e) {
      Logger.log(`Title logo error: ${e.message}`);
    }
  }

  
  
  const titleRect = layout.getRect('titleSlide.title');
  
  
  const newTop = layout.offsetY_pt + (layout.contentH_pt - titleRect.height) / 2;
  
  
  const newWidth = titleRect.width + layout.pxToPt(60);
  
  
  const titleShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, titleRect.left, newTop, newWidth, titleRect.height);
  
  
  setStyledText(titleShape, data.title, {
    size: CONFIG.FONTS.sizes.title,
    bold: true,
    fontType: 'large'
  });
  
  try {
    titleShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); 
  } catch (e) {
    
  }
  
  try {
    

    
    
    const titleText = data.title || '';
    if (titleText.indexOf('\n') === -1) {
      
      Logger.log(`Title Slide: Title does not contain newline. Running adjustment logic.`); 

      
      const preCalculatedWidth = (data && typeof data._title_widthPt === 'number') ?
  data._title_widthPt : null;

      
      if (preCalculatedWidth !== null && preCalculatedWidth < 900) {
        Logger.log(`Title Slide: preCalculatedWidth (${preCalculatedWidth.toFixed(1)}pt) is less than 900pt. Running adjustment.`);
  
        
        const adjustmentLog = adjustShapeText_External(titleShape, preCalculatedWidth);
  Logger.log(`Title Slide Title Adjustment Result: ${adjustmentLog.log}`); 
      } else if (preCalculatedWidth !== null) {
        
        Logger.log(`Title Slide: preCalculatedWidth (${preCalculatedWidth.toFixed(1)}pt) is 900pt or greater. Skipping adjustment.`);
  
        
      } else {
        
        Logger.log(`Title Slide: preCalculatedWidth not available. Running adjustment with internal estimation.`);
  
        const adjustmentLog = adjustShapeText_External(titleShape, null);
  
        Logger.log(`Title Slide Title Adjustment Result (Fallback): ${adjustmentLog.log}`); 
  }
      

    } else {
      
      Logger.log(`Title Slide: Title contains newline character (\\n). Skipping font adjustment logic.`);
    }
    

  } catch (e) {
    Logger.log(`Error during title slide title adjustment logic: ${e.message}`);
  }

  try {
    const titleTextRange = titleShape.getText();
    if (!titleTextRange.isEmpty()) {
      
      const firstRun = titleTextRange.getRuns()[0];
      if (firstRun) {
        const currentFontSize = firstRun.getTextStyle().getFontSize();
        
        
        if (currentFontSize === 41) {
          Logger.log('Title font size is 41pt. Adjusting to 40pt.');
          
          titleTextRange.getTextStyle().setFontSize(40);
        }
      }
    }
  } catch (e) {
    Logger.log('Error during 41pt to 40pt font size adjustment: ' + e.message);
  }
  
  
  if (settings.showDateColumn) {
    const dateRect = layout.getRect('titleSlide.date');
    
    _insertTextBox(slide, dateRect, data.date || '', {
      size: CONFIG.FONTS.sizes.date,
      fontType: 'large'
    });
  }
  
  if (settings.showBottomBar) {
    drawBottomBar(slide, layout, settings);
  }

  if (__CREDIT_IMAGE_BLOB || __CREDIT_TEXT) {
    drawCreditImage(slide, layout, __CREDIT_IMAGE_BLOB, CREDIT_IMAGE_LINK);
  }

}



function createSectionSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  
  
  const imageUrl = CONFIG.BACKGROUND_IMAGES.section || CONFIG.BACKGROUND_IMAGES.main || '';
  
  let fallbackColor;

  if (imageUrl) {
    
    
    fallbackColor = CONFIG.COLORS.background_white;
  } else {
    
    if (CONFIG.COLORS.background_white.toUpperCase() !== '#FFFFFF') {
      
      fallbackColor = CONFIG.COLORS.background_white;
    } else {
      
      fallbackColor = CONFIG.COLORS.background_gray;
    }
  }

  
  perfTime('sec:bg', () => setBackgroundImageFromUrl(slide, layout, imageUrl, fallbackColor, imageUpdateOption));
  
  const __secT0 = Date.now(); 
  __SECTION_COUNTER++;
  
  const parsedNum = (() => {
    if (Number.isFinite(data.sectionNo)) {
      return Number(data.sectionNo);
    }
    const m = String(data.title || '').match(/^\s*(\d+)[\.．]/);
    return m ? Number(m[1]) : __SECTION_COUNTER;
  })();
  const num = String(parsedNum).padStart(2, '0');
  const ghostRect = layout.getRect('sectionSlide.ghostNum');
  let ghostImageInserted = false;
  
  
  
  
  if (!__NATIVE_SECTION_ACTIVE && imageUpdateOption === 'update') { 
    if (data.ghostImageBase64 && ghostRect) {
      try {
        
        const imageData = insertImageFromUrlOrFileId(data.ghostImageBase64);
        if (imageData) {
          
          const __gt = Date.now();
          const ghostImage = slide.insertImage(imageData);
          perfAdd('ghost:insert', Date.now() - __gt);
          
          const __gd = Date.now();
          const imgWidth = ghostImage.getWidth();
          const imgHeight = ghostImage.getHeight();
          perfAdd('ghost:dims', Date.now() - __gd);
          const scale = Math.min(ghostRect.width / imgWidth, ghostRect.height / imgHeight);
          const w = imgWidth * scale;
          const h = imgHeight * scale;

          
          const __gs = Date.now();
          ghostImage.setWidth(w).setHeight(h)
            .setLeft(ghostRect.left + (ghostRect.width - w) / 2)
            .setTop(ghostRect.top + (ghostRect.height - h) / 2);
          perfAdd('ghost:setter', Date.now() - __gs);
          ghostImageInserted = true;
          perfAdd('ghostPath:image', 0); 
        }
      } catch (e) {
        Logger.log(`Failed to insert ghost number image for section ${num}: ${e.message}`);
        
      }
    }
  }

  
  
  
  
  
  
  
  if (!__NATIVE_SECTION_ACTIVE && !ghostImageInserted && ghostRect && imageUpdateOption === 'update') {
    try {
      const ghost = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, ghostRect.left, ghostRect.top, ghostRect.width, ghostRect.height);
      ghost.getText().setText(num);
      const ghostTextStyle = ghost.getText().getTextStyle();
      ghostTextStyle.setFontFamily(CONFIG.FONTS.family)
        .setFontSize(CONFIG.FONTS.sizes.ghostNum)
        .setBold(true);
      try {
        ghostTextStyle.setForegroundColorWithAlpha(CONFIG.COLORS.ghost_gray, 0.15);
      } catch (e2) {
        ghostTextStyle.setForegroundColor(CONFIG.COLORS.ghost_gray);
      }
      try {
        ghost.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
      } catch (e2) {}
      perfAdd('ghostPath:slidesApp', 0); 
    } catch (e) {
      Logger.log(`ゴースト数字の描画に失敗: ${e.message}`);
      perfAdd('ghostPath:error', 0);
    }
  }
  
  perfAdd('sec:ghost', Date.now() - __secT0); 
  const __secT1 = Date.now();

  const titleRect = layout.getRect('sectionSlide.title');
  const titleShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, titleRect.left, titleRect.top, titleRect.width, titleRect.height);
  titleShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
  setStyledText(titleShape, data.title, {
    size: CONFIG.FONTS.sizes.sectionTitle,
    bold: true,
    align: SlidesApp.ParagraphAlignment.CENTER,
    fontType: 'large'
  });
  perfAdd('sec:titleCreate', Date.now() - __secT1); 
  const __secT2 = Date.now();
  
  try {
    
    const titleText = data.title ||
    '';
    if (titleText.indexOf('\n') === -1) {

      Logger.log(`Section Slide: Title does not contain newline. Running adjustment logic.`);
      

      
      
      const preCalculatedWidthPt = (data && typeof data._title_widthPt === 'number') ?
      data._title_widthPt : null;

      
      const textAreaWidthPt = titleRect.width;
      
      
      if (textAreaWidthPt > 0 && (preCalculatedWidthPt === null || preCalculatedWidthPt < (textAreaWidthPt * 1.4))) {

        if (preCalculatedWidthPt !== null) {
          Logger.log(`Section Slide Title: preCalculatedWidth (${preCalculatedWidthPt.toFixed(1)}pt) is less than 1.4x textAreaWidth (${(textAreaWidthPt * 1.4).toFixed(1)}pt). Running adjustment.`);
        } else {
          Logger.log(`Section Slide Title: preCalculatedWidth not available. Running fallback adjustment.`);
        }

        
        
        
        const __secBase = CONFIG.FONTS.sizes.sectionTitle;
        const __secFinalSize = computeAutofitFontSize(titleText, __secBase, titleRect.width, titleRect.height, preCalculatedWidthPt, 0);
        if (__secFinalSize !== __secBase) {
          titleShape.getText().getTextStyle().setFontSize(__secFinalSize);
        }
        Logger.log(`Section Slide Title Adjustment(memory): ${__secBase}→${__secFinalSize}`);

      } else if (preCalculatedWidthPt !== null) {
        
        Logger.log(`Section Slide Title: preCalculatedWidth (${preCalculatedWidthPt.toFixed(1)}pt) is >= 1.4x textAreaWidth (${(textAreaWidthPt * 1.4).toFixed(1)}pt). Skipping adjustment.`);
      }
      

    } else {
      
      Logger.log(`Section Slide: Title contains newline character (\\n). Skipping font adjustment logic.`);
    }
  } catch (e) {
    Logger.log(`Error during section slide title adjustment: ${e.message}`);
  }
  
  perfAdd('sec:titleAdjust', Date.now() - __secT2); 

  perfTime('sec:footer', () => addCucFooter(slide, layout, pageNum, settings));
}




function createClosingSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setBackgroundImageFromUrl(slide, layout, CONFIG.BACKGROUND_IMAGES.closing, CONFIG.COLORS.background_white, imageUpdateOption);
  if (imageUpdateOption === 'update') { 
    try {
      const imgW_pt = layout.pxToPt(450) * layout.scaleX;
      
      if (CONFIG.LOGOS.closing && __CLOSING_LOGO_SRC && __REST_PRESENTATION_ID && __SLIDES_API_OK) {
        const h_pt = imgW_pt * __CLOSING_LOGO_SRC.aspect;
        restAddImage(slide.getObjectId(), {
          left: layout.offsetX_pt + (layout.contentW_pt - imgW_pt) / 2,
          top: layout.offsetY_pt + (layout.contentH_pt - h_pt) / 2,
          width: imgW_pt, height: h_pt
        }, __CLOSING_LOGO_SRC.url);
      } else if (CONFIG.LOGOS.closing) {
        const imageData = insertImageFromUrlOrFileId(CONFIG.LOGOS.closing);
        if (imageData) {
          const image = slide.insertImage(imageData);
          const aspect = image.getHeight() / image.getWidth();
          image.setWidth(imgW_pt).setHeight(imgW_pt * aspect);
          image.setLeft(layout.offsetX_pt + (layout.contentW_pt - imgW_pt) / 2).setTop(layout.offsetY_pt + (layout.contentH_pt - (imgW_pt * aspect)) / 2);
        }
      }
    } catch (e) {
      Logger.log(`Closing logo error: ${e.message}`);
    }
  }

  if (__CREDIT_IMAGE_BLOB || __CREDIT_TEXT) {
    drawCreditImage(slide, layout, __CREDIT_IMAGE_BLOB, CREDIT_IMAGE_LINK);
  }

}


function createContentSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout); 

  
  
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null;
  
  drawStandardTitleHeader(slide, layout, 'contentSlide', data.title, settings, titleWidthPt, imageUpdateOption);

  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'contentSlide', data.subhead, subheadWidthPt); 
  

  
  const isAgenda = isAgendaTitle(data.title || '');
  let points = Array.isArray(data.points) ? data.points.slice(0) : [];
  if (isAgenda && points.length === 0) {
    points = buildAgendaFromSlideData();
    if (points.length === 0) {
      points = ['本日の目的', '進め方', '次のアクション'];
    }
  }
  const hasImages = Array.isArray(data.images) && data.images.length > 0;
  const isTwo = !!(data.twoColumn || data.columns); 

  
  if ((isTwo && (data.columns || points)) || (!isTwo && points && points.length > 0)) {
    if (isTwo) {
      
      let L = [], R = [];
      if (Array.isArray(data.columns) && data.columns.length === 2) {
        L = data.columns[0] || [];
        R = data.columns[1] || [];
      } else { 
        const mid = Math.ceil(points.length / 2);
        L = points.slice(0, mid);
        R = points.slice(mid);
      }
      
      const baseLeftRect = layout.getRect('contentSlide.twoColLeft');
      const baseRightRect = layout.getRect('contentSlide.twoColRight');
      const adjustedLeftRect = adjustAreaForSubhead(baseLeftRect, data.subhead, layout);
      const adjustedRightRect = adjustAreaForSubhead(baseRightRect, data.subhead, layout);
      const leftRect = offsetRect(adjustedLeftRect, 0, dy);
      const rightRect = offsetRect(adjustedRightRect, 0, dy);

      
      const padding = layout.pxToPt(20);
      const leftTextRect = { left: leftRect.left + padding, top: leftRect.top + padding, width: leftRect.width - (padding * 2), height: leftRect.height - (padding * 2) };
      const rightTextRect = { left: rightRect.left + padding, top: rightRect.top + padding, width: rightRect.width - (padding * 2), height: rightRect.height - (padding * 2) };

      
      
      let __colsDone = false;
      if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
        const __colsQLen = __REST_REQUESTS.length;
        try {
          createContentCushion(slide, leftRect, settings, layout, { rest: true });
          createContentCushion(slide, rightRect, settings, layout, { rest: true });
          _restBulletsAutofitBox(slide, leftTextRect, L, { size: CONFIG.FONTS.sizes.body }, { boldRunSize: 16 });
          _restBulletsAutofitBox(slide, rightTextRect, R, { size: CONFIG.FONTS.sizes.body }, { boldRunSize: 16 });
          __colsDone = true;
        } catch (e) { __REST_REQUESTS.length = __colsQLen; Logger.log('[REST] content2カラム(座布団+本文)失敗→SlidesAppフォールバック: ' + e.message); }
      }
      if (!__colsDone) {
      
      createContentCushion(slide, leftRect, settings, layout);
      createContentCushion(slide, rightRect, settings, layout);
      
      const leftShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, leftTextRect.left, leftTextRect.top, leftTextRect.width, leftTextRect.height);
      const rightShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, rightTextRect.left, rightTextRect.top, rightTextRect.width, rightTextRect.height);
      setBulletsWithInlineStyles(leftShape, L);
      setBulletsWithInlineStyles(rightShape, R);
      setBoldTextSize(leftShape, 16);
      setBoldTextSize(rightShape, 16);

      
      try {
        
        const leftAdjustmentLog = adjustShapeText_External(leftShape, null);
        Logger.log(`Content Slide (Left Column) Adjustment: ${leftAdjustmentLog.log}`);
      } catch(e) {
        Logger.log(`Error during Content Slide (Left Column) adjustment: ${e.message}`);
      }
      try {
        
        const rightAdjustmentLog = adjustShapeText_External(rightShape, null);
        Logger.log(`Content Slide (Right Column) Adjustment: ${rightAdjustmentLog.log}`);
      } catch(e) {
        Logger.log(`Error during Content Slide (Right Column) adjustment: ${e.message}`);
      }
      
      } 

    } else {
      
      const baseBodyRect = layout.getRect('contentSlide.body');
      const adjustedBodyRect = adjustAreaForSubhead(baseBodyRect, data.subhead, layout);
      const bodyRect = offsetRect(adjustedBodyRect, 0, dy);
      if (isAgenda) {
        
        createContentCushion(slide, bodyRect, settings, layout);
        drawNumberedItems(slide, layout, bodyRect, points, settings);
      } else {
        const padding = layout.pxToPt(20);
        const textRect = { left: bodyRect.left + padding, top: bodyRect.top + padding, width: bodyRect.width - (padding * 2), height: bodyRect.height - (padding * 2) };
        
        let __bodyDone = false;
        if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
          const __bodyQLen = __REST_REQUESTS.length;
          try {
            createContentCushion(slide, bodyRect, settings, layout, { rest: true });
            _restBulletsAutofitBox(slide, textRect, points, { size: CONFIG.FONTS.sizes.body },
              { boldRunSize: 16, shrinkToMiddleBelow: CONFIG.FONTS.sizes.body });
            __bodyDone = true;
          } catch (e) { __REST_REQUESTS.length = __bodyQLen; Logger.log('[REST] content単一カラム(座布団+本文)失敗→SlidesAppフォールバック: ' + e.message); }
        }
        if (!__bodyDone) {
        createContentCushion(slide, bodyRect, settings, layout);
        const bodyShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, textRect.left, textRect.top, textRect.width, textRect.height);
        setBulletsWithInlineStyles(bodyShape, points);



        setBoldTextSize(bodyShape, 16);
        try {
          
          bodyShape.setContentAlignment(SlidesApp.ContentAlignment.TOP);
        } catch(e) {
          Logger.log('Content slide (1-col) vertical alignment failed: ' + e.message);
        }

        try {
          
          const bodyAdjustmentLog = adjustShapeText_External(bodyShape, null);
          Logger.log(`Content Slide (Single Column) Adjustment: ${bodyAdjustmentLog.log}`);

          
          
          
          if (bodyAdjustmentLog.finalSize !== null && bodyAdjustmentLog.finalSize < CONFIG.FONTS.sizes.body) {
             bodyShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
             Logger.log('Text overflowed/adjusted: Switched alignment to MIDDLE.');
          }

        } catch(e) {
          Logger.log(`Error during Content Slide (Single Column) adjustment: ${e.message}`);
        }
        } 


      }
    }
  }

  
  if (hasImages && !points.length && !isTwo) {
    const baseArea = layout.getRect('contentSlide.body');
    const adjustedArea = adjustAreaForSubhead(baseArea, data.subhead, layout);
    const area = offsetRect(adjustedArea, 0, dy);
    createContentCushion(slide, area, settings, layout);
    renderImagesInArea(slide, layout, area, normalizeImages(data.images), imageUpdateOption);
  }

  drawBottomBarAndFooter(slide, layout, pageNum, settings); 
}


function createCompareSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout); 
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null; 
  
  drawStandardTitleHeader(slide, layout, 'cardsSlide', data.title, settings, titleWidthPt, imageUpdateOption); 
  

  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'cardsSlide', data.subhead, subheadWidthPt);

  
  
  const baseArea = layout.getRect('cardsSlide.gridArea');
  const adjustedArea = adjustAreaForSubhead(baseArea, data.subhead, layout); 
  const area = offsetRect(adjustedArea, 0, dy); 

  const cols = 2; 
  const gap = layout.pxToPt(16); 
  const cardW = (area.width - gap * (cols - 1)) / cols; 
  const cardH = area.height; 

  
  const leftBoxLeft = area.left;
  const leftBoxTop = area.top;
  const leftBoxWidth = cardW;
  const leftBoxHeight = cardH;

  
  const rightBoxLeft = area.left + cardW + gap; 
  const rightBoxTop = area.top;
  const rightBoxWidth = cardW;
  const rightBoxHeight = cardH;

  
  drawCompareBox(slide, layout, leftBoxLeft, leftBoxTop, leftBoxWidth, leftBoxHeight, data.leftTitle || '選択肢A', data.leftItems || [], settings, true);
  drawCompareBox(slide, layout, rightBoxLeft, rightBoxTop, rightBoxWidth, rightBoxHeight, data.rightTitle || '選択肢B', data.rightItems || [], settings, false);
  

  drawBottomBarAndFooter(slide, layout, pageNum, settings); 
}

function createProcessSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout, imageUpdateOption);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null;
  drawStandardTitleHeader(slide, layout, 'processSlide', data.title, settings, titleWidthPt, imageUpdateOption);

  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'processSlide', data.subhead, subheadWidthPt);
  
  
  const baseArea = layout.getRect('processSlide.area');
  const adjustedArea = adjustAreaForSubhead(baseArea, data.subhead, layout);
  const area = offsetRect(adjustedArea, 0, dy);
  
  const steps = Array.isArray(data.steps) ? data.steps.slice(0, 4) : []; 
  if (steps.length === 0) {
    drawBottomBarAndFooter(slide, layout, pageNum, settings);
    return;
  }

  
  const processBodyBgColor = generateTintedGray(settings.primaryColor, 30, 94);

  
  const n = steps.length;
  let boxHPx, arrowHPx, fontSize;
  
  if (n <= 2) {
    boxHPx = 100; 
    arrowHPx = 25;
    fontSize = 16;
  } else if (n === 3) {
    boxHPx = 80; 
    arrowHPx = 20;
    fontSize = 16;
  } else {
    boxHPx = 65; 
    arrowHPx = 15;
    fontSize = 14;
  }

  
  const processColors = generateProcessColors(settings.primaryColor, n);

  const startY = area.top + layout.pxToPt(10);
  let currentY = startY;
  let boxHPt = layout.pxToPt(boxHPx),
    arrowHPt = layout.pxToPt(arrowHPx);
  
  
  try {
    var __barTop = layout.getRect('bottomBar').top;
    var __footTop = layout.getRect('footer.rightPage').top;
    var __safeBottom = Math.min(__barTop, __footTop) - layout.pxToPt(8); 
    var __availH = Math.max(layout.pxToPt(40), __safeBottom - startY);
    var __totalH = boxHPt * n + arrowHPt * (n - 1);
    if (__totalH > __availH) {
      var __fit = __availH / __totalH;
      boxHPt *= __fit;
      arrowHPt *= __fit;
      fontSize = Math.max(9, Math.round(fontSize * __fit));
    }
  } catch (eFit) {  }
  const headerWPt = layout.pxToPt(120);
  const bodyLeft = area.left + headerWPt;
  const bodyWPt = area.width - headerWPt;

  for (let i = 0; i < n; i++) {
    const cleanText = String(steps[i] || '').replace(/^\s*\d+(\.\s*|\s+)/, '');
    const stepText = `STEP ${i + 1}`;
    const isLast = (i >= n - 1);
    const headerRect = { left: area.left, top: currentY, width: headerWPt, height: boxHPt };
    const bodyRect = { left: bodyLeft, top: currentY, width: bodyWPt, height: boxHPt };
    const psRect = { left: bodyLeft + layout.pxToPt(20), top: currentY, width: bodyWPt - layout.pxToPt(40), height: boxHPt };
    const arrowLeft = area.left + headerWPt / 2 - layout.pxToPt(8);
    const arrowRect = { left: arrowLeft, top: currentY + boxHPt, width: layout.pxToPt(16), height: arrowHPt };
    const headerOpt = { size: fontSize, bold: true, color: CONFIG.COLORS.background_gray, align: SlidesApp.ParagraphAlignment.CENTER };

    
    let __pDone = false;
    if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
      const __pQLen = __REST_REQUESTS.length;
      try {
        const __sid = slide.getObjectId();
        restAddShape(__sid, headerRect, 'RECTANGLE', { fillRgb: hexToRgb01(processColors[i]), noOutline: true });
        _restStyledAutofitBox(slide, headerRect, stepText, headerOpt, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
        restAddShape(__sid, bodyRect, 'RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), noOutline: true });
        _restStyledAutofitBox(slide, psRect, cleanText, { size: fontSize }, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
        if (!isLast) restAddShape(__sid, arrowRect, 'DOWN_ARROW', { fillRgb: hexToRgb01(CONFIG.COLORS.process_arrow), noOutline: true });
        __pDone = true;
      } catch (eP) {
        __REST_REQUESTS.length = __pQLen;
        Logger.log('[PhaseA] process REST失敗→SlidesApp: ' + eP.message);
      }
    }
    if (!__pDone) {
      const header = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, area.left, currentY, headerWPt, boxHPt);
      header.getFill().setSolidFill(processColors[i]); 
      header.getBorder().setTransparent();
      setStyledText(header, stepText, {
        size: fontSize,
        bold: true,
        color: CONFIG.COLORS.background_gray,
        align: SlidesApp.ParagraphAlignment.CENTER
      });
      try {
        header.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
      } catch (e) {}
      const body = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, bodyLeft, currentY, bodyWPt, boxHPt);

      body.getFill().setSolidFill(CONFIG.COLORS.background_gray);

      body.getBorder().setTransparent();
      const textShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, psRect.left, psRect.top, psRect.width, psRect.height);
      setStyledText(textShape, cleanText, {
        size: fontSize
      });
      try {
        textShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
      } catch (e) {}
      if (!isLast) {
        const arrow = slide.insertShape(SlidesApp.ShapeType.DOWN_ARROW, arrowLeft, currentY + boxHPt, layout.pxToPt(16), arrowHPt);
        arrow.getFill().setSolidFill(CONFIG.COLORS.process_arrow);
        arrow.getBorder().setTransparent();
      }
    }
    currentY += boxHPt;
    if (!isLast) currentY += arrowHPt;
  }
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}

function createProcessListSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null; 
  drawStandardTitleHeader(slide, layout, 'processSlide', data.title, settings, titleWidthPt, imageUpdateOption); 

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'processSlide', data.subhead, subheadWidthPt); 
  

  const area = offsetRect(layout.getRect('processSlide.area'), 0, dy);
  const steps = Array.isArray(data.steps) ? data.steps : [];
  if (steps.length === 0) {
    drawBottomBarAndFooter(slide, layout, pageNum, settings);
    return;
  }

  const n = Math.max(1, steps.length);

  const topPadding = layout.pxToPt(30);
  const bottomPadding = layout.pxToPt(10);



  const drawableHeight = area.height - topPadding - bottomPadding; 

  
  const maxGapY_pt = layout.pxToPt(70);
  
  let idealGapY = Infinity; 
  if (n > 1) {
    idealGapY = drawableHeight / (n - 1);
  }

  const finalGapY = Math.min(idealGapY, maxGapY_pt); 
  

  const cx = area.left + layout.pxToPt(44); 
  const top0 = area.top + topPadding;

  const line = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, cx - layout.pxToPt(1), top0 + layout.pxToPt(6), layout.pxToPt(2), finalGapY * (n - 1)); 
  line.getFill().setSolidFill(CONFIG.COLORS.faint_gray);
  line.getBorder().setTransparent();

  for (let i = 0; i < n; i++) {
    const cy = top0 + finalGapY * i;


    const sz = layout.pxToPt(28);
    const numBox = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, cx - sz/2, cy - sz/2, sz, sz);
    numBox.getFill().setSolidFill(settings.primaryColor);
    numBox.getBorder().setTransparent();
    const num = numBox.getText(); num.setText(String(i + 1));
    applyTextStyle(num, { size: 12, bold: true, color: CONFIG.COLORS.background_gray, align: SlidesApp.ParagraphAlignment.CENTER });

    
    let cleanText = String(steps[i] || '');
    cleanText = cleanText.replace(/^\s*\d+(\.\s*|\s+)/, '');

    const txt = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, cx + layout.pxToPt(28), cy - layout.pxToPt(16), area.width - layout.pxToPt(70), layout.pxToPt(32));
    setStyledText(txt, cleanText, { size: CONFIG.FONTS.sizes.processStep, fontType: 'large' });
    try { txt.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch(e){}
  }

  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}

function createTimelineSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null; 
  drawStandardTitleHeader(slide, layout, 'timelineSlide', data.title, settings, titleWidthPt, imageUpdateOption); 

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'timelineSlide', data.subhead, subheadWidthPt); 
  
  
  
  const baseArea = layout.getRect('timelineSlide.area');
  const adjustedArea = adjustAreaForSubhead(baseArea, data.subhead, layout);
  const area = offsetRect(adjustedArea, 0, dy);
  
  const milestones = Array.isArray(data.milestones) ? data.milestones : [];
  if (milestones.length === 0) {
    drawBottomBarAndFooter(slide, layout, pageNum, settings);
    return;
  }
  const inner = layout.pxToPt(80),
    baseY = area.top + area.height * 0.50;
  const leftX = area.left + inner,
    rightX = area.left + area.width - inner;
  const line = slide.insertLine(SlidesApp.LineCategory.STRAIGHT, leftX, baseY, rightX, baseY);
  line.getLineFill().setSolidFill(CONFIG.COLORS.faint_gray);
  line.setWeight(2);
  const dotR = layout.pxToPt(10);
  const gap = (milestones.length > 1) ? (rightX - leftX) / (milestones.length - 1) : 0;
  const cardW_pt = layout.pxToPt(180); 
  const vOffset = layout.pxToPt(40); 
  const headerHeight = layout.pxToPt(28); 
  const bodyHeight = layout.pxToPt(80); 
  const cardPadding = layout.pxToPt(8);
  milestones.forEach((m, i) => {
    const x = leftX + gap * i;
    const isAbove = i % 2 === 0;

    
    const dateText = String(m.date || '');
    const labelText = String(m.label || '');
    
    
    const cardH_pt = headerHeight + bodyHeight;
    
    
    const cardLeft = x - (cardW_pt / 2);
    const cardTop = isAbove ? (baseY - vOffset - cardH_pt) : (baseY + vOffset);
    
    
    const timelineColors = generateTimelineCardColors(settings.primaryColor, milestones.length);
    
    
    
    
    let __tlDone = false;
    if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
      const __tlSid = slide.getObjectId();
      const __tlQLen = __REST_REQUESTS.length;
      try {
        const __tlHeaderRect = { left: cardLeft, top: cardTop, width: cardW_pt, height: headerHeight };
        const __tlBodyRect = { left: cardLeft, top: cardTop + headerHeight, width: cardW_pt, height: bodyHeight };
        const __tlConnStart = isAbove ? (cardTop + cardH_pt) : baseY;
        const __tlConnEnd = isAbove ? baseY : cardTop;
        let __tlBodySize = CONFIG.FONTS.sizes.body;
        const __tlLen = labelText.length;
        if (__tlLen > 40) __tlBodySize = 10; else if (__tlLen > 30) __tlBodySize = 11; else if (__tlLen > 20) __tlBodySize = 12;
        restAddShape(__tlSid, __tlHeaderRect, 'RECTANGLE', { fillRgb: hexToRgb01(timelineColors[i]), outlineRgb: hexToRgb01(timelineColors[i]), outlineWeight: 1 });
        restAddShape(__tlSid, __tlBodyRect, 'RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), outlineRgb: hexToRgb01(CONFIG.COLORS.card_border), outlineWeight: 1 });
        restAddLine(__tlSid, x, __tlConnStart, x, __tlConnEnd, { colorRgb: hexToRgb01(CONFIG.COLORS.neutral_gray), weight: 1 });
        restAddShape(__tlSid, { left: x - dotR / 2, top: baseY - dotR / 2, width: dotR, height: dotR }, 'ELLIPSE', { fillRgb: hexToRgb01(timelineColors[i]), noOutline: true });
        const __tlHdr = _restParenthesizedRanges(dateText, 8);
        _restStyledAutofitBox(slide, __tlHeaderRect, __tlHdr.text, { size: CONFIG.FONTS.sizes.body, bold: true, color: CONFIG.COLORS.background_gray, align: SlidesApp.ParagraphAlignment.CENTER }, SlidesApp.ContentAlignment.MIDDLE, null, { extraRanges: __tlHdr.ranges });
        _restStyledAutofitBox(slide, __tlBodyRect, labelText, { size: __tlBodySize, align: SlidesApp.ParagraphAlignment.CENTER }, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
        __tlDone = true;
      } catch (e) {
        __REST_REQUESTS.length = __tlQLen;
        __tlDone = false;
        Logger.log('[REST] timeline holistic失敗→SlidesAppフォールバック: ' + e.message);
      }
    }

    if (!__tlDone) {
    
    const headerShape = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, cardLeft, cardTop, cardW_pt, headerHeight);
    headerShape.getFill().setSolidFill(timelineColors[i]);
    headerShape.getBorder().getLineFill().setSolidFill(timelineColors[i]);
    
    
    const bodyShape = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, cardLeft, cardTop + headerHeight, cardW_pt, bodyHeight);
    bodyShape.getFill().setSolidFill(CONFIG.COLORS.background_gray);
    bodyShape.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border);
    const connectorY_start = isAbove ? (cardTop + cardH_pt) : baseY;
    const connectorY_end = isAbove ? baseY : cardTop;
    const connector = slide.insertLine(SlidesApp.LineCategory.STRAIGHT, x, connectorY_start, x, connectorY_end);
    connector.getLineFill().setSolidFill(CONFIG.COLORS.neutral_gray);
    connector.setWeight(1);
    
    const dot = slide.insertShape(SlidesApp.ShapeType.ELLIPSE, x - dotR / 2, baseY - dotR / 2, dotR, dotR);
    dot.getFill().setSolidFill(timelineColors[i]);
    dot.getBorder().setTransparent();
    
    const headerTextShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 
      cardLeft, cardTop, 
      cardW_pt, headerHeight);
    setStyledText(headerTextShape, dateText, {
      size: CONFIG.FONTS.sizes.body, 
      bold: true,
      color: CONFIG.COLORS.background_gray,
      align: SlidesApp.ParagraphAlignment.CENTER
    });
    Logger.log(adjustShapeText_External(headerTextShape));
    setParenthesizedTextSize(headerTextShape, 8);
    try {
      headerTextShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
    } catch (e) {}
    
    
    let bodyFontSize = CONFIG.FONTS.sizes.body; 
    const textLength = labelText.length;
    
    if (textLength > 40) bodyFontSize = 10;      
    else if (textLength > 30) bodyFontSize = 11; 
    else if (textLength > 20) bodyFontSize = 12; 
    
    
    const bodyTextShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 
      cardLeft, cardTop + headerHeight, 
      cardW_pt, bodyHeight);
    setStyledText(bodyTextShape, labelText, {
      size: bodyFontSize,
      align: SlidesApp.ParagraphAlignment.CENTER
    });
    try {
      bodyTextShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
    } catch (e) {}
    }
  });
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}

function createDiagramSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null; 
  drawStandardTitleHeader(slide, layout, 'diagramSlide', data.title, settings, titleWidthPt, imageUpdateOption); 

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'diagramSlide', data.subhead, subheadWidthPt); 
  

  const lanes = Array.isArray(data.lanes) ? data.lanes : [];
  
  
  const baseArea = layout.getRect('diagramSlide.lanesArea');
  const adjustedArea = adjustAreaForSubhead(baseArea, data.subhead, layout);
  const area = offsetRect(adjustedArea, 0, dy);
  
  const px = (p) => layout.pxToPt(p);
  const {
    laneGap_px,
    lanePad_px,
    laneTitle_h_px,
    cardGap_px,
    cardMin_h_px,
    cardMax_h_px,
    arrow_h_px,
    arrowGap_px
  } = CONFIG.DIAGRAM;
  const n = Math.max(1, lanes.length);
  const laneW = (area.width - px(laneGap_px) * (n - 1)) / n;
  
  const cardBoxes = [];
  const laneSpecs = [];
  for (let j = 0; j < n; j++) {
    const lane = lanes[j] || { title: '', items: [] };
    const left = area.left + j * (laneW + px(laneGap_px));
    const ltRect = { left: left, top: area.top, width: laneW, height: px(laneTitle_h_px) };
    const laneBodyTop = area.top + px(laneTitle_h_px), laneBodyHeight = area.height - px(laneTitle_h_px);
    const laneBgRect = { left: left, top: laneBodyTop, width: laneW, height: laneBodyHeight };
    const items = Array.isArray(lane.items) ? lane.items : [];
    const availH = laneBodyHeight - px(lanePad_px) * 2, rows = Math.max(1, items.length);
    const idealH = (availH - px(cardGap_px) * (rows - 1)) / rows;
    const cardH = Math.max(px(cardMin_h_px), Math.min(px(cardMax_h_px), idealH));
    const firstTop = laneBodyTop + px(lanePad_px) + Math.max(0, (availH - (cardH * rows + px(cardGap_px) * (rows - 1))) / 2);
    const cards = [];
    cardBoxes[j] = [];
    for (let i = 0; i < rows; i++) {
      const cardTop = firstTop + i * (cardH + px(cardGap_px));
      const cardRect = { left: left + px(lanePad_px), top: cardTop, width: laneW - px(lanePad_px) * 2, height: cardH };
      cards.push({ rect: cardRect, text: items[i] || '' });
      cardBoxes[j][i] = cardRect;
    }
    laneSpecs[j] = { ltRect: ltRect, ltText: lane.title || '', laneBgRect: laneBgRect, cards: cards };
  }
  const maxRows = Math.max(0, ...cardBoxes.map(a => a ? a.length : 0));

  
  
  let __dgDone = false;
  if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
    const __dgQLen = __REST_REQUESTS.length;
    try {
      const __sid = slide.getObjectId();
      laneSpecs.forEach(function (L) {
        restAddShape(__sid, L.ltRect, 'RECTANGLE', { fillRgb: hexToRgb01(settings.primaryColor), outlineRgb: hexToRgb01(settings.primaryColor), outlineWeight: 1 });
        _restStyledAutofitBox(slide, L.ltRect, L.ltText, { size: CONFIG.FONTS.sizes.laneTitle, bold: true, color: CONFIG.COLORS.background_gray, align: SlidesApp.ParagraphAlignment.CENTER }, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
        restAddShape(__sid, L.laneBgRect, 'RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), outlineRgb: hexToRgb01(CONFIG.COLORS.lane_border), outlineWeight: 1 });
        L.cards.forEach(function (c) {
          restAddShape(__sid, c.rect, 'ROUND_RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), outlineRgb: hexToRgb01(CONFIG.COLORS.card_border), outlineWeight: 1 });
          _restStyledAutofitBox(slide, c.rect, c.text, { size: CONFIG.FONTS.sizes.body }, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
        });
      });
      for (let j = 0; j < n - 1; j++) {
        for (let i = 0; i < maxRows; i++) {
          if (cardBoxes[j] && cardBoxes[j][i] && cardBoxes[j + 1] && cardBoxes[j + 1][i]) {
            const a = cardBoxes[j][i], b = cardBoxes[j + 1][i];
            const fromX = a.left + a.width, fromY = a.top + a.height / 2, toX = b.left, toY = b.top + b.height / 2;
            if (toX - fromX > 0) restAddLine(__sid, fromX, fromY, toX, toY, { colorRgb: hexToRgb01(settings.primaryColor), weight: 1.5, endArrow: 'FILL_ARROW' });
          }
        }
      }
      __dgDone = true;
    } catch (eDg) {
      __REST_REQUESTS.length = __dgQLen;
      Logger.log('[PhaseA] diagram REST失敗→SlidesApp: ' + eDg.message);
    }
  }
  if (!__dgDone) {
    laneSpecs.forEach(function (L) {
      const lt = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, L.ltRect.left, L.ltRect.top, L.ltRect.width, L.ltRect.height);
      lt.getFill().setSolidFill(settings.primaryColor);
      lt.getBorder().getLineFill().setSolidFill(settings.primaryColor);
      setStyledText(lt, L.ltText, {
        size: CONFIG.FONTS.sizes.laneTitle,
        bold: true,
        color: CONFIG.COLORS.background_gray,
        align: SlidesApp.ParagraphAlignment.CENTER
      });
      try {
        lt.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
      } catch (e) {}
      const laneBg = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, L.laneBgRect.left, L.laneBgRect.top, L.laneBgRect.width, L.laneBgRect.height);
      laneBg.getFill().setSolidFill(CONFIG.COLORS.background_gray);
      laneBg.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.lane_border);
      L.cards.forEach(function (c) {
        const card = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, c.rect.left, c.rect.top, c.rect.width, c.rect.height);
        card.getFill().setSolidFill(CONFIG.COLORS.background_gray);
        card.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border);
        setStyledText(card, c.text, {
          size: CONFIG.FONTS.sizes.body
        });
        try {
          card.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
        } catch (e) {}
      });
    });
    for (let j = 0; j < n - 1; j++) {
      for (let i = 0; i < maxRows; i++) {
        if (cardBoxes[j] && cardBoxes[j][i] && cardBoxes[j + 1] && cardBoxes[j + 1][i]) {
          drawArrowBetweenRects(slide, cardBoxes[j][i], cardBoxes[j + 1][i], px(arrow_h_px), px(arrowGap_px), settings);
        }
      }
    }
  }
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}

function createCycleSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null; 
  drawStandardTitleHeader(slide, layout, 'contentSlide', data.title, settings, titleWidthPt, imageUpdateOption); 

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'contentSlide', data.subhead, subheadWidthPt); 
  

  const area = offsetRect(layout.getRect('contentSlide.body'), 0, dy);
  const items = Array.isArray(data.items) && data.items.length === 4 ? data.items : [];
  if (items.length === 0) {
    drawBottomBarAndFooter(slide, layout, pageNum, settings);
    return;
  }

  
  const textLengths = items.map(item => {
    const labelLength = (item.label || '').length;
    const subLabelLength = (item.subLabel || '').length;
    return labelLength + subLabelLength;
  });
  const maxLength = Math.max(...textLengths);
  const avgLength = textLengths.reduce((sum, len) => sum + len, 0) / textLengths.length;

  const centerX = area.left + area.width / 2;
  const centerY = area.top + area.height / 2;
  
  
  const radiusX = area.width / 3.2;
  const radiusY = area.height / 2.6;
  
  
  const maxCardW = Math.min(layout.pxToPt(220), radiusX * 0.8); 
  const maxCardH = Math.min(layout.pxToPt(100), radiusY * 0.6); 
  
  
  let cardW, cardH, fontSize;
  if (maxLength > 25 || avgLength > 18) {
    
    cardW = Math.min(layout.pxToPt(230), maxCardW);
    cardH = Math.min(layout.pxToPt(105), maxCardH);
    fontSize = 13;  
  } else if (maxLength > 15 || avgLength > 10) {
    
    cardW = Math.min(layout.pxToPt(215), maxCardW);
    cardH = Math.min(layout.pxToPt(95), maxCardH);
    fontSize = 14;
  } else {
    
    cardW = layout.pxToPt(200);
    cardH = layout.pxToPt(90);
    fontSize = 16;
  }

  
  const centerRect = { left: centerX - layout.pxToPt(100), top: centerY - layout.pxToPt(50), width: layout.pxToPt(200), height: layout.pxToPt(100) };
  const positions = [
    { x: centerX + radiusX, y: centerY }, 
    { x: centerX, y: centerY + radiusY }, 
    { x: centerX - radiusX, y: centerY }, 
    { x: centerX, y: centerY - radiusY }  
  ];
  
  const arrowRadiusX = radiusX * 0.75;
  const arrowRadiusY = radiusY * 0.80;
  const arrowSize = layout.pxToPt(80);
  const arrowPositions = [
    { left: centerX + arrowRadiusX, top: centerY - arrowRadiusY, rotation: 90 },  
    { left: centerX + arrowRadiusX, top: centerY + arrowRadiusY, rotation: 180 }, 
    { left: centerX - arrowRadiusX, top: centerY + arrowRadiusY, rotation: 270 }, 
    { left: centerX - arrowRadiusX, top: centerY - arrowRadiusY, rotation: 0 }    
  ];

  
  
  let __cyDone = false;
  if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
    const __cyQLen = __REST_REQUESTS.length;
    try {
      const __sid = slide.getObjectId();
      if (data.centerText) {
        _restStyledAutofitBox(slide, centerRect, data.centerText, { size: 20, bold: true, align: SlidesApp.ParagraphAlignment.CENTER, color: CONFIG.COLORS.text_primary }, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
      }
      positions.forEach((pos, i) => {
        const cardRect = { left: pos.x - cardW / 2, top: pos.y - cardH / 2, width: cardW, height: cardH };
        restAddShape(__sid, cardRect, 'ROUND_RECTANGLE', { fillRgb: hexToRgb01(settings.primaryColor), noOutline: true });
        const item = items[i] || {};
        const subLabelText = item.subLabel || `${i + 1}番目`;
        const labelText = item.label || '';
        _restStyledAutofitBox(slide, cardRect, `${subLabelText}\n${labelText}`,
          { size: fontSize, bold: true, color: CONFIG.COLORS.background_gray, align: SlidesApp.ParagraphAlignment.CENTER },
          SlidesApp.ContentAlignment.MIDDLE, null,
          { noAutofit: true, extraRanges: [{ start: 0, end: subLabelText.length, size: Math.max(10, fontSize - 2) }] });
      });
      arrowPositions.forEach(pos => {
        const arrowRect = { left: pos.left - arrowSize / 2, top: pos.top - arrowSize / 2, width: arrowSize, height: arrowSize };
        restAddShape(__sid, arrowRect, 'BENT_ARROW', { fillRgb: hexToRgb01(CONFIG.COLORS.ghost_gray), noOutline: true, rotation: pos.rotation });
      });
      __cyDone = true;
    } catch (eCy) {
      __REST_REQUESTS.length = __cyQLen;
      Logger.log('[PhaseA] cycle REST失敗→SlidesApp: ' + eCy.message);
    }
  }
  if (!__cyDone) {
    if (data.centerText) {
      const centerTextBox = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, centerX - layout.pxToPt(100), centerY - layout.pxToPt(50), layout.pxToPt(200), layout.pxToPt(100));
      setStyledText(centerTextBox, data.centerText, { size: 20, bold: true, align: SlidesApp.ParagraphAlignment.CENTER, color: CONFIG.COLORS.text_primary });
      try { centerTextBox.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch (e) {}
    }
    positions.forEach((pos, i) => {
      const cardX = pos.x - cardW / 2;
      const cardY = pos.y - cardH / 2;
      const card = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, cardX, cardY, cardW, cardH);
      card.getFill().setSolidFill(settings.primaryColor);
      card.getBorder().setTransparent();
      const item = items[i] || {};
      const subLabelText = item.subLabel || `${i + 1}番目`;
      const labelText = item.label || '';

      setStyledText(card, `${subLabelText}\n${labelText}`, { size: fontSize, bold: true, color: CONFIG.COLORS.background_gray, align: SlidesApp.ParagraphAlignment.CENTER });
      try {
        card.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
        const textRange = card.getText();
        const subLabelEnd = subLabelText.length;
        if (textRange.asString().length > subLabelEnd) {
          
          textRange.getRange(0, subLabelEnd).getTextStyle().setFontSize(Math.max(10, fontSize - 2));
        }
      } catch (e) {}
    });
    arrowPositions.forEach(pos => {
      const arrow = slide.insertShape(SlidesApp.ShapeType.BENT_ARROW, pos.left - arrowSize / 2, pos.top - arrowSize / 2, arrowSize, arrowSize);
      arrow.getFill().setSolidFill(CONFIG.COLORS.ghost_gray);
      arrow.getBorder().setTransparent();
      arrow.setRotation(pos.rotation);
    });
  }

  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}

function createCardsSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout, imageUpdateOption);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null;
  drawStandardTitleHeader(slide, layout, 'cardsSlide', data.title, settings, titleWidthPt, imageUpdateOption);

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
 data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'cardsSlide', data.subhead, subheadWidthPt); 
  
  
  
  const baseArea = layout.getRect('cardsSlide.gridArea');
  const adjustedArea = adjustAreaForSubhead(baseArea, data.subhead, layout);
  const area = offsetRect(adjustedArea, 0, dy);
  
  const items = Array.isArray(data.items) ? data.items : [];
  const cols = Math.min(3, Math.max(2, Number(data.columns) || (items.length <= 4 ? 2 : 3)));
  const gap = layout.pxToPt(16),
    rows = Math.ceil(items.length / cols);
  const cardW = (area.width - gap * (cols - 1)) / cols,
    cardH = Math.max(layout.pxToPt(92), (area.height - gap * (rows - 1)) / rows);
  for (let idx = 0; idx < items.length; idx++) {
    const r = Math.floor(idx / cols),
      c = idx % cols;
    const cardRect = { left: area.left + c * (cardW + gap), top: area.top + r * (cardH + gap), width: cardW, height: cardH };
    const obj = items[idx];
    
    let __cardText, __cardOpt, __cardExtraRanges = null;
    if (typeof obj === 'string') {
      __cardText = obj; __cardOpt = { size: CONFIG.FONTS.sizes.body };
    } else {
      const title = String(obj.title || ''), desc = String(obj.desc || '');
      if (title && desc) {
        __cardText = `${title}\n\n${desc}`; __cardOpt = { size: CONFIG.FONTS.sizes.body };
        __cardExtraRanges = [{ start: 0, end: title.length, bold: true }];
      } else if (title) {
        __cardText = title; __cardOpt = { size: CONFIG.FONTS.sizes.body, bold: true };
      } else {
        __cardText = desc; __cardOpt = { size: CONFIG.FONTS.sizes.body };
      }
    }
    __cardOpt.align = SlidesApp.ParagraphAlignment.CENTER; 
    
    let __cardDone = false;
    if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
      const __cardQLen = __REST_REQUESTS.length; 
      try {
        const __sid = slide.getObjectId();
        restAddShape(__sid, cardRect, 'ROUND_RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), outlineRgb: hexToRgb01(CONFIG.COLORS.card_border), outlineWeight: 1 });
        _restStyledAutofitBox(slide, cardRect, __cardText, __cardOpt, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true, extraRanges: __cardExtraRanges });
        __cardDone = true;
      } catch (eCard) {
        __REST_REQUESTS.length = __cardQLen; 
        Logger.log('[PhaseA] cards REST失敗→SlidesApp: ' + eCard.message);
      }
    }
    if (!__cardDone) {
      const card = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, cardRect.left, cardRect.top, cardRect.width, cardRect.height);
      card.getFill().setSolidFill(CONFIG.COLORS.background_gray);
      card.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border);
      if (typeof obj === 'string') {
        setStyledText(card, obj, {
          size: CONFIG.FONTS.sizes.body
        });
      } else {
        const title = String(obj.title || ''),
          desc = String(obj.desc || '');
        if (title && desc) {
          const combined = `${title}\n\n${desc}`;
          setStyledText(card, combined, {
            size: CONFIG.FONTS.sizes.body
          });
          try {
            card.getText().getRange(0, title.length).getTextStyle().setBold(true);
          } catch (e) {}
        } else if (title) {
          setStyledText(card, title, {
            size: CONFIG.FONTS.sizes.body,
            bold: true
          });
        } else {
          setStyledText(card, desc, {
            size: CONFIG.FONTS.sizes.body
          });
        }
      }
      try {
        card.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
        
        card.getText().getParagraphs().forEach(function (p) { try { p.getRange().getParagraphStyle().setParagraphAlignment(SlidesApp.ParagraphAlignment.CENTER); } catch (e2) {} });
      } catch (e) {}
    }
  }
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}

function createHeaderCardsSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null; 
  drawStandardTitleHeader(slide, layout, 'cardsSlide', data.title, settings, titleWidthPt, imageUpdateOption); 

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'cardsSlide', data.subhead, subheadWidthPt); 
  
  
  
  const baseArea = layout.getRect('cardsSlide.gridArea');
  const adjustedArea = adjustAreaForSubhead(baseArea, data.subhead, layout);
  const area = offsetRect(adjustedArea, 0, dy);
  
  const items = Array.isArray(data.items) ? data.items : [];
  const cols = Math.min(3, Math.max(2, Number(data.columns) || (items.length <= 4 ? 2 : 3)));
  const gap = layout.pxToPt(16),
    rows = Math.ceil(items.length / cols);
  const cardW = (area.width - gap * (cols - 1)) / cols,
    cardH = Math.max(layout.pxToPt(92), (area.height - gap * (rows - 1)) / rows);
  for (let idx = 0; idx < items.length; idx++) {
    const r = Math.floor(idx / cols),
      c = idx % cols;
    const left = area.left + c * (cardW + gap),
      top = area.top + r * (cardH + gap);
    const titleText = String(items[idx].title || ''),
      descText = String(items[idx].desc || '');
    const headerHeight = layout.pxToPt(40);
    
    
    
    const __hcBodyRectShp = { left: left, top: top + headerHeight, width: cardW, height: cardH - headerHeight };
    const __hcHeaderRectShp = { left: left, top: top, width: cardW, height: headerHeight };
    let __hcShapeDone = false;
    if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
      try {
        restAddShape(slide.getObjectId(), __hcBodyRectShp, 'RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), outlineRgb: hexToRgb01(CONFIG.COLORS.card_border), outlineWeight: 1 });
        restAddShape(slide.getObjectId(), __hcHeaderRectShp, 'RECTANGLE', { fillRgb: hexToRgb01(settings.primaryColor), outlineRgb: hexToRgb01(settings.primaryColor), outlineWeight: 1 });
        __hcShapeDone = true;
      } catch (eShp) { Logger.log('[PhaseA] headerCards図形REST失敗→SlidesApp: ' + eShp.message); }
    }
    if (!__hcShapeDone) {
      const bodyShape = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, left, top + headerHeight, cardW, cardH - headerHeight);
      bodyShape.getFill().setSolidFill(CONFIG.COLORS.background_gray);
      bodyShape.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border);
      const headerShape = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, left, top, cardW, headerHeight);
      headerShape.getFill().setSolidFill(settings.primaryColor);
      headerShape.getBorder().getLineFill().setSolidFill(settings.primaryColor);
    }

    const hcHeaderRect = { left: left, top: top, width: cardW, height: headerHeight };
    const hcBodyRect = { left: left + layout.pxToPt(12), top: top + headerHeight, width: cardW - layout.pxToPt(24), height: cardH - headerHeight };
    const hcHeaderOpt = { size: CONFIG.FONTS.sizes.body, bold: true, color: CONFIG.COLORS.background_gray, align: SlidesApp.ParagraphAlignment.CENTER };
    const hcBodyOpt = { size: CONFIG.FONTS.sizes.body, align: SlidesApp.ParagraphAlignment.CENTER };
    
    let __hcDone = false;
    if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
      try {
        _restStyledAutofitBox(slide, hcHeaderRect, titleText, hcHeaderOpt, SlidesApp.ContentAlignment.MIDDLE, null, {});
        _restStyledAutofitBox(slide, hcBodyRect, descText, hcBodyOpt, SlidesApp.ContentAlignment.MIDDLE, null, { boldRunSize: 16, lineSpacing: 115 });
        __hcDone = true;
      } catch (e) { Logger.log('[REST] headerCards失敗→SlidesAppフォールバック: ' + e.message); }
    }
    if (!__hcDone) {
    const headerTextShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, hcHeaderRect.left, hcHeaderRect.top, hcHeaderRect.width, hcHeaderRect.height);
    setStyledText(headerTextShape, titleText, hcHeaderOpt);
    try {
      headerTextShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
    } catch (e) {}
    const bodyTextShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, hcBodyRect.left, hcBodyRect.top, hcBodyRect.width, hcBodyRect.height);
    setStyledText(bodyTextShape, descText, hcBodyOpt);

    try {
      setBoldTextSize(bodyTextShape, 16); 
      Logger.log(`Applied setBoldTextSize(16) to headerCard desc (Index ${idx})`);
    } catch (e) {
      Logger.log(`Error applying setBoldTextSize to headerCard desc (Index ${idx}): ${e.message}`);
    }

    try {
      bodyTextShape.getText().getParagraphStyle().setLineSpacing(115);
    } catch (e) {
      Logger.log(`Error setting line spacing for headerCards body: ${e.message}`);
    }
    try {
      bodyTextShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
    } catch (e) {}

    
    try {
      
      const adjustmentLog = adjustShapeText_External(bodyTextShape, null);
      Logger.log(`HeaderCards Desc Adjustment (Index ${idx}): ${adjustmentLog.log}`);
    } catch(e) {
      Logger.log(`Error during HeaderCards desc adjustment (Index ${idx}): ${e.message}`);
    }
    
    } 

  }
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}


function createTableSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null;
  drawStandardTitleHeader(slide, layout, 'tableSlide', data.title, settings, titleWidthPt, imageUpdateOption);

  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'tableSlide', data.subhead, subheadWidthPt);
  
  
  const baseArea = layout.getRect('tableSlide.area');
  const adjustedArea = adjustAreaForSubhead(baseArea, data.subhead, layout);
  const area = offsetRect(adjustedArea, 0, dy);
  
  const headers = Array.isArray(data.headers) ? data.headers : [];
  const rows = Array.isArray(data.rows) ?
  data.rows : [];
  
  try {
    if (headers.length > 0) {
      const numRowsTotal = rows.length + 1;
      const table = slide.insertTable(numRowsTotal, headers.length, area.left, area.top, area.width, area.height);

      
      
      const estimatedRowHeight = area.height / numRowsTotal;

      
      const __colWidths = headers.map(function (_, c) { return table.getColumn(c).getWidth(); });

      
      for (let c = 0; c < headers.length; c++) {
        const cell = table.getCell(0, c);
        const cellWidth = __colWidths[c]; 

        cell.getFill().setSolidFill(CONFIG.COLORS.table_header_bg);
        setStyledText(cell, String(headers[c] || ''), {
          bold: true,
          color: CONFIG.COLORS.text_small_font,
          align: SlidesApp.ParagraphAlignment.CENTER
        });
        try {
          cell.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
        } catch (e) {}

        
        try {
          
          adjustShapeText_External(cell, null, cellWidth, estimatedRowHeight);
        } catch (adjustError) {
          Logger.log(`Error adjusting text in header cell (0, ${c}): ${adjustError.message}`);
        }
      }

      
      for (let r = 0; r < rows.length; r++) {
        for (let c = 0; c < headers.length; c++) {
          const cell = table.getCell(r + 1, c);
          const cellWidth = __colWidths[c]; 

          cell.getFill().setSolidFill(CONFIG.COLORS.background_gray);
          setStyledText(cell, String((rows[r] || [])[c] || ''), {
            align: SlidesApp.ParagraphAlignment.CENTER
          });
          try {
            cell.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
          } catch (e) {}

          
          try {
            
            adjustShapeText_External(cell, null, cellWidth, estimatedRowHeight);
          } catch (adjustError) {
            Logger.log(`Error adjusting text in data cell (${r + 1}, ${c}): ${adjustError.message}`);
          }
        }
      }
    }
  } catch (e) {
    Logger.log(`Table creation error: ${e.message}`);
  }
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}

function createProgressSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null; 
  drawStandardTitleHeader(slide, layout, 'progressSlide', data.title, settings, titleWidthPt, imageUpdateOption); 

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'progressSlide', data.subhead, subheadWidthPt); 
  
  
  
  const baseArea = layout.getRect('progressSlide.area');
  const adjustedArea = adjustAreaForSubhead(baseArea, data.subhead, layout);
  const area = offsetRect(adjustedArea, 0, dy);
  
  const items = Array.isArray(data.items) ? data.items.slice(0, 4) : [];
  if (items.length === 0) {
    drawBottomBarAndFooter(slide, layout, pageNum, settings);
    return;
  }
  const n = Math.max(1, items.length);
  
  const cardGap = layout.pxToPt(12); 
  const cardHeight = Math.max(layout.pxToPt(50), (area.height - cardGap * (n - 1)) / n);
  const cardPadding = layout.pxToPt(15);
  const barHeight = layout.pxToPt(12);
  
  const percentHeight = layout.pxToPt(30);
  const percentWidth = layout.pxToPt(120); 
  
  for (let i = 0; i < n; i++) {
    const cardTop = area.top + i * (cardHeight + cardGap);
    const p = Math.max(0, Math.min(100, Number(items[i].percent || 0)));

    
    const labelHeight = layout.pxToPt(20);
    const labelWidth = area.width - percentWidth - cardPadding * 3; 
    const labelText = String(items[i].label || '');
    const cardRect = { left: area.left, top: cardTop, width: area.width, height: cardHeight };
    const labelRect = { left: area.left + cardPadding, top: cardTop + cardPadding, width: labelWidth, height: labelHeight };
    const pctRect = { left: area.left + area.width - percentWidth - cardPadding, top: cardTop + cardPadding - layout.pxToPt(2), width: percentWidth, height: percentHeight };
    const barTop = cardTop + cardHeight - cardPadding - barHeight;
    const barWidth = area.width - cardPadding * 2;
    const barBGRect = { left: area.left + cardPadding, top: barTop, width: barWidth, height: barHeight };
    const filledBarWidth = Math.max(layout.pxToPt(6), barWidth * (p / 100));
    const barFGRect = { left: area.left + cardPadding, top: barTop, width: filledBarWidth, height: barHeight };
    const labelOpt = { size: CONFIG.FONTS.sizes.body, bold: true, align: SlidesApp.ParagraphAlignment.LEFT };
    const pctOpt = { size: 20, bold: true, color: settings.primaryColor, align: SlidesApp.ParagraphAlignment.RIGHT };

    
    let __progDone = false;
    if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
      const __progQLen = __REST_REQUESTS.length; 
      try {
        const __sid = slide.getObjectId();
        restAddShape(__sid, cardRect, 'ROUND_RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), outlineRgb: hexToRgb01(CONFIG.COLORS.card_border), outlineWeight: 1 });
        _restStyledAutofitBox(slide, labelRect, labelText, labelOpt, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
        _restStyledAutofitBox(slide, pctRect, `${p}%`, pctOpt, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
        restAddShape(__sid, barBGRect, 'ROUND_RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.faint_gray), noOutline: true });
        if (p > 0) restAddShape(__sid, barFGRect, 'ROUND_RECTANGLE', { fillRgb: hexToRgb01(settings.primaryColor), noOutline: true });
        __progDone = true;
      } catch (eProg) {
        __REST_REQUESTS.length = __progQLen; 
        Logger.log('[PhaseA] progress REST失敗→SlidesApp: ' + eProg.message);
      }
    }
    if (!__progDone) {
      
      const card = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE,
        area.left, cardTop, area.width, cardHeight);
      card.getFill().setSolidFill(CONFIG.COLORS.background_gray);
      card.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border);

      
      const label = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX,
        area.left + cardPadding, cardTop + cardPadding,
        labelWidth, labelHeight);
      setStyledText(label, labelText, {
        size: CONFIG.FONTS.sizes.body,
        bold: true,
        align: SlidesApp.ParagraphAlignment.LEFT
      });
      try {
      label.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
    } catch (e) {}

      
      const pct = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX,
        area.left + area.width - percentWidth - cardPadding,
        cardTop + cardPadding - layout.pxToPt(2),
        percentWidth, percentHeight);

      setStyledText(pct, `${p}%`, {
        size: 20, 
        bold: true,
        color: settings.primaryColor, 
        align: SlidesApp.ParagraphAlignment.RIGHT 
      });
      try {
      pct.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
    } catch (e) {}

      
      const barBG = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE,
        area.left + cardPadding, barTop, barWidth, barHeight);
      barBG.getFill().setSolidFill(CONFIG.COLORS.faint_gray);
      barBG.getBorder().setTransparent();

      
      if (p > 0) {
        const barFG = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE,
          area.left + cardPadding, barTop, filledBarWidth, barHeight);
        barFG.getFill().setSolidFill(settings.primaryColor); 
        barFG.getBorder().setTransparent();
      }
    }
  }
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}

function createQuoteSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null; 
  drawStandardTitleHeader(slide, layout, 'quoteSlide', data.title || '引用', settings, titleWidthPt, imageUpdateOption); 

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'quoteSlide', data.subhead, subheadWidthPt); 
  
  
  
  const baseTop = 120;
  const subheadHeight = data.subhead ? layout.pxToPt(40) : 0; 
  const margin = layout.pxToPt(10); 
  
  const area = offsetRect(layout.getRect({
    left: 40,
    top: baseTop + subheadHeight + margin,
    width: 880,
    height: 320 - subheadHeight - margin 
  }), 0, dy);
  
  let __qCardDone = false;
  if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
    try {
      restAddShape(slide.getObjectId(), { left: area.left, top: area.top, width: area.width, height: area.height }, 'ROUND_RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_white), outlineRgb: hexToRgb01(CONFIG.COLORS.card_border), outlineWeight: 2 });
      __qCardDone = true;
    } catch (eQ) { Logger.log('[PhaseA] quote bgCard REST失敗→SlidesApp: ' + eQ.message); }
  }
  if (!__qCardDone) {
    const bgCard = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, area.left, area.top, area.width, area.height);
    bgCard.getFill().setSolidFill(CONFIG.COLORS.background_white);
    const border = bgCard.getBorder();
    border.getLineFill().setSolidFill(CONFIG.COLORS.card_border);
    border.setWeight(2);
  }
  const padding = layout.pxToPt(40);
  
  
  const textLeft = area.left + padding,
    textTop = area.top + padding;
  const textWidth = area.width - (padding * 2),
    textHeight = area.height - (padding * 2);
  const quoteTextHeight = textHeight - layout.pxToPt(30);
  const authorTop = textTop + quoteTextHeight;
  const quoteRect = { left: textLeft, top: textTop, width: textWidth, height: quoteTextHeight };
  const authorRect = { left: textLeft, top: authorTop, width: textWidth, height: layout.pxToPt(30) };
  const quoteOpt = { size: 24, align: SlidesApp.ParagraphAlignment.CENTER, fontType: 'large' };
  const authorOpt = { size: 16, color: CONFIG.COLORS.neutral_gray, align: SlidesApp.ParagraphAlignment.END };
  
  let __qDone = false;
  if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
    try {
      _restStyledAutofitBox(slide, quoteRect, data.text || '', quoteOpt, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
      _restStyledAutofitBox(slide, authorRect, `— ${data.author || ''}`, authorOpt, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
      __qDone = true;
    } catch (e) { Logger.log('[REST] quote失敗→SlidesAppフォールバック: ' + e.message); }
  }
  if (!__qDone) {
  const textShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, quoteRect.left, quoteRect.top, quoteRect.width, quoteRect.height);
  setStyledText(textShape, data.text || '', quoteOpt);
  try {
    textShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
  } catch (e) {}
  const authorShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, authorRect.left, authorRect.top, authorRect.width, authorRect.height);
  setStyledText(authorShape, `— ${data.author || ''}`, authorOpt);
  try {
    authorShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
  } catch (e) {}
  } 
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}





function createKpiSlide(slide, data, layout, pageNum, settings, imageUpdateOption) {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null;
  drawStandardTitleHeader(slide, layout, 'kpiSlide', data.title || '主要指標', settings, titleWidthPt, imageUpdateOption);

  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ? data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'kpiSlide', data.subhead, subheadWidthPt);
  
  
  
  
  const items = (Array.isArray(data.items) ? data.items.slice(0, 8) : []);
  const n = items.length;
  if (n === 0) {
    drawBottomBarAndFooter(slide, layout, pageNum, settings);
    return;
  }

  const gap = layout.pxToPt(16);
  const pad = layout.pxToPt(15);
  const originalCardH_pt = layout.pxToPt(240); 

  if (n <= 4) {
    
    Logger.log(`KPI Slide: ${n} items. Using 1-row layout.`);
    
    
    const baseArea = layout.getRect('kpiSlide.gridArea');
const adjustedArea = adjustAreaForSubhead(baseArea, data.subhead, layout);

    
    
    
    let finalDy = dy;
    if (dy === 0) {
        
        finalDy = layout.pxToPt(36); 
        Logger.log('KPI Slide (1-row): No subhead. Adding 36pt vertical offset.');
    }
    
    const area = offsetRect(adjustedArea, 0, finalDy);
    

    const cols = n; 


    const cardW = (area.width - gap * (cols - 1)) / cols;
    const cardH = originalCardH_pt; 

    for (let idx = 0; idx < n; idx++) {
      const left = area.left + idx * (cardW + gap);
      const top = area.top; 
      
      
      drawKpiCard(slide, layout, items[idx], left, top, cardW, cardH, pad, 1.0);
    }

  } else {
    
    Logger.log(`KPI Slide: ${n} items. Using 2-row layout.`);

    
    const baseArea = layout.getRect('kpiSlide.gridArea');
    const adjustedArea = adjustAreaForSubhead(baseArea, data.subhead, layout);
    
    const area = offsetRect(adjustedArea, 0, 0); 

    
    const rows = 2;
    
    const cols = (n <= 6) ? 3 : 4; 
    
    const cardW = (area.width - gap * (cols - 1)) / cols;
    const cardH = (area.height - gap * (rows - 1)) / rows; 
    
    
    const scaleY = cardH / originalCardH_pt; 

    for (let idx = 0; idx < n; idx++) {
      const c = idx % cols;
      const r = Math.floor(idx / cols);
      
      const left = area.left + c * (cardW + gap);
      const top = area.top + r * (cardH + gap);
      
      
      drawKpiCard(slide, layout, items[idx], left, top, cardW, cardH, pad, scaleY);
    }
  }
  

  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}


function drawKpiCard(slide, layout, item, left, top, cardW, cardH, pad, scaleY) {
  
  const extraWidthPt = layout.pxToPt(30);
  const labelLeft = (left + pad) - (extraWidthPt / 2);
  const labelWidth = (cardW - pad * 2) + extraWidthPt;
  const labelTop = top + layout.pxToPt(25) * scaleY;
  const labelHeight = layout.pxToPt(35) * scaleY;
  const valueLeft = left + pad;
  const valueWidth = cardW - pad * 2;
  const valueTop = top + layout.pxToPt(80) * scaleY;
  const valueHeight = layout.pxToPt(80) * scaleY;
  const changeLeft = left + pad;
  const changeWidth = cardW - pad * 2;
  const changeTop = top + layout.pxToPt(180) * scaleY;
  const changeHeight = layout.pxToPt(40) * scaleY;
  let changeColor = CONFIG.COLORS.text_primary;
  if (item.status === 'bad') changeColor = '#d93025';
  if (item.status === 'good') changeColor = '#1e8e3e';
  if (item.status === 'neutral') changeColor = CONFIG.COLORS.neutral_gray;
  const changeOpt = { size: 14, color: changeColor, bold: true, align: SlidesApp.ParagraphAlignment.END };

  
  
  
  
  let __kpiDone = false;
  if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
    const __kpiSid = slide.getObjectId();
    const __kpiQLen = __REST_REQUESTS.length;
    try {
      restAddShape(__kpiSid, { left: left, top: top, width: cardW, height: cardH }, 'RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), outlineRgb: hexToRgb01(CONFIG.COLORS.card_border), outlineWeight: 1 });
      _restStyledAutofitBox(slide, { left: labelLeft, top: labelTop, width: labelWidth, height: labelHeight }, item.label || 'KPI', { size: 14, color: CONFIG.COLORS.neutral_gray, align: SlidesApp.ParagraphAlignment.CENTER }, SlidesApp.ContentAlignment.MIDDLE, null, {});
      const __kpiValueText = item.value || '0';
      const __kpiLarge = '0123456789+-.,:';
      let __kpiHasLarge = false;
      for (let i = 0; i < __kpiValueText.length; i++) { if (__kpiLarge.includes(__kpiValueText[i])) { __kpiHasLarge = true; break; } }
      const __kpiVExtra = [];
      if (__kpiHasLarge) {
        for (let i = 0; i < __kpiValueText.length; i++) { if (!__kpiLarge.includes(__kpiValueText[i])) __kpiVExtra.push({ start: i, end: i + 1, size: 16 }); }
      }
      const __kpiNumeric = /[0-9+-]/.test(__kpiValueText);
      _restStyledAutofitBox(slide, { left: valueLeft, top: valueTop, width: valueWidth, height: valueHeight }, __kpiValueText, { size: 32, bold: true, align: SlidesApp.ParagraphAlignment.CENTER }, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: __kpiNumeric, extraRanges: __kpiVExtra });
      _restStyledAutofitBox(slide, { left: changeLeft, top: changeTop, width: changeWidth, height: changeHeight }, item.change || '', changeOpt, null, null, {});
      __kpiDone = true;
    } catch (e) {
      __REST_REQUESTS.length = __kpiQLen;
      __kpiDone = false;
      Logger.log('[REST] kpiCard holistic失敗→SlidesAppフォールバック: ' + e.message);
    }
  }

  if (!__kpiDone) {
    
    
    const card = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, left, top, cardW, cardH);
    card.getFill().setSolidFill(CONFIG.COLORS.background_gray);
    card.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border);

    
    const labelShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, labelLeft, labelTop, labelWidth, labelHeight);
    setStyledText(labelShape, item.label || 'KPI', {
      size: 14,
      color: CONFIG.COLORS.neutral_gray,
      align: SlidesApp.ParagraphAlignment.CENTER
    });
    try {
      labelShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
    } catch (e) {
      Logger.log(`Failed to set ContentAlignment.MIDDLE for KPI Label: ${e.message}`);
    }
    
    try {
      const adjustmentResult = adjustShapeText_External(labelShape, null);
    } catch(e) {
      Logger.log(`Error during KPI label adjustment: ${e.message}`);
    }

    
    const valueShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, valueLeft, valueTop, valueWidth, valueHeight);
    const valueText = item.value || '0';
    const textRange = valueShape.getText().setText(valueText);
    applyTextStyle(textRange, { size: 32, bold: true, align: SlidesApp.ParagraphAlignment.CENTER });
    const largeChars = '0123456789+-.,:';
    const smallerSize = 16;
    let containsNumericOrSymbol = false;
    for (let i = 0; i < valueText.length; i++) {
      if (largeChars.includes(valueText[i])) {
        containsNumericOrSymbol = true;
        break;
      }
    }
    if (containsNumericOrSymbol) {
      try {
        for (let i = 0; i < valueText.length; i++) {
          const char = valueText[i];
          if (!largeChars.includes(char)) {
            const charRange = textRange.getRange(i, i + 1);
            charRange.getTextStyle().setFontSize(smallerSize);
          }
        }
      } catch (e) {  }
    }
    try { valueShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch (e) {}

    
    try {
      const adjustmentResult = adjustShapeText_External(valueShape, null);
      const textRange = valueShape.getText();
      const valueText = textRange.asString();
      const containsSpecificChars = /[0-9+-]/.test(valueText);
      if (containsSpecificChars) {
        const currentStyle = textRange.getTextStyle();
        if (currentStyle && currentStyle.getFontSize() !== 32) {
          currentStyle.setFontSize(32);
          try {
            for (let i = 0; i < valueText.length; i++) {
              const char = valueText[i];
              if (!largeChars.includes(char)) {
                const charRange = textRange.getRange(i, i + 1);
                charRange.getTextStyle().setFontSize(smallerSize);
              }
            }
          } catch (e) {  }
        }
      } else {
        const firstRun = textRange.getRuns()[0];
        if (firstRun) {
          const currentSize = firstRun.getTextStyle().getFontSize();
          if (typeof currentSize === 'number' && currentSize < 16) {
            textRange.getTextStyle().setFontSize(16);
          }
        }
      }
    } catch(e) {
      Logger.log(`Error during KPI value adjustment: ${e.message}`);
    }

    
    const changeShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, changeLeft, changeTop, changeWidth, changeHeight);
    setStyledText(changeShape, item.change || '', changeOpt);
    
    try {
      adjustShapeText_External(changeShape, null);
    } catch(e) {
      Logger.log(`Error during KPI change adjustment: ${e.message}`);
    }
  }
}



function createBulletCardsSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null; 
  drawStandardTitleHeader(slide, layout, 'contentSlide', data.title, settings, titleWidthPt, imageUpdateOption); 

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'contentSlide', data.subhead, subheadWidthPt); 
  

  const area = offsetRect(layout.getRect('contentSlide.body'), 0, dy);
  const items = Array.isArray(data.items) ? data.items.slice(0, 3) : [];
  if (items.length === 0) {
    drawBottomBarAndFooter(slide, layout, pageNum, settings);
    return;
  }

  const gap = layout.pxToPt(16);
  const cardHeight = (area.height - gap * (items.length - 1)) / items.length;

  for (let i = 0; i < items.length; i++) {
    
    const __bcCardRect = { left: area.left, top: area.top + i * (cardHeight + gap), width: area.width, height: cardHeight };
    let __bcShapeDone = false;
    if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
      try {
        restAddShape(slide.getObjectId(), __bcCardRect, 'RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), outlineRgb: hexToRgb01(CONFIG.COLORS.card_border), outlineWeight: 1 });
        __bcShapeDone = true;
      } catch (eBc) { Logger.log('[PhaseA] bulletCards図形REST失敗→SlidesApp: ' + eBc.message); }
    }
    if (!__bcShapeDone) {
      const card = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, area.left, area.top + i * (cardHeight + gap), area.width, cardHeight);
      card.getFill().setSolidFill(CONFIG.COLORS.background_gray);
      card.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border);
    }

    const padding = layout.pxToPt(20);
    const title = String(items[i].title || '');
    const desc = String(items[i].desc || '');

    
    let __bcDone = false;
    if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
      try {
        if (title && desc) {
          const tH = layout.pxToPt(16 + 4);
          _restStyledAutofitBox(slide, { left: area.left + padding - layout.pxToPt(10), top: area.top + i * (cardHeight + gap) + layout.pxToPt(12), width: area.width - padding * 2, height: tH },
            title, { size: 16, bold: true }, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
          const dTop = area.top + i * (cardHeight + gap) + layout.pxToPt(12) + tH + layout.pxToPt(8);
          const dH = cardHeight - layout.pxToPt(12) - tH - layout.pxToPt(8) - layout.pxToPt(12);
          let bDesc = CONFIG.FONTS.sizes.body;
          if (desc.length > 100) bDesc = 12; else if (desc.length > 80) bDesc = 13;
          _restStyledAutofitBox(slide, { left: area.left + padding, top: dTop, width: area.width - padding * 2, height: dH },
            desc, { size: bDesc }, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true, boldRunSize: 16 });
        } else {
          const sText = title || desc;
          const isTO = !!title;
          const sFS = isTO ? 16 : CONFIG.FONTS.sizes.body;
          const sH = layout.pxToPt(sFS + 8);
          _restStyledAutofitBox(slide, { left: area.left + padding - layout.pxToPt(10), top: area.top + i * (cardHeight + gap) + (cardHeight - sH) / 2, width: area.width - padding * 2, height: sH },
            sText, { size: sFS, bold: isTO }, SlidesApp.ContentAlignment.MIDDLE, null, isTO ? { noAutofit: true } : { noAutofit: true, boldRunSize: 16 });
        }
        __bcDone = true;
      } catch (e) { Logger.log('[REST] bulletCards失敗→SlidesAppフォールバック: ' + e.message); }
    }
    if (!__bcDone) {
    if (title && desc) {
      
      const titleHeight = layout.pxToPt(16 + 4); 

      
      const titleShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX,
        area.left + padding - layout.pxToPt(10),
        area.top + i * (cardHeight + gap) + layout.pxToPt(12),
        area.width - padding * 2,
        titleHeight
      );
      
      setStyledText(titleShape, title, {
        size: 16, 
        bold: true
      });
      try {
        titleShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
      } catch (e) {}

      
      const descTop = area.top + i * (cardHeight + gap) + layout.pxToPt(12) + titleHeight + layout.pxToPt(8);
      const descHeight = cardHeight - layout.pxToPt(12) - titleHeight - layout.pxToPt(8) - layout.pxToPt(12); 

      
      let baseDescFontSize = CONFIG.FONTS.sizes.body; 
      if (desc.length > 100) {
        baseDescFontSize = 12;
      } else if (desc.length > 80) {
        baseDescFontSize = 13;
      }

      const descShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX,
        area.left + padding,
        descTop,
        area.width - padding * 2,
        descHeight
      );
      
      setStyledText(descShape, desc, {
        size: baseDescFontSize
      });

      
      setBoldTextSize(descShape, 16);

      try {
        descShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); 
      } catch (e) {}

    } else {
      
      const singleText = title || desc;
      const isTitleOnly = !!title;

      
      const singleFontSize = isTitleOnly ? 16 : CONFIG.FONTS.sizes.body; 

      const singleHeight = layout.pxToPt(singleFontSize + 8);
      const shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX,
        area.left + padding - layout.pxToPt(10),
        area.top + i * (cardHeight + gap) + (cardHeight - singleHeight) / 2, 
        area.width - padding * 2,
        singleHeight
      );
      
      setStyledText(shape, singleText, {
        size: singleFontSize,
        bold: isTitleOnly 
      });

      
      if (!isTitleOnly) {
        setBoldTextSize(shape, 16);
      }

      try {
        shape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
      } catch (e) {}
    }
    } 
  }
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}

function createAgendaSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout, imageUpdateOption);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null;
  drawStandardTitleHeader(slide, layout, 'processSlide', data.title, settings, titleWidthPt, imageUpdateOption);

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'processSlide', data.subhead, subheadWidthPt); 
  

  
  let effectiveDy = dy; 
  
  if (dy > 0) {
    
    
    
    effectiveDy = dy - layout.pxToPt(20); 
  }

  
  const area = offsetRect(layout.getRect('processSlide.area'), 0, effectiveDy);
  
  
  
  let items = Array.isArray(data.items) ? data.items : [];
  if (items.length === 0) {
    
    items = buildAgendaFromSlideData();
    if (items.length === 0) {
      items = ['本日の目的', '進め方', '次のアクション'];
    }
  }

  const n = Math.max(1, items.length);

  const topPadding = layout.pxToPt(30);
  const bottomPadding = layout.pxToPt(10);
  const drawableHeight = area.height - topPadding - bottomPadding;


  
  const maxGapY_pt = layout.pxToPt(60); 

  let idealGapY = Infinity; 
  if (n > 1) {
    idealGapY = drawableHeight / (n - 1);
  }

  const finalGapY = Math.min(idealGapY, maxGapY_pt); 

  const top0 = area.top + topPadding;

  


  const cx = area.left + layout.pxToPt(44);

  for (let i = 0; i < n; i++) {
    const cy = top0 + finalGapY * i;
    const sz = layout.pxToPt(28);
    
    
    
    
    
    const __numRect = { left: cx - sz / 2, top: cy - sz / 2, width: sz, height: sz };
    let __numDone = false;
    if (__REST_TEXT_ACTIVE) {
      try {
        restAddShape(slide.getObjectId(), __numRect, 'RECTANGLE', { fillRgb: hexToRgb01(settings.primaryColor), noOutline: true });
        restAddTextBox(slide.getObjectId(), __numRect, String(i + 1), {
          size: 12, bold: true, fontFamily: CONFIG.FONTS.family,
          colorRgb: hexToRgb01(CONFIG.COLORS.background_gray),
          align: 'CENTER', contentAlignment: 'MIDDLE'
        });
        __numDone = true;
      } catch (eNum) { __numDone = false; Logger.log('[PhaseA] 番号ボックスREST失敗→SlidesApp: ' + eNum.message); }
    }
    if (!__numDone) {
      const numBox = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, cx - sz/2, cy - sz/2, sz, sz);
      numBox.getFill().setSolidFill(settings.primaryColor);
      numBox.getBorder().setTransparent();

      const num = numBox.getText();
      num.setText(String(i + 1));
      applyTextStyle(num, {
        size: 12,
        bold: true,
        color: CONFIG.COLORS.background_gray,
        align: SlidesApp.ParagraphAlignment.CENTER
      });
    }

    
    let cleanText = String(items[i] || '');
    cleanText = cleanText.replace(/^\s*\d+(\.\s*|\s+)/, '');

    const txt = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, cx + layout.pxToPt(28), cy - layout.pxToPt(16), area.width - layout.pxToPt(70), layout.pxToPt(32));
    setStyledText(txt, cleanText, {
      size: CONFIG.FONTS.sizes.processStep,
      bold: true,                        
      fontType: 'large'
    });
    try { 
      txt.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); 
    } catch(e){}
  }

  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}


function createFaqSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null; 
  drawStandardTitleHeader(slide, layout, 'contentSlide', data.title || 'よくあるご質問', settings, titleWidthPt, imageUpdateOption); 

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'contentSlide', data.subhead, subheadWidthPt); 
  

  const area = offsetRect(layout.getRect('contentSlide.body'), 0, dy);
  const items = Array.isArray(data.items) ? data.items.slice(0, 4) : [];
  if (items.length === 0) {
    drawBottomBarAndFooter(slide, layout, pageNum, settings);
    return;
  }

  let currentY = area.top;
  const cardGap = layout.pxToPt(12); 
  
  
  const totalGaps = cardGap * (items.length - 1);
  const availableHeight = area.height - totalGaps;
  const cardHeight = availableHeight / items.length;

  items.forEach((item, index) => {
    
    const card = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, 
      area.left, currentY, area.width, cardHeight);
    card.getFill().setSolidFill(CONFIG.COLORS.background_gray);
    card.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border);
    card.getBorder().setWeight(1);

    
    let cardPadding, qAreaRatio, qAGap;
    
    if (items.length <= 2) {
      
      cardPadding = layout.pxToPt(16);  
      qAreaRatio = 0.30;               
      qAGap = layout.pxToPt(6);        
    } else if (items.length === 3) {
      
      cardPadding = layout.pxToPt(12);  
      qAreaRatio = 0.35;               
      qAGap = layout.pxToPt(4);        
    } else {
      
      cardPadding = layout.pxToPt(8);   
      qAreaRatio = 0.40;               
      qAGap = layout.pxToPt(2);        
    }
    
    
    const baseFontSize = items.length >= 4 ? 12 : 14;
    
    
    const availableHeight = cardHeight - cardPadding * 2;
    const qAreaHeight = Math.floor(availableHeight * qAreaRatio);
    const aAreaHeight = availableHeight - qAreaHeight - qAGap;
    
    
    const qTop = currentY + cardPadding;
    const qText = item.q || '';
    
    
    const qParsed = parseInlineStyles(qText);
    const qFullText = `Q. ${qParsed.output}`;
    
    const qTextShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 
      area.left + cardPadding, qTop, 
      area.width - cardPadding * 2, qAreaHeight - layout.pxToPt(2));
    qTextShape.getFill().setTransparent();
    qTextShape.getBorder().setTransparent();
    
    
    const qTextRange = qTextShape.getText().setText(qFullText);
    applyTextStyle(qTextRange, {
      size: baseFontSize,
      color: CONFIG.COLORS.text_small_font,
      align: SlidesApp.ParagraphAlignment.LEFT
    });
    
    
    try {
      const qPrefixRange = qTextRange.getRange(0, 2); 
      qPrefixRange.getTextStyle()
        .setBold(true)
        .setForegroundColor(settings.primaryColor);
        
      
      if (qFullText.length > 3) {
        const qContentRange = qTextRange.getRange(3, qFullText.length); 
        qContentRange.getTextStyle().setBold(true);
      }
      
      
      qParsed.ranges.forEach(r => {
        const adjustedRange = qTextRange.getRange(r.start + 3, r.end + 3); 
        if (r.bold) adjustedRange.getTextStyle().setBold(true);
        if (r.color) adjustedRange.getTextStyle().setForegroundColor(r.color);
      });
    } catch (e) {}
    
    try {
      qTextShape.setContentAlignment(SlidesApp.ContentAlignment.TOP);
    } catch (e) {}

    
    const aTop = qTop + qAreaHeight + qAGap;
    const aText = item.a || '';
    
    
    const aParsed = parseInlineStyles(aText);
    const aFullText = `A. ${aParsed.output}`;
    
    
    const aIndent = layout.pxToPt(16);
    
    const aTextShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 
      area.left + cardPadding + aIndent, aTop, 
      area.width - cardPadding * 2 - aIndent, aAreaHeight - layout.pxToPt(2));
    aTextShape.getFill().setTransparent();
    aTextShape.getBorder().setTransparent();
    
    
    const aTextRange = aTextShape.getText().setText(aFullText);
    applyTextStyle(aTextRange, {
      size: baseFontSize,
      color: CONFIG.COLORS.text_small_font,
      align: SlidesApp.ParagraphAlignment.LEFT
    });
    
    
    try {
      const aPrefixRange = aTextRange.getRange(0, 2); 
      aPrefixRange.getTextStyle()
        .setBold(true)
        .setForegroundColor(generateTintedGray(settings.primaryColor, 15, 70));
        
      
      aParsed.ranges.forEach(r => {
        const adjustedRange = aTextRange.getRange(r.start + 3, r.end + 3); 
        if (r.bold) adjustedRange.getTextStyle().setBold(true);
        if (r.color) adjustedRange.getTextStyle().setForegroundColor(r.color);
      });
    } catch (e) {}
    
    try {
      aTextShape.setContentAlignment(SlidesApp.ContentAlignment.TOP);
    } catch (e) {}

    currentY += cardHeight + cardGap;
  });

  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}


function drawFaqItems(slide, items, layout, listArea, settings) {
  if (!items || !items.length) return;

  const px = v => layout.pxToPt(v);
  const GAP_ITEM = px(16); 
  const PADDING = px(20); 

  
  const totalCardHeight = listArea.height - (GAP_ITEM * (items.length - 1));
  const cardHeight = totalCardHeight / items.length;
  
  let currentY = listArea.top;

  items.forEach((qa) => {
    
    const card = slide.insertShape(
      SlidesApp.ShapeType.RECTANGLE,
      listArea.left, currentY,
      listArea.width, cardHeight
    );
    card.getFill().setSolidFill(CONFIG.COLORS.background_gray);
    card.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border);

    const q = qa.q || '';
    const a = qa.a || '';

    const qIconWidth = px(30);
    const qTextLeft = listArea.left + PADDING + qIconWidth;
    const qTextWidth = listArea.width - PADDING * 2 - qIconWidth;
    
    
    const qIcon = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX,
      listArea.left + PADDING, currentY + PADDING, qIconWidth, px(24));
    setStyledText(qIcon, 'Q.', { size: 18, bold: true, color: settings.primaryColor });

    
    const qBox = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX,
      qTextLeft, currentY + PADDING, qTextWidth, px(40));
    setStyledText(qBox, q, { size: 14, bold: true, color: CONFIG.COLORS.text_primary });
    
    const aTop = currentY + PADDING + px(35); 
    const aHeight = cardHeight - (PADDING * 2) - px(35); 

    
    let answerFontSize = 14;
    if (a.length > 100) {
      answerFontSize = 11;
    } else if (a.length > 60) {
      answerFontSize = 12.5;
    }

    
    const aIcon = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX,
      listArea.left + PADDING, aTop, qIconWidth, aHeight);
    const tintedGrayColor = generateTintedGray(settings.primaryColor, 15, 70);
    setStyledText(aIcon, 'A.', { size: 18, bold: true, color: tintedGrayColor });

    
    const aBox = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX,
      qTextLeft, aTop, qTextWidth, aHeight);
    setStyledText(aBox, a, { size: answerFontSize, color: CONFIG.COLORS.text_primary });

    
    try {
      [qIcon, qBox, aIcon, aBox].forEach(s => {
        s.setContentAlignment(SlidesApp.ContentAlignment.TOP);
        
        s.setAutofit(SlidesApp.AutofitType.SHRINK_ON_OVERFLOW);
      });
    } catch(e){}
    
    currentY += cardHeight + GAP_ITEM;
  });
}


function safeAlignTop(box){
  try { box.setContentAlignment(SlidesApp.ContentAlignment.TOP);
  } catch(e){}
}


function insertTrendIcon(slide, position, trend, settings) {
  const iconSize = 20;
  const icon = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 
    position.left, position.top - iconSize/2, iconSize, iconSize);
  
  let iconText = '';
  let iconColor = CONFIG.COLORS.text_primary;
  
  switch (trend) {
    case 'up':
      iconText = '↑';
      iconColor = CONFIG.COLORS.success_green;
      break;
    case 'down':
      iconText = '↓';
      iconColor = CONFIG.COLORS.error_red;
      break;
    case 'neutral':
      iconText = '→';
      iconColor = CONFIG.COLORS.neutral_gray;
      break;
    default:
      iconText = '→';
      iconColor = CONFIG.COLORS.neutral_gray;
  }
  
  setStyledText(icon, iconText, {
    size: 16,
    bold: true,
    color: iconColor,
    align: SlidesApp.ParagraphAlignment.CENTER
  });
  
  try {
    icon.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
  } catch (e) {}
  
  return icon;
}


function parseNumericValue(str) {
  if (typeof str !== 'string') return 0;
  const match = str.match(/(\d+(\.\d+)?)/);
  return match ? parseFloat(match[1]) : 0;
}


function createStatsCompareSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null; 
  drawStandardTitleHeader(slide, layout, 'compareSlide', data.title, settings, titleWidthPt, imageUpdateOption); 

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'compareSlide', data.subhead, subheadWidthPt); 
  

  const area = offsetRect(layout.getRect({
    left: 25,
    top: 130,
    width: 910,
    height: 330
  }), 0, dy);
  const stats = Array.isArray(data.stats) ? data.stats : [];
  if (stats.length === 0) {
    drawBottomBarAndFooter(slide, layout, pageNum, settings);
    return;
  }

  const headerHeight = layout.pxToPt(40); 

  
  const cushionTop = area.top + headerHeight; 
  const cushionHeight = area.height - headerHeight; 

  
  const totalContentWidth = area.width;
  const centerColWidth = totalContentWidth * 0.25; 
  const sideColWidth = (totalContentWidth - centerColWidth) / 2; 

  const leftValueColX = area.left;
  const centerLabelColX = leftValueColX + sideColWidth;
  const rightValueColX = centerLabelColX + centerColWidth;

  
  const labelColor = generateTintedGray(settings.primaryColor, 35, 70);

  const compareColors = generateCompareColors(settings.primaryColor);

  const contentAreaHeight = area.height - headerHeight;
  const rowHeight = contentAreaHeight / stats.length;

  const leftHeaderRect = { left: leftValueColX, top: area.top, width: sideColWidth, height: headerHeight };
  const rightHeaderRect = { left: rightValueColX, top: area.top, width: sideColWidth, height: headerHeight };
  const scHeaderOpt = { size: 14, bold: true, color: CONFIG.COLORS.background_gray, align: SlidesApp.ParagraphAlignment.CENTER };

  
  
  
  
  let __scHolisticDone = false;
  if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
    const __scSlideId = slide.getObjectId();
    const __scQLen = __REST_REQUESTS.length;
    try {
      if (cushionHeight > 0) {
        restAddShape(__scSlideId, { left: area.left, top: cushionTop, width: area.width, height: cushionHeight }, 'RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), noOutline: true });
      }
      restAddShape(__scSlideId, leftHeaderRect, 'RECTANGLE', { fillRgb: hexToRgb01(compareColors.left), noOutline: true });
      _restStyledAutofitBox(slide, leftHeaderRect, data.leftTitle || '', scHeaderOpt, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
      restAddShape(__scSlideId, rightHeaderRect, 'RECTANGLE', { fillRgb: hexToRgb01(compareColors.right), noOutline: true });
      _restStyledAutofitBox(slide, rightHeaderRect, data.rightTitle || '', scHeaderOpt, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
      let currentY = area.top + headerHeight;
      stats.forEach((stat, index) => {
        const centerY = currentY + rowHeight / 2;
        const valueHeight = layout.pxToPt(40);
        const scLabelRect = { left: centerLabelColX, top: centerY - valueHeight / 2, width: centerColWidth, height: valueHeight };
        const scLeftRect = { left: leftValueColX, top: centerY - valueHeight / 2, width: sideColWidth, height: valueHeight };
        const scRightRect = { left: rightValueColX, top: centerY - valueHeight / 2, width: sideColWidth, height: valueHeight };
        const scLabelOpt = { size: 14, align: SlidesApp.ParagraphAlignment.CENTER, bold: true };
        const scLeftOpt = { size: 22, bold: true, align: SlidesApp.ParagraphAlignment.CENTER, color: compareColors.left };
        const scRightOpt = { size: 22, bold: true, align: SlidesApp.ParagraphAlignment.CENTER, color: compareColors.right };
        _restStyledAutofitBox(slide, scLabelRect, stat.label || '', scLabelOpt, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
        _restStyledAutofitBox(slide, scLeftRect, stat.leftValue || '', scLeftOpt, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
        _restStyledAutofitBox(slide, scRightRect, stat.rightValue || '', scRightOpt, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
        if (index < stats.length - 1) {
          const lineY = currentY + rowHeight;
          restAddLine(__scSlideId, area.left + layout.pxToPt(15), lineY, area.left + area.width - layout.pxToPt(15), lineY, { colorRgb: hexToRgb01(CONFIG.COLORS.faint_gray), weight: 1 });
        }
        currentY += rowHeight;
      });
      __scHolisticDone = true;
    } catch (e) {
      __REST_REQUESTS.length = __scQLen;
      __scHolisticDone = false;
      Logger.log('[REST] statsCompare holistic失敗→SlidesAppフォールバック: ' + e.message);
    }
  }

  if (!__scHolisticDone) {
    
    
    if (cushionHeight > 0) {
        const cushion = slide.insertShape(
            SlidesApp.ShapeType.RECTANGLE,
            area.left,
            cushionTop, 
            area.width,
            cushionHeight 
        );
        cushion.getFill().setSolidFill(CONFIG.COLORS.background_gray);
        cushion.getBorder().setTransparent();
    }

    const leftHeader = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, leftValueColX, area.top, sideColWidth, headerHeight);
    leftHeader.getFill().setSolidFill(compareColors.left); 
    leftHeader.getBorder().setTransparent();
    setStyledText(leftHeader, data.leftTitle || '', {
      size: 14,
      bold: true,
      color: CONFIG.COLORS.background_gray,
      align: SlidesApp.ParagraphAlignment.CENTER
    });
    try { leftHeader.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch (e) {}

    const rightHeader = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, rightValueColX, area.top, sideColWidth, headerHeight);
    rightHeader.getFill().setSolidFill(compareColors.right); 
    rightHeader.getBorder().setTransparent();
    setStyledText(rightHeader, data.rightTitle || '', {
      size: 14,
      bold: true,
      color: CONFIG.COLORS.background_gray,
      align: SlidesApp.ParagraphAlignment.CENTER
    });
    try { rightHeader.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch (e) {}

    let currentY = area.top + headerHeight;
    stats.forEach((stat, index) => {
      const centerY = currentY + rowHeight / 2;
      const valueHeight = layout.pxToPt(40);

      const scLabelRect = { left: centerLabelColX, top: centerY - valueHeight / 2, width: centerColWidth, height: valueHeight };
      const scLeftRect = { left: leftValueColX, top: centerY - valueHeight / 2, width: sideColWidth, height: valueHeight };
      const scRightRect = { left: rightValueColX, top: centerY - valueHeight / 2, width: sideColWidth, height: valueHeight };
      const scLabelOpt = { size: 14, align: SlidesApp.ParagraphAlignment.CENTER, bold: true };
      const scLeftOpt = { size: 22, bold: true, align: SlidesApp.ParagraphAlignment.CENTER, color: compareColors.left };
      const scRightOpt = { size: 22, bold: true, align: SlidesApp.ParagraphAlignment.CENTER, color: compareColors.right };

      
      const labelShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, scLabelRect.left, scLabelRect.top, scLabelRect.width, scLabelRect.height);
      setStyledText(labelShape, stat.label || '', scLabelOpt);
      try { labelShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch (e) {}

      
      const leftValueShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, scLeftRect.left, scLeftRect.top, scLeftRect.width, scLeftRect.height);
      setStyledText(leftValueShape, stat.leftValue || '', scLeftOpt);
      try { leftValueShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch (e) {}

      
      const rightValueShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, scRightRect.left, scRightRect.top, scRightRect.width, scRightRect.height);
      setStyledText(rightValueShape, stat.rightValue || '', scRightOpt);
      try { rightValueShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch (e) {}

      

      
      if (index < stats.length - 1) {
        const lineY = currentY + rowHeight;
        const line = slide.insertLine(SlidesApp.LineCategory.STRAIGHT, area.left + layout.pxToPt(15), lineY, area.left + area.width - layout.pxToPt(15), lineY);
        line.getLineFill().setSolidFill(CONFIG.COLORS.faint_gray);
        line.setWeight(1);
      }

      currentY += rowHeight;
    });
  }

  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}


function createBarCompareSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null; 
  drawStandardTitleHeader(slide, layout, 'compareSlide', data.title, settings, titleWidthPt, imageUpdateOption); 

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'compareSlide', data.subhead, subheadWidthPt); 
  

  const area = offsetRect(layout.getRect({
    left: 40,
    top: 130,
    width: 880,
    height: 340
  }), 0, dy);
  const stats = Array.isArray(data.stats) ? data.stats : [];
  if (stats.length === 0) {
    drawBottomBarAndFooter(slide, layout, pageNum, settings);
    return;
  }

  
  const showTrends = !!data.showTrends;

  
  const blockMargin = layout.pxToPt(20);
  const totalContentHeight = area.height - (blockMargin * (stats.length - 1));
  const blockHeight = totalContentHeight / stats.length;
  let currentY = area.top;

  stats.forEach(stat => {
    const blockTop = currentY;
    const titleHeight = layout.pxToPt(35); 
    const barAreaHeight = blockHeight - titleHeight;
    const barRowHeight = barAreaHeight / 2;

    
    const shouldShowTrend = showTrends && stat.trend;

    
    const bcTitleRect = { left: area.left, top: blockTop, width: area.width, height: titleHeight };
    const bcTitleOpt = { size: 16, bold: true };
    if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
      try { _restStyledAutofitBox(slide, bcTitleRect, stat.label || '', bcTitleOpt, SlidesApp.ContentAlignment.BOTTOM, null, { noAutofit: true }); }
      catch (e) { const s = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, bcTitleRect.left, bcTitleRect.top, bcTitleRect.width, bcTitleRect.height); setStyledText(s, stat.label || '', bcTitleOpt); try { s.setContentAlignment(SlidesApp.ContentAlignment.BOTTOM); } catch(e2){} }
    } else {
    const statTitleShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, area.left, blockTop, area.width, titleHeight);
    
    setStyledText(statTitleShape, stat.label || '', bcTitleOpt);
    try { statTitleShape.setContentAlignment(SlidesApp.ContentAlignment.BOTTOM); } catch(e){}
    }

    const asIsY = blockTop + titleHeight;
    const toBeY = asIsY + barRowHeight;

    const labelWidth = layout.pxToPt(90);  
    const valueWidth = layout.pxToPt(140); 
    const barWidth = Math.max(layout.pxToPt(50), area.width - labelWidth - valueWidth - layout.pxToPt(10));
    const barLeft = area.left + labelWidth;
    const barHeight = layout.pxToPt(18); 

    const val1 = parseNumericValue(stat.leftValue);
    const val2 = parseNumericValue(stat.rightValue);
    const maxValue = Math.max(val1, val2, 1);

    
    const asIsLabelRect = { left: area.left, top: asIsY, width: labelWidth, height: barRowHeight };
    const asIsValueRect = { left: barLeft + barWidth, top: asIsY, width: valueWidth, height: barRowHeight };
    const asIsLabelOpt = { size: 13, color: CONFIG.COLORS.neutral_gray };
    const asIsValueOpt = { size: 18, bold: true, align: SlidesApp.ParagraphAlignment.END };
    if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
      try { _restStyledAutofitBox(slide, asIsLabelRect, '現状', asIsLabelOpt, null, null, { noAutofit: true }); _restStyledAutofitBox(slide, asIsValueRect, stat.leftValue || '', asIsValueOpt, null, null, { noAutofit: true }); }
      catch (e) { setStyledText(slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, asIsLabelRect.left, asIsLabelRect.top, asIsLabelRect.width, asIsLabelRect.height), '現状', asIsLabelOpt); setStyledText(slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, asIsValueRect.left, asIsValueRect.top, asIsValueRect.width, asIsValueRect.height), stat.leftValue || '', asIsValueOpt); }
    } else {
      setStyledText(slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, asIsLabelRect.left, asIsLabelRect.top, asIsLabelRect.width, asIsLabelRect.height), '現状', asIsLabelOpt);
      setStyledText(slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, asIsValueRect.left, asIsValueRect.top, asIsValueRect.width, asIsValueRect.height), stat.leftValue || '', asIsValueOpt);
    }
    
    
    const asIsTrack = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, barLeft, asIsY + barRowHeight/2 - barHeight/2, barWidth, barHeight);
    asIsTrack.getFill().setSolidFill(CONFIG.COLORS.faint_gray);
    asIsTrack.getBorder().setTransparent();
    const asIsFillWidth = Math.max(layout.pxToPt(2), barWidth * (val1 / maxValue));
    const asIsFill = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, barLeft, asIsY + barRowHeight/2 - barHeight/2, asIsFillWidth, barHeight);
    asIsFill.getFill().setSolidFill(CONFIG.COLORS.neutral_gray);
    asIsFill.getBorder().setTransparent();

    
    const toBeLabelRect = { left: area.left, top: toBeY, width: labelWidth, height: barRowHeight };
    const toBeValueRect = { left: barLeft + barWidth, top: toBeY, width: valueWidth, height: barRowHeight };
    const toBeLabelOpt = { size: 13, color: settings.primaryColor, bold: true };
    const toBeValueOpt = { size: 18, bold: true, color: settings.primaryColor, align: SlidesApp.ParagraphAlignment.END };
    if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
      try { _restStyledAutofitBox(slide, toBeLabelRect, '導入後', toBeLabelOpt, null, null, { noAutofit: true }); _restStyledAutofitBox(slide, toBeValueRect, stat.rightValue || '', toBeValueOpt, null, null, { noAutofit: true }); }
      catch (e) { setStyledText(slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, toBeLabelRect.left, toBeLabelRect.top, toBeLabelRect.width, toBeLabelRect.height), '導入後', toBeLabelOpt); setStyledText(slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, toBeValueRect.left, toBeValueRect.top, toBeValueRect.width, toBeValueRect.height), stat.rightValue || '', toBeValueOpt); }
    } else {
      setStyledText(slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, toBeLabelRect.left, toBeLabelRect.top, toBeLabelRect.width, toBeLabelRect.height), '導入後', toBeLabelOpt);
      setStyledText(slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, toBeValueRect.left, toBeValueRect.top, toBeValueRect.width, toBeValueRect.height), stat.rightValue || '', toBeValueOpt);
    }
    
    
    const toBeTrack = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, barLeft, toBeY + barRowHeight/2 - barHeight/2, barWidth, barHeight);
    toBeTrack.getFill().setSolidFill(generateTintedGray(settings.primaryColor, 20, 96));
    toBeTrack.getBorder().setTransparent();
    const toBeFillWidth = Math.max(layout.pxToPt(2), barWidth * (val2 / maxValue));
    
    
    const shape = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, barLeft, toBeY + barRowHeight/2 - barHeight/2, toBeFillWidth, barHeight);
    shape.getFill().setSolidFill(settings.primaryColor);
    shape.getBorder().setTransparent();

    try {
        [asIsLabel, asIsValue, toBeLabel, toBeValue].forEach(shape => shape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE));
    } catch(e){}

    if (shouldShowTrend) {
      const trendIcon = insertTrendIcon(slide, { left: barLeft + barWidth + layout.pxToPt(10), top: toBeY + barRowHeight/2 }, stat.trend, settings);
    }

    
    currentY += blockHeight + blockMargin;
  });

  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}



function smartFormatTriangleText(text) {
  if (!text || text.length <= 30) {
    
    return { text: text, isSimple: true, headerLength: 0 };
  }
  
  
  const separators = [
    { pattern: '：', priority: 1 },
    { pattern: ':', priority: 2 },
    { pattern: '。', priority: 3 },
    { pattern: 'について', priority: 4, keepSeparator: true },
    { pattern: 'における', priority: 5, keepSeparator: true }
  ];
  
  for (let sep of separators) {
    const index = text.indexOf(sep.pattern);
    if (index > 5 && index < text.length * 0.6) { 
      const headerEnd = sep.keepSeparator ? index + sep.pattern.length : index;
      const header = text.substring(0, headerEnd).trim();
      const body = text.substring(index + sep.pattern.length).trim();
      
      if (header.length >= 3 && body.length >= 3) {
        return {
          text: `${header}\n${body}`,
          isSimple: false,
          headerLength: header.length
        };
      }
    }
  }
  
  
  if (text.length > 50) {
    const midPoint = Math.floor(text.length * 0.4);
    const header = text.substring(0, midPoint).trim();
    const body = text.substring(midPoint).trim();
    
    return {
      text: `${header}\n${body}`,
      isSimple: false,
      headerLength: header.length
    };
  }
  
  
  return { text: text, isSimple: true, headerLength: 0 };
}



function computeTriangleGeometry(data, layout) {
  
  const dy = (data && data.subhead) ? layout.pxToPt(36) : 0;
  const baseArea = layout.getRect('triangleSlide.area');
  const adjustedArea = adjustAreaForSubhead(baseArea, data && data.subhead, layout); 
  const area = offsetRect(adjustedArea, 0, dy);
  area.top -= layout.pxToPt(26.67); 

  const items = Array.isArray(data && data.items) ? data.items.slice(0, 3) : [];

  
  const IMAGE_ADJUSTMENTS_LARGE = {
    topLeft:  { sizePt: layout.pxToPt(76), offsetXPt: layout.pxToPt(-105), offsetYPt: layout.pxToPt(-25) },
    topRight: { sizePt: layout.pxToPt(78), offsetXPt: layout.pxToPt(100),  offsetYPt: layout.pxToPt(-30) },
    bottom:   { sizePt: layout.pxToPt(95), offsetXPt: layout.pxToPt(0),    offsetYPt: layout.pxToPt(0)  }
  };
  const IMAGE_ADJUSTMENTS_MEDIUM = {
    topLeft:  { sizePt: layout.pxToPt(78), offsetXPt: layout.pxToPt(-95), offsetYPt: layout.pxToPt(-35) },
    topRight: { sizePt: layout.pxToPt(80), offsetXPt: layout.pxToPt(90),  offsetYPt: layout.pxToPt(-40) },
    bottom:   { sizePt: layout.pxToPt(100), offsetXPt: layout.pxToPt(0),   offsetYPt: layout.pxToPt(10)  }
  };
  const IMAGE_ADJUSTMENTS_SMALL = {
    topLeft:  { sizePt: layout.pxToPt(80), offsetXPt: layout.pxToPt(-85), offsetYPt: layout.pxToPt(-25) },
    topRight: { sizePt: layout.pxToPt(82), offsetXPt: layout.pxToPt(80),  offsetYPt: layout.pxToPt(-30) },
    bottom:   { sizePt: layout.pxToPt(105), offsetXPt: layout.pxToPt(0),   offsetYPt: layout.pxToPt(0)   }
  };

  const textLengths = items.map(item => (typeof item === 'string' ? item : (item.title || '') + (item.desc || '')).length);
  const maxLength = Math.max(0, ...textLengths);
  const avgLength = textLengths.length > 0 ? textLengths.reduce((sum, len) => sum + len, 0) / textLengths.length : 0;
  let cardW, cardH, fontSize, selectedAdjustments, isLargeCardSize = false;
  if (maxLength > 60 || avgLength > 40) {
    cardW = layout.pxToPt(340); cardH = layout.pxToPt(160); fontSize = 13;
    selectedAdjustments = IMAGE_ADJUSTMENTS_LARGE; isLargeCardSize = true;
  } else if (maxLength > 35 || avgLength > 25) {
    cardW = layout.pxToPt(290); cardH = layout.pxToPt(135); fontSize = 14;
    selectedAdjustments = IMAGE_ADJUSTMENTS_MEDIUM;
  } else {
    cardW = layout.pxToPt(250); cardH = layout.pxToPt(115); fontSize = 15;
    selectedAdjustments = IMAGE_ADJUSTMENTS_SMALL;
  }
  const maxCardW = (area.width - layout.pxToPt(160)) / 1.5;
  const maxCardH = (area.height - layout.pxToPt(80)) / 2;
  cardW = Math.min(cardW, maxCardW); cardH = Math.min(cardH, maxCardH);

  const bottomCardMarginPt = isLargeCardSize ? layout.pxToPt(50) : layout.pxToPt(80);

  const positions = [
    { x: area.left + area.width / 2,                              y: area.top + layout.pxToPt(40) + cardH / 2 }, 
    { x: area.left + area.width - bottomCardMarginPt - cardW / 2, y: area.top + area.height - cardH / 2 },       
    { x: area.left + bottomCardMarginPt + cardW / 2,             y: area.top + area.height - cardH / 2 }         
  ];

  return { items, area, cardW, cardH, fontSize, selectedAdjustments, isLargeCardSize, positions };
}



var TRIANGLE_ARROW_CONFIGS = {
  topLeft:  { endX: -60, endY: 0, curveIntensity: 10, rotation: -55, flipX: 0, flipY: 0 },
  topRight: { endX: -60, endY: 0, curveIntensity: 10, rotation: 40,  flipX: 0, flipY: 0 },
  bottom:   { endX: 60,  endY: 0, curveIntensity: 0,  rotation: 0,   flipX: 0, flipY: 0 }
};

var TRIANGLE_ARROWHEAD_PATH_D = 'M 0 -43.30127 L -25 0 H 25 Z';
var TRIANGLE_ARROW_STROKE_WIDTH = 21.566; 


function _triangleArrowLocalShape(cfg) {
  const startX = 100, startY = 100;
  const endPointMin = 10, endPointMax = 490; 
  let relativeY_base = (cfg.endY || 0) * -1;
  let intensity_base = Math.max(-100, Math.min((cfg.curveIntensity || 0), 100));
  const flipX = (cfg.flipX === 1) ? -1 : 1;
  const flipY = (cfg.flipY === 1) ? -1 : 1;
  const rX_flipped = (cfg.endX || 0) * flipX;
  const rY_flipped = relativeY_base * flipY;
  const intensity = intensity_base * flipX * flipY;
  const rad = (cfg.rotation || 0) * Math.PI / 180;
  const cos_r = Math.cos(rad), sin_r = Math.sin(rad);
  const relativeX = rX_flipped * cos_r - rY_flipped * sin_r;
  const relativeY = rX_flipped * sin_r + rY_flipped * cos_r;
  let ex = startX + relativeX, ey = startY + relativeY;
  ex = Math.max(endPointMin, Math.min(ex, endPointMax));
  ey = Math.max(endPointMin, Math.min(ey, endPointMax));
  const midX = (startX + ex) / 2, midY = (startY + ey) / 2;
  const vx = ex - startX, vy = ey - startY, len = Math.sqrt(vx * vx + vy * vy);
  let ctrlX, ctrlY;
  if (len === 0 || intensity === 0) { ctrlX = midX; ctrlY = midY; }
  else { const normX = -vy / len, normY = vx / len; ctrlX = midX + normX * intensity; ctrlY = midY + normY * intensity; }
  const stemD = 'M ' + startX + ',' + startY + ' Q ' + ctrlX + ',' + ctrlY + ' ' + ex + ',' + ey;
  const angleRad = Math.atan2(startY - ctrlY, startX - ctrlX);
  const angleDeg = (angleRad * 180 / Math.PI) + 90;
  
  
  
  const hw = TRIANGLE_ARROW_STROKE_WIDTH / 2;
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
  const acc = (x, y) => { if (x < minX) minX = x; if (y < minY) minY = y; if (x > maxX) maxX = x; if (y > maxY) maxY = y; };
  const qAt = (t) => { const mt = 1 - t; return [mt * mt * startX + 2 * mt * t * ctrlX + t * t * ex, mt * mt * startY + 2 * mt * t * ctrlY + t * t * ey]; };
  const qExtremaT = (p0, pc, p1) => { const den = p0 - 2 * pc + p1; if (Math.abs(den) < 1e-9) return -1; const t = (p0 - pc) / den; return (t > 0 && t < 1) ? t : -1; };
  const stemPts = [[startX, startY], [ex, ey]]; 
  [qExtremaT(startX, ctrlX, ex), qExtremaT(startY, ctrlY, ey)].forEach(t => { if (t >= 0) stemPts.push(qAt(t)); }); 
  stemPts.forEach(p => { acc(p[0] - hw, p[1] - hw); acc(p[0] + hw, p[1] + hw); });
  
  const a = angleDeg * Math.PI / 180, ca = Math.cos(a), sa = Math.sin(a);
  [[0, -43.30127], [-25, 0], [25, 0]].forEach(p => {
    const rx = p[0] * ca - p[1] * sa, ry = p[0] * sa + p[1] * ca;
    acc(startX + rx, startY + ry);
  });
  return { startX, startY, endX: ex, endY: ey, ctrlX, ctrlY, angleDeg, stemD, bbox: { minX, minY, maxX, maxY } };
}

function createTriangleSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout); 
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null; 
  drawStandardTitleHeader(slide, layout, 'triangleSlide', data.title, settings, titleWidthPt, imageUpdateOption); 

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  drawSubheadIfAny(slide, layout, 'triangleSlide', data.subhead, subheadWidthPt); 
  

  
  const geo = computeTriangleGeometry(data, layout);
  const items = geo.items;
  const area = geo.area;
  const cardW = geo.cardW;
  const cardH = geo.cardH;
  const fontSize = geo.fontSize;
  const selectedAdjustments = geo.selectedAdjustments;
  const positions = geo.positions;
  if (items.length === 0) { 
    drawBottomBarAndFooter(slide, layout, pageNum, settings);
    return;
  }

  
  
  
  const triangleArrows = settings.triangleArrows; 
  const hasValidImageData = !__NATIVE_DECOR_ACTIVE && triangleArrows &&
                             triangleArrows.topLeftArrowBase64 &&
                             triangleArrows.topRightArrowBase64 &&
                             triangleArrows.bottomArrowBase64;
  const __useRestForTriangle = __REST_TEXT_ACTIVE && __REST_PRESENTATION_ID && !hasValidImageData;
  const __triSid = (__useRestForTriangle) ? slide.getObjectId() : null;

  
  positions.forEach((pos, i) => {
    if (!items[i]) return; 
    const cardX = pos.x - cardW / 2;
    const cardY = pos.y - cardH / 2;
    const cardRect = { left: cardX, top: cardY, width: cardW, height: cardH };
    
    const item = items[i] || {};
    const itemTitle = typeof item === 'string' ? '' : (item.title || '');
    const itemDesc = typeof item === 'string' ? item : (item.desc || '');

    let __triCardDone = false;
    if (__useRestForTriangle) {
      const __triQLen = __REST_REQUESTS.length;
      try {
        restAddShape(__triSid, cardRect, 'ROUND_RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), outlineRgb: hexToRgb01(CONFIG.COLORS.card_border), outlineWeight: 1 });
        let __tText, __tBase, __tExtra = [];
        if (typeof item === 'string' || !itemTitle) {
          const itemText = typeof item === 'string' ? item : itemDesc;
          const processedText = smartFormatTriangleText(itemText);
          if (processedText.isSimple) {
            
            __tText = processedText.text;
            __tBase = { size: 16, bold: true, color: CONFIG.COLORS.text_small_font, align: SlidesApp.ParagraphAlignment.CENTER };
          } else {
            const lines = processedText.text.split('\n');
            const header = lines[0] || '';
            const body = lines.slice(1).join('\n') || '';
            __tText = `${header}\n${body}`;
            __tBase = { size: Math.max(fontSize - 3, 11), bold: false, color: CONFIG.COLORS.text_small_font, align: SlidesApp.ParagraphAlignment.CENTER };
            if (header.length > 0) __tExtra.push({ start: 0, end: header.length, bold: true, color: settings.primaryColor, size: 16 });
          }
        } else {
          const enhancedText = itemDesc ? `${itemTitle}\n${itemDesc}` : itemTitle;
          __tText = enhancedText;
          __tBase = { size: Math.max(fontSize - 3, 11), bold: false, color: CONFIG.COLORS.text_small_font, align: SlidesApp.ParagraphAlignment.CENTER };
          if (itemTitle.length > 0) __tExtra.push({ start: 0, end: itemTitle.length, bold: true, color: settings.primaryColor, size: 16 });
        }
        _restStyledAutofitBox(slide, cardRect, __tText, __tBase, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true, extraRanges: __tExtra, boldRunSize: 16 });
        __triCardDone = true;
      } catch (e) {
        __REST_REQUESTS.length = __triQLen;
        __triCardDone = false;
        Logger.log('[REST] triangle card holistic失敗→SlidesAppフォールバック: ' + e.message);
      }
    }

    if (!__triCardDone) {
    
    const card = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, cardX, cardY, cardW, cardH);
    card.getFill().setSolidFill(CONFIG.COLORS.background_gray);
    card.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border);

    
    if (typeof item === 'string' || !itemTitle) {
        
        const itemText = typeof item === 'string' ? item : itemDesc;
        const processedText = smartFormatTriangleText(itemText); 
        if (processedText.isSimple) { 
            let appliedFontSize = fontSize;
            if ((processedText.text || '').length > 35) { appliedFontSize = Math.max(fontSize - 1, 12); } 
            setStyledText(card, processedText.text, { size: appliedFontSize, bold: true, color: CONFIG.COLORS.text_small_font, align: SlidesApp.ParagraphAlignment.CENTER });
        } else { 
            const lines = processedText.text.split('\n');
            const header = lines[0] || '';
            const body = lines.slice(1).join('\n') || '';
            const enhancedText = `${header}\n${body}`;
            const headerFontSize = Math.max(fontSize - 1, 13); 
            const bodyFontSize = Math.max(fontSize - 3, 11);   
            setStyledText(card, enhancedText, { size: bodyFontSize, bold: false, color: CONFIG.COLORS.text_small_font, align: SlidesApp.ParagraphAlignment.CENTER });
            try { 
                const textRange = card.getText();
                const headerEndIndex = header.length;
                if (headerEndIndex > 0) {
                    const headerRange = textRange.getRange(0, headerEndIndex);
                    headerRange.getTextStyle().setBold(true).setFontSize(headerFontSize).setForegroundColor(settings.primaryColor);
                }
            } catch (e) { Logger.log(`Error styling triangle header (auto-split): ${e.message}`); }
        }
    } else {
        
        const enhancedText = itemDesc ? `${itemTitle}\n${itemDesc}` : itemTitle; 
        const headerFontSize = Math.max(fontSize - 1, 13); 
        const bodyFontSize = Math.max(fontSize - 3, 11);   
        setStyledText(card, enhancedText, { size: bodyFontSize, bold: false, color: CONFIG.COLORS.text_small_font, align: SlidesApp.ParagraphAlignment.CENTER });
        try { 
            const textRange = card.getText();
            const headerEndIndex = itemTitle.length;
            if (headerEndIndex > 0) {
                const headerRange = textRange.getRange(0, headerEndIndex);
                headerRange.getTextStyle().setBold(true).setFontSize(headerFontSize).setForegroundColor(settings.primaryColor);
            }
        } catch (e) { Logger.log(`Error styling triangle header (structured): ${e.message}`); }
    }
    
    try { setBoldTextSize(card, 16); } catch(e) { Logger.log(`Error applying setBoldTextSize in triangle slide: ${e.message}`); }
    
    try { card.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch (e) {}
    } 
  });

  
  
  
  

  if (hasValidImageData && items.length === 3) { 
    Logger.log('Triangle: Inserting adjusted arrow images.');

    
    function insertTriangleArrowImage(arrowName, base64Data, left, top, size) {
    if (imageUpdateOption !== 'update') return;
      if (!base64Data) {
        Logger.log(`Triangle Arrow Image data for "${arrowName}" is missing or null. Skipping.`);
        return; 
      }
      try {
        const imageData = insertImageFromUrlOrFileId(base64Data); 
        if (imageData) {
          const img = slide.insertImage(imageData); 
          
          const originalWidth = img.getWidth();
          const originalHeight = img.getHeight();
          let targetWidth = size;
          let targetHeight = size;
          if (originalWidth > originalHeight) {
              targetHeight = size * (originalHeight / originalWidth);
          } else if (originalHeight > originalWidth) {
              targetWidth = size * (originalWidth / originalHeight);
          }
          
          img.setLeft(left + (size - targetWidth) / 2)
             .setTop(top + (size - targetHeight) / 2)
             .setWidth(targetWidth)
             .setHeight(targetHeight);
        }
      } catch (e) {
        Logger.log(`Error inserting triangle arrow image "${arrowName}": ${e.message}`); 
      }
    }

    

    
    const arrow1_baseX = (positions[2].x + positions[0].x) / 2; 
    const arrow1_baseY = (positions[2].y + positions[0].y) / 2; 
    const adj1 = selectedAdjustments.topLeft; 
    insertTriangleArrowImage(
      'topLeft',
      triangleArrows.topLeftArrowBase64,
      arrow1_baseX - (adj1.sizePt / 2) + adj1.offsetXPt,
      arrow1_baseY - (adj1.sizePt / 2) + adj1.offsetYPt,
      adj1.sizePt
    );

    
    const arrow2_baseX = (positions[0].x + positions[1].x) / 2; 
    const arrow2_baseY = (positions[0].y + positions[1].y) / 2; 
    const adj2 = selectedAdjustments.topRight; 
    insertTriangleArrowImage(
      'topRight',
      triangleArrows.topRightArrowBase64,
      arrow2_baseX - (adj2.sizePt / 2) + adj2.offsetXPt,
      arrow2_baseY - (adj2.sizePt / 2) + adj2.offsetYPt,
      adj2.sizePt
    );

    
    const arrow3_baseX = (positions[1].x + positions[2].x) / 2; 
    const arrow3_baseY = (positions[1].y + positions[2].y) / 2; 
    const adj3 = selectedAdjustments.bottom; 
    insertTriangleArrowImage(
      'bottom',
      triangleArrows.bottomArrowBase64,
      arrow3_baseX - (adj3.sizePt / 2) + adj3.offsetXPt,
      arrow3_baseY - (adj3.sizePt / 2) + adj3.offsetYPt,
      adj3.sizePt
    );

  } else if (items.length === 3 && imageUpdateOption === 'update' && !__NATIVE_DECOR_ACTIVE) {
    
    
    Logger.log('Triangle: Arrow image data missing, invalid or items count not 3. Falling back to drawing lines.');
    if (items.length === 3) { 
      const arrowPadding = cardW > layout.pxToPt(300) ? layout.pxToPt(25) : layout.pxToPt(20); 
      const cardEdges = [];
      cardEdges[0] = { rightCenter: { x: positions[0].x + cardW / 2, y: positions[0].y }, leftCenter: { x: positions[0].x - cardW / 2, y: positions[0].y }, bottomCenter: { x: positions[0].x, y: positions[0].y + cardH / 2 } }; 
      cardEdges[1] = { leftCenter: { x: positions[1].x - cardW / 2, y: positions[1].y }, topCenter: { x: positions[1].x, y: positions[1].y - cardH / 2 } }; 
      cardEdges[2] = { rightCenter: { x: positions[2].x + cardW / 2, y: positions[2].y }, topCenter: { x: positions[2].x, y: positions[2].y - cardH / 2 } }; 
      const arrowLines = [];
      arrowLines[0] = { startX: cardEdges[0].rightCenter.x + arrowPadding, startY: cardEdges[0].rightCenter.y, endX: cardEdges[1].topCenter.x + arrowPadding / 2 , endY: cardEdges[1].topCenter.y - arrowPadding}; 
      arrowLines[1] = { startX: cardEdges[1].leftCenter.x - arrowPadding, startY: cardEdges[1].leftCenter.y + arrowPadding / 2, endX: cardEdges[2].rightCenter.x + arrowPadding, endY: cardEdges[2].rightCenter.y + arrowPadding / 2}; 
      arrowLines[2] = { startX: cardEdges[2].topCenter.x - arrowPadding / 2, startY: cardEdges[2].topCenter.y - arrowPadding, endX: cardEdges[0].leftCenter.x - arrowPadding, endY: cardEdges[0].leftCenter.y}; 
      
      let __triLineDone = false;
      if (__useRestForTriangle) {
        const __lnQLen = __REST_REQUESTS.length;
        try {
          arrowLines.forEach(lineDef => {
            restAddLine(__triSid, lineDef.startX, lineDef.startY, lineDef.endX, lineDef.endY, { colorRgb: hexToRgb01(CONFIG.COLORS.ghost_gray), weight: 4, endArrow: 'FILL_ARROW' });
          });
          __triLineDone = true;
        } catch (e) {
          __REST_REQUESTS.length = __lnQLen;
          __triLineDone = false;
          Logger.log('[REST] triangle arrow line失敗→SlidesAppフォールバック: ' + e.message);
        }
      }
      if (!__triLineDone) {
      arrowLines.forEach(lineDef => {
        try {
          const line = slide.insertLine(
            SlidesApp.LineCategory.STRAIGHT,
            lineDef.startX, lineDef.startY,
            lineDef.endX, lineDef.endY
          );
          line.getLineFill().setSolidFill(CONFIG.COLORS.ghost_gray); 
          line.setWeight(4); 
          line.setEndArrow(SlidesApp.ArrowStyle.FILL_ARROW); 
        } catch (e) {
          Logger.log(`Error drawing fallback arrow line: ${e.message}`); 
        }
      });
      }
    }
  }

  drawBottomBarAndFooter(slide, layout, pageNum, settings); 
} 


function createPyramidSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null;
  Logger.log("★タイトルの大きさ：" + titleWidthPt);
  drawStandardTitleHeader(slide, layout, 'pyramidSlide', data.title, settings, titleWidthPt, imageUpdateOption);

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'pyramidSlide', data.subhead, subheadWidthPt); 
  
  
  
  const baseArea = layout.getRect('pyramidSlide.pyramidArea');
  const adjustedArea = adjustAreaForSubhead(baseArea, data.subhead, layout);
  const area = offsetRect(adjustedArea, 0, dy);

  
  const levels = Array.isArray(data.levels) ? data.levels.slice(0, 4) : [];
  if (levels.length === 0) {
    drawBottomBarAndFooter(slide, layout, pageNum, settings);
    return;
  }

  const levelHeight = layout.pxToPt(70); 
  const levelGap = layout.pxToPt(2); 
  const totalHeight = (levelHeight * levels.length) + (levelGap * (levels.length - 1));
  const startY = area.top + (area.height - totalHeight) / 2;

  
  const pyramidWidth = layout.pxToPt(480); 
  const gap = layout.pxToPt(30); 

  const pyramidLeft = area.left;
  const textColumnLeft = pyramidLeft + pyramidWidth + gap;
  
  
  const __pyrRightLimit = layout.contentRight_pt;
  let textColumnWidth = layout.pxToPt(400); 
  if (textColumnLeft + textColumnWidth > __pyrRightLimit) {
    textColumnWidth = Math.max(layout.pxToPt(120), __pyrRightLimit - textColumnLeft);
  }
  

  
  const pyramidColors = generatePyramidColors(settings.primaryColor, levels.length);
  
  
  const baseWidth = pyramidWidth;
  const widthIncrement = baseWidth / levels.length;
  const centerX = pyramidLeft + pyramidWidth / 2; 

  
  
  
  let __pyDone = false;
  if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
    const __pySlideId = slide.getObjectId();
    const __pyQLen = __REST_REQUESTS.length;
    try {
      levels.forEach((level, index) => {
        const levelWidth = baseWidth - (widthIncrement * (levels.length - 1 - index));
        const levelX = centerX - levelWidth / 2;
        const levelY = startY + index * (levelHeight + levelGap);
        restAddShape(__pySlideId, { left: levelX, top: levelY, width: levelWidth, height: levelHeight }, 'ROUND_RECTANGLE', { fillRgb: hexToRgb01(pyramidColors[index]), noOutline: true });
        const levelTitle = level.title || `レベル${index + 1}`;
        _restStyledAutofitBox(slide, { left: levelX, top: levelY, width: levelWidth, height: levelHeight }, levelTitle, { size: CONFIG.FONTS.sizes.body, bold: true, color: CONFIG.COLORS.background_gray, align: SlidesApp.ParagraphAlignment.CENTER }, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
        const connectionStartX = levelX + levelWidth;
        const connectionEndX = textColumnLeft;
        const connectionY = levelY + levelHeight / 2;
        if (connectionEndX > connectionStartX) {
          restAddLine(__pySlideId, connectionStartX, connectionY, connectionEndX, connectionY, { colorRgb: hexToRgb01('#D0D7DE'), weight: 1.5 });
        }
        const levelDesc = level.description || '';
        let formattedText;
        if (levelDesc.includes('•') || levelDesc.includes('・')) { formattedText = levelDesc; }
        else if (levelDesc.includes('\n')) { const lines = levelDesc.split('\n').filter(line => line.trim()).slice(0, 2); formattedText = lines.map(line => `• ${line.trim()}`).join('\n'); }
        else { formattedText = levelDesc; }
        _restStyledAutofitBox(slide, { left: textColumnLeft, top: levelY, width: textColumnWidth, height: levelHeight }, formattedText, { size: CONFIG.FONTS.sizes.body - 1, align: SlidesApp.ParagraphAlignment.LEFT, color: CONFIG.COLORS.text_primary, bold: true }, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
      });
      __pyDone = true;
    } catch (e) {
      __REST_REQUESTS.length = __pyQLen;
      __pyDone = false;
      Logger.log('[REST] pyramid holistic失敗→SlidesAppフォールバック: ' + e.message);
    }
  }

  if (!__pyDone) {
  levels.forEach((level, index) => {
    const levelWidth = baseWidth - (widthIncrement * (levels.length - 1 - index)); 
    const levelX = centerX - levelWidth / 2; 
    const levelY = startY + index * (levelHeight + levelGap);

    
    const levelBox = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, levelX, levelY, levelWidth, levelHeight);
    levelBox.getFill().setSolidFill(pyramidColors[index]); 
    levelBox.getBorder().setTransparent();

    
    const titleShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, levelX, levelY, levelWidth, levelHeight);
    titleShape.getFill().setTransparent();
    titleShape.getBorder().setTransparent();

    const levelTitle = level.title || `レベル${index + 1}`;
    setStyledText(titleShape, levelTitle, {
      size: CONFIG.FONTS.sizes.body,
      bold: true,
      color: CONFIG.COLORS.background_gray,
      align: SlidesApp.ParagraphAlignment.CENTER
    });

    try {
      titleShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
    } catch (e) {}

    
    const connectionStartX = levelX + levelWidth;
    const connectionEndX = textColumnLeft;
    const connectionY = levelY + levelHeight / 2;
    
    if (connectionEndX > connectionStartX) {
      const connectionLine = slide.insertLine(
        SlidesApp.LineCategory.STRAIGHT,
        connectionStartX,
        connectionY,
        connectionEndX,
        connectionY
      );
      connectionLine.getLineFill().setSolidFill('#D0D7DE'); 
      connectionLine.setWeight(1.5);
    }

    
    const textShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, textColumnLeft, levelY, textColumnWidth, levelHeight);
    textShape.getFill().setTransparent();
    textShape.getBorder().setTransparent();

    const levelDesc = level.description || '';
    
    
    let formattedText;
    if (levelDesc.includes('•') || levelDesc.includes('・')) {
      
      formattedText = levelDesc;
    } else if (levelDesc.includes('\n')) {
      
      const lines = levelDesc.split('\n').filter(line => line.trim()).slice(0, 2);
      formattedText = lines.map(line => `• ${line.trim()}`).join('\n');
    } else {
      
      formattedText = levelDesc;
    }

    setStyledText(textShape, formattedText, {
      size: CONFIG.FONTS.sizes.body - 1, 
      align: SlidesApp.ParagraphAlignment.LEFT,
      color: CONFIG.COLORS.text_primary,
      bold: true
    });

    try {
      textShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
    } catch (e) {}
  });
  }

  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}


function createFlowChartSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null;
  drawStandardTitleHeader(slide, layout, 'flowChartSlide', data.title, settings, titleWidthPt, imageUpdateOption);

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'flowChartSlide', data.subhead, subheadWidthPt); 
  
  
  const flows = Array.isArray(data.flows) ? data.flows : [{ steps: data.steps || [] }];
  
  
  let isDouble = flows.length > 1;
  let upperFlow, lowerFlow, maxStepsPerRow;
  
  if (!isDouble && flows[0] && flows[0].steps && flows[0].steps.length >= 5) {
    
    isDouble = true;
    const allSteps = flows[0].steps;
    const midPoint = Math.ceil(allSteps.length / 2);
    upperFlow = { steps: allSteps.slice(0, midPoint) };
    lowerFlow = { steps: allSteps.slice(midPoint) };
    maxStepsPerRow = midPoint; 
  } else {
    upperFlow = flows[0];
    lowerFlow = flows.length > 1 ? flows[1] : null; 
    maxStepsPerRow = Math.max(
      upperFlow?.steps?.length || 0, 
      lowerFlow?.steps?.length || 0
    );
  }
  
  if (isDouble) {
    
    const upperArea = offsetRect(layout.getRect('flowChartSlide.upperRow'), 0, dy);
    const lowerArea = offsetRect(layout.getRect('flowChartSlide.lowerRow'), 0, dy);
    drawFlowRow(slide, upperFlow, upperArea, settings, layout, maxStepsPerRow);
    if (lowerFlow && lowerFlow.steps && lowerFlow.steps.length > 0) { 
      drawFlowRow(slide, lowerFlow, lowerArea, settings, layout, maxStepsPerRow);
    }
  } else {
    
    const baseSingleArea = layout.getRect('flowChartSlide.singleRow'); 
    let singleArea = offsetRect(baseSingleArea, 0, dy); 

    
    const verticalShiftDownPt = layout.pxToPt(26.67); 
    singleArea.top += verticalShiftDownPt; 
    Logger.log(`FlowChart (Single Row): Shifted drawing area down by ${verticalShiftDownPt.toFixed(1)}pt.`);
    

    drawFlowRow(slide, flows[0], singleArea, settings, layout); 
  }
  
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}


function drawFlowRow(slide, flow, area, settings, layout, maxStepsPerRow = null) {
  
  if (!flow || !flow.steps || !Array.isArray(flow.steps)) {
    return; 
  }
  
  const steps = flow.steps.filter(step => step && String(step).trim()); 
  if (steps.length === 0) return;
  
  
  const actualSteps = maxStepsPerRow || steps.length;
  
  
  const baseArrowSpace = layout.pxToPt(25); 
  const arrowSpace = Math.max(baseArrowSpace, area.width * 0.04); 
  const totalArrowSpace = (actualSteps - 1) * arrowSpace; 
  const cardW = (area.width - totalArrowSpace) / actualSteps; 
  const cardH = area.height;
  
  
  const arrowHeight = Math.min(cardH * 0.3, layout.pxToPt(40)); 
  const arrowWidth = arrowSpace; 
  
  steps.forEach((step, index) => {
    const cardX = area.left + index * (cardW + arrowSpace);
    const stepText = String(step || '').trim() || 'ステップ'; 

    
    
    let __flowDone = false;
    if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
      const __flowSid = slide.getObjectId();
      const __flowQLen = __REST_REQUESTS.length;
      try {
        const cardRect = { left: cardX, top: area.top, width: cardW, height: cardH };
        restAddShape(__flowSid, cardRect, 'ROUND_RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), outlineRgb: hexToRgb01(CONFIG.COLORS.card_border), outlineWeight: 1 });
        _restStyledAutofitBox(slide, cardRect, stepText, { size: CONFIG.FONTS.sizes.body, align: SlidesApp.ParagraphAlignment.CENTER }, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
        if (index < steps.length - 1) {
          const arrowStartX = cardX + cardW;
          const arrowCenterY = area.top + cardH / 2;
          const arrowTop = arrowCenterY - (arrowHeight / 2);
          restAddShape(__flowSid, { left: arrowStartX, top: arrowTop, width: arrowWidth, height: arrowHeight }, 'RIGHT_ARROW', { fillRgb: hexToRgb01(settings.primaryColor), noOutline: true });
        }
        __flowDone = true;
      } catch (e) {
        __REST_REQUESTS.length = __flowQLen;
        __flowDone = false;
        Logger.log('[REST] flowRow holistic失敗→SlidesAppフォールバック: ' + e.message);
      }
    }

    if (!__flowDone) {
    
    const card = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, cardX, area.top, cardW, cardH);
    card.getFill().setSolidFill(CONFIG.COLORS.background_gray);
    card.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border);

    setStyledText(card, stepText, {
      size: CONFIG.FONTS.sizes.body,
      align: SlidesApp.ParagraphAlignment.CENTER
    });

    try {
      card.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
    } catch (e) {}

    
    if (index < steps.length - 1) {
      const arrowStartX = cardX + cardW; 
      const arrowCenterY = area.top + cardH / 2;
      const arrowTop = arrowCenterY - (arrowHeight / 2);

      
      const arrow = slide.insertShape(SlidesApp.ShapeType.RIGHT_ARROW, arrowStartX, arrowTop, arrowWidth, arrowHeight);
      arrow.getFill().setSolidFill(settings.primaryColor);
      arrow.getBorder().setTransparent();
    }
    }
  });
}


function createStepUpSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout);
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null; 
  drawStandardTitleHeader(slide, layout, 'stepUpSlide', data.title, settings, titleWidthPt, imageUpdateOption); 

  
  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'stepUpSlide', data.subhead, subheadWidthPt); 
  

  const area = offsetRect(layout.getRect('stepUpSlide.stepArea'), 0, dy);
  const items = Array.isArray(data.items) ? data.items : [];
  
  if (items.length === 0) {
    drawBottomBarAndFooter(slide, layout, pageNum, settings);
    return;
  }
  
  const numSteps = Math.min(5, items.length); 
  const gap = 0; 
  const headerHeight = layout.pxToPt(40);
  
  const maxHeight = area.height * 0.95; 
  
  
  let minHeightRatio;
  if (numSteps <= 2) {
    minHeightRatio = 0.70; 
  } else if (numSteps === 3) {
    minHeightRatio = 0.60; 
  } else {
    minHeightRatio = 0.50; 
  }
  const minHeight = maxHeight * minHeightRatio;
  
  
  const totalWidth = area.width;
  const cardW = totalWidth / numSteps;
  
  
  const stepUpColors = generateStepUpColors(settings.primaryColor, numSteps);
  
  for (let idx = 0; idx < numSteps; idx++) {
    const item = items[idx] || {};
    const titleText = String(item.title || `STEP ${idx + 1}`);
    const descText = String(item.desc || '');
    
    
    const heightRatio = (idx / Math.max(1, numSteps - 1)); 
    const cardH = minHeight + (maxHeight - minHeight) * heightRatio;
    
    const left = area.left + idx * cardW;
    const top = area.top + area.height - cardH; 

    
    
    
    let __stepDone = false;
    if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
      const __stepSid = slide.getObjectId();
      const __stepQLen = __REST_REQUESTS.length;
      try {
        restAddShape(__stepSid, { left: left, top: top + headerHeight, width: cardW, height: cardH - headerHeight }, 'RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), outlineRgb: hexToRgb01(CONFIG.COLORS.card_border), outlineWeight: 1 });
        restAddShape(__stepSid, { left: left, top: top, width: cardW, height: headerHeight }, 'RECTANGLE', { fillRgb: hexToRgb01(stepUpColors[idx]), outlineRgb: hexToRgb01(stepUpColors[idx]), outlineWeight: 1 });
        const __hdr = _restParenthesizedRanges(titleText, 10);
        _restStyledAutofitBox(slide, { left: left, top: top, width: cardW, height: headerHeight }, __hdr.text, { size: CONFIG.FONTS.sizes.body, bold: true, color: CONFIG.COLORS.background_gray, align: SlidesApp.ParagraphAlignment.CENTER }, SlidesApp.ContentAlignment.MIDDLE, null, { extraRanges: __hdr.ranges });
        _restStyledAutofitBox(slide, { left: left + layout.pxToPt(8), top: top + headerHeight, width: cardW - layout.pxToPt(16), height: cardH - headerHeight }, descText, { size: CONFIG.FONTS.sizes.body, align: SlidesApp.ParagraphAlignment.CENTER }, SlidesApp.ContentAlignment.MIDDLE, null, {});
        __stepDone = true;
      } catch (e) {
        __REST_REQUESTS.length = __stepQLen;
        __stepDone = false;
        Logger.log('[REST] stepUp holistic失敗→SlidesAppフォールバック: ' + e.message);
      }
    }

    if (!__stepDone) {
    
    const bodyShape = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, left, top + headerHeight, cardW, cardH - headerHeight);
    bodyShape.getFill().setSolidFill(CONFIG.COLORS.background_gray);
    bodyShape.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border);

    
    const headerShape = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, left, top, cardW, headerHeight);
    headerShape.getFill().setSolidFill(stepUpColors[idx]); 
    headerShape.getBorder().getLineFill().setSolidFill(stepUpColors[idx]);
    
    
    const headerTextShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, left, top, cardW, headerHeight);
    setStyledText(headerTextShape, titleText, {
      size: CONFIG.FONTS.sizes.body,
      bold: true,
      color: CONFIG.COLORS.background_gray,
      align: SlidesApp.ParagraphAlignment.CENTER
    });
    
    try {
      setParenthesizedTextSize(headerTextShape, 10); 
    } catch(e) {
      Logger.log(`Error applying setParenthesizedTextSize in stepUp slide header: ${e.message}`);
    }

    try {
      headerTextShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
      headerTextShape.setAutofit(SlidesApp.AutofitType.SHRINK_ON_OVERFLOW);
    } catch (e) {}
    
    
    const bodyTextShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 
      left + layout.pxToPt(8), top + headerHeight, 
      cardW - layout.pxToPt(16), cardH - headerHeight);
    setStyledText(bodyTextShape, descText, {
      size: CONFIG.FONTS.sizes.body,
      align: SlidesApp.ParagraphAlignment.CENTER
    });

    try {
      bodyTextShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
      bodyTextShape.setAutofit(SlidesApp.AutofitType.SHRINK_ON_OVERFLOW);
    } catch (e) {}
    }
  }

  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}


function createImageTextSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  setMainSlideBackground(slide, layout, imageUpdateOption);
  
  
  const titleWidthPt = (data && typeof data._title_widthPt === 'number') ? data._title_widthPt : null;
  drawStandardTitleHeader(slide, layout, 'imageTextSlide', data.title, settings, titleWidthPt, imageUpdateOption);
  

  
  const subheadWidthPt = (data && typeof data._subhead_widthPt === 'number') ?
  data._subhead_widthPt : null;
  
  const dy = drawSubheadIfAny(slide, layout, 'imageTextSlide', data.subhead, subheadWidthPt);
  
  
  let imageUrl = '';
  let imageInfo = null; 
  const imageData = data.image;
  
  if (typeof imageData === 'string') {
    
    imageUrl = imageData;
  } else if (imageData && typeof imageData === 'object') {
    
    imageUrl = imageData.data || ''; 
    imageInfo = imageData.info || null; 
  }
  

  const imageCaption = data.imageCaption || '';
  const points = Array.isArray(data.points) ? data.points : [];
  const imagePosition = data.imagePosition === 'right' ? 'right' : 'left';
  
  
  const __chartIsNative = (__NATIVE_CHART_ACTIVE && imageData && typeof imageData === 'object' && imageData._nativeComposited === true);

  if (imagePosition === 'left') {
    
    
    const textArea = offsetRect(layout.getRect('imageTextSlide.rightText'), 0, dy); 

    if (imageUrl && !__chartIsNative) {
      
      
      
      let newMaxWidthPt;
      let newMaxHeightPt;

      if (imageInfo === 'chart') {
        
        newMaxWidthPt = layout.pxToPt(400); 
        newMaxHeightPt = layout.pxToPt(400); 
      } else {
        
        newMaxWidthPt = layout.pxToPt(400); 
        newMaxHeightPt = layout.pxToPt(300); 
      }
      
      renderImageMirrored(
        slide, 
        layout, 
        textArea, 
        imageUrl, 
        newMaxWidthPt, 
        newMaxHeightPt, 
        imageUpdateOption
      );
      
    }
    
    if (points.length > 0) { 
      createContentCushion(slide, textArea, settings, layout);
      const padding = layout.pxToPt(20);
      const textRect = { 
        left: textArea.left + padding,
        top: textArea.top + padding,
        width: textArea.width - (padding * 2),
        height: textArea.height - (padding * 2)
      };
      const textShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, textRect.left, textRect.top, textRect.width, textRect.height);  
      setBulletsWithInlineStyles(textShape, points);
      try {
        setBoldTextSize(textShape, 16);
      } catch(e) {
        Logger.log(`Error applying setBoldTextSize in imageText (left image): ${e.message}`);
      }
      
      try {
        adjustShapeText_External(textShape);
      } catch(e) {
        Logger.log(`Error adjusting text in createImageTextSlide (left image): ${e.message}`);
      }
      try {
        setParenthesizedTextSize(textShape, 10);
      } catch(e) {
        Logger.log(`Error applying setParenthesizedTextSize in imageText (left image): ${e.message}`);
      }
      try {
        textShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
      } catch(e) {
        Logger.log(`Error setting ContentAlignment in imageText (left image): ${e.message}`);
      }
    }
  } else {
    
    const textArea = offsetRect(layout.getRect('imageTextSlide.leftText'), 0, dy);
    const imageArea = offsetRect(layout.getRect('imageTextSlide.rightImage'), 0, dy);
    
    if (points.length > 0) { 
      createContentCushion(slide, textArea, settings, layout);
      const padding = layout.pxToPt(20);
      const textRect = { 
        left: textArea.left + padding,
        top: textArea.top + padding,
        width: textArea.width - (padding * 2),
        height: textArea.height - (padding * 2)
      };
      const textShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, textRect.left, textRect.top, textRect.width, textRect.height); 
      setBulletsWithInlineStyles(textShape, points); 
      try {
      setBoldTextSize(textShape, 16);
      } catch(e) {
        Logger.log(`Error applying setBoldTextSize in imageText (right image): ${e.message}`);
      }
      
      try {
        adjustShapeText_External(textShape);
      } catch(e) {
        Logger.log(`Error adjusting text in createImageTextSlide (right image): ${e.message}`);
      }
      try {
      setParenthesizedTextSize(textShape, 10);
      } catch(e) {
        Logger.log(`Error applying setParenthesizedTextSize in imageText (right image): ${e.message}`);
      }
      try {
      textShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
      } catch(e) {
        Logger.log(`Error setting ContentAlignment (POST-ADJUST) in imageText (right image): ${e.message}`);
      }
    }
    
    if (imageUrl && !__chartIsNative) {
      

      
      let newMaxWidthPt;
      let newMaxHeightPt;

      if (imageInfo === 'chart') {
        
        newMaxWidthPt = layout.pxToPt(400); 
        newMaxHeightPt = layout.pxToPt(400); 
      } else {
        
        newMaxWidthPt = layout.pxToPt(400); 
        newMaxHeightPt = layout.pxToPt(300); 
      }

      
      renderImageMirrored(
        slide, 
        layout, 
        textArea, 
        imageUrl, 
        newMaxWidthPt, 
        newMaxHeightPt, 
        imageUpdateOption
      );
    }
  }
  
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}




function createFullImageSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  
  let imageUrl = '';
  if (typeof data.image === 'string') {
    imageUrl = data.image;
  } else if (data.image && data.image.data) {
    imageUrl = data.image.data;
  }

  
  if (imageUpdateOption === 'update' && imageUrl) {
    try {
      const imageBlob = insertImageFromUrlOrFileId(imageUrl);
      if (imageBlob) {
        const img = slide.insertImage(imageBlob);

        
        
        const pageWidthPt = layout.pageW_pt;
        
        const pageHeightPt = layout.pageH_pt; 

        
        img.setLeft(0)
           .setTop(0)
           .setWidth(pageWidthPt)
           .setHeight(pageHeightPt); 
      }
    } catch (e) {
      Logger.log(`FullImage creation failed: ${e.message}`);
    }
  }

  
  
}



function createIntegratedContentSlide(slide, data, layout, pageNum, settings, imageUpdateOption) {
  
  var __bgImg = null, __img = null;
  if (data.backgroundImage && imageUpdateOption === 'update') {
    __bgImg = renderFullSizeImage(slide, layout, data.backgroundImage);
  }
  if (data.image && imageUpdateOption === 'update') {
    __img = renderFullSizeImage(slide, layout, data.image);
  }
  
  
  if (__img) { try { __img.sendToBack(); } catch (e) {} }
  if (__bgImg) { try { __bgImg.sendToBack(); } catch (e) {} }

  
  
  const displayTitle = (pageNum === 1) ? "" : data.title;
  
  
  drawStandardTitleHeader(slide, layout, 'contentSlide', displayTitle, settings, null, imageUpdateOption);

  
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}




function createIntegratedTitleSlide(slide, data, layout, pageNum, settings, imageUpdateOption) {
  
  if (data.backgroundImage && imageUpdateOption === 'update') {
    var __bgImg = renderFullSizeImage(slide, layout, data.backgroundImage);
    if (__bgImg) { try { __bgImg.sendToBack(); } catch (e) {} }
  }

  
  const titleRect = layout.getRect('titleSlide.title');
  
  const titleShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, titleRect.left, titleRect.top, titleRect.width, titleRect.height);
  setStyledText(titleShape, data.title, { size: CONFIG.FONTS.sizes.title, bold: true, fontType: 'large' });
  titleShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);

  
  if (data.date) {
    const dateRect = layout.getRect('titleSlide.date');
    const dateShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, dateRect.left, dateRect.top, dateRect.width, dateRect.height);
    dateShape.getText().setText(data.date);
    applyTextStyle(dateShape.getText(), { size: CONFIG.FONTS.sizes.date, fontType: 'large' });
  }

  
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}


function renderFullSizeImage(slide, layout, imageUrl) {
  try {
    const imageBlob = insertImageFromUrlOrFileId(imageUrl);
    if (imageBlob) {
      const img = slide.insertImage(imageBlob);
      img.setLeft(0).setTop(0)
         .setWidth(layout.pageW_pt)
         .setHeight(layout.pageH_pt);
      
      return img;
    }
  } catch (e) {
    Logger.log(`Integrated Image Error: ${e.message}`);
  }
  return null;
}



function createSplitImageSlide(slide, data, layout, pageNum, settings, imageUpdateOption = 'update') {
  
  const imgInfo = data.image;
  if (!imgInfo || imageUpdateOption !== 'update') {
      const displayTitle = (pageNum === 1) ? "" : data.title;
      drawStandardTitleHeader(slide, layout, 'contentSlide', displayTitle, settings, null, imageUpdateOption);
      drawBottomBarAndFooter(slide, layout, pageNum, settings);
      return;
  }

  
  
  
  
  var __splitBg = null;
  if (imgInfo.backgroundImage) {
    __splitBg = renderFullSizeImage(slide, layout, imgInfo.backgroundImage);
  }
  if (__splitBg) { try { __splitBg.sendToBack(); } catch (e) {} }

  
  if (imgInfo.layers && Array.isArray(imgInfo.layers)) {
    const scaleX = layout.pageW_pt / imgInfo.originalWidth;
    const scaleY = layout.pageH_pt / imgInfo.originalHeight;

    
    
    
    const __zArea = (l) => ((l && (l.w || l.width) || 0) * (l && (l.h || l.height) || 0));
    const __orderedLayers = imgInfo.layers.slice().sort((a, b) => {
      const at = !!(a && a.type === 'text'), bt = !!(b && b.type === 'text');
      if (at !== bt) return at ? 1 : -1;   
      if (at && bt) return 0;              
      return __zArea(b) - __zArea(a);      
    });

    __orderedLayers.forEach(layer => {
        try {
            const targetLeft = layer.x * scaleX;
            const targetTop = layer.y * scaleY;
            const rawW = layer.w || layer.width;
            const rawH = layer.h || layer.height;
            const targetWidth = rawW * scaleX;
            const targetHeight = rawH * scaleY;

            
            if (layer.type === 'text') {
                const shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, targetLeft, targetTop, targetWidth, targetHeight);
                
                shape.getFill().setTransparent();
                shape.getBorder().setTransparent();

                const textRange = shape.getText();
                textRange.setText(layer.text || "");

                const style = textRange.getTextStyle();
                
                if (layer.fontSize) {
                    style.setFontSize(layer.fontSize * scaleY);
                }
                if (layer.color) {
                    try { style.setForegroundColor(layer.color); } catch(e){}
                }
                if (layer.fontWeight === 'bold') {
                    style.setBold(true);
                }

                
                shape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
                
                textRange.getParagraphStyle().setParagraphAlignment(SlidesApp.ParagraphAlignment.START);
                

            } 
            
            else {
                const imageBlob = insertImageFromUrlOrFileId(layer.key);
                if (imageBlob) {
                    const img = slide.insertImage(imageBlob);
                    
                    img.setLeft(targetLeft)
                       .setTop(targetTop)
                       .setWidth(targetWidth)
                       .setHeight(targetHeight);
                }
            }
        } catch (e) {
            Logger.log("Error inserting split layer: " + e.message);
        }
    });
  }

  if (data._suppressHeaderFooter) {
      return;
  }

  const displayTitle = (pageNum === 1) ? "" : data.title;
  drawStandardTitleHeader(slide, layout, 'contentSlide', displayTitle, settings, null, imageUpdateOption);
  drawBottomBarAndFooter(slide, layout, pageNum, settings);
}



function insertSlideAt(presentationId, index, singleSlideDataString, settings) {
  if (!presentationId || index == null || !singleSlideDataString) {
    return { status: 'error', message: 'パラメータが不足しています。' };
  }

  try {
    
    
    resetRunState_();

    
    const presentation = SlidesApp.openById(presentationId);

    
    const data = JSON.parse(singleSlideDataString);

    
    
    
    
    
    
    
    
    
    
    let slide = null;
    var __b2HasTemplate = !!(settings && settings.templateUrl && String(settings.templateUrl).trim());
    var __b2ExistingSlides = presentation.getSlides();
    if (__b2HasTemplate && __b2ExistingSlides.length > 0) {
      try {
        var __b2Rec = null;
        try { __b2Rec = _loadNativeDecoRecord(presentationId); } catch (eB2Load) { __b2Rec = null; }
        var __b2RecOk = !!(__b2Rec && Array.isArray(__b2Rec.d)
            && __b2Rec.d.length === __b2ExistingSlides.length
            && typeof __b2Rec.dm === 'boolean');
        var __b2Category = getTemplateCategoryForType(data.type);
        var __b2State = loadLatestGenerationState(presentationId);
        var __b2SavedData = (__b2State && __b2State.status === 'success' && __b2State.data && Array.isArray(__b2State.data.slideData))
          ? __b2State.data.slideData : null;
        if (__b2RecOk && __b2SavedData) {
          
          var __b2SrcIndex = -1;
          for (var __b2i = 0; __b2i < __b2SavedData.length && __b2i < __b2ExistingSlides.length; __b2i++) {
            var __b2sd = __b2SavedData[__b2i];
            if (__b2sd && __b2sd.type && getTemplateCategoryForType(__b2sd.type) === __b2Category) { __b2SrcIndex = __b2i; break; }
          }
          if (__b2SrcIndex >= 0) {
            
            var __b2KeepIds = {};
            (__b2Rec.d[__b2SrcIndex] || []).forEach(function (oid) { if (oid) __b2KeepIds[String(oid)] = true; });
            var __b2SrcEls = __b2ExistingSlides[__b2SrcIndex].getPageElements();
            var __b2KeepPos = {};
            for (var __b2p = 0; __b2p < __b2SrcEls.length; __b2p++) {
              try { if (__b2KeepIds[String(__b2SrcEls[__b2p].getObjectId())]) __b2KeepPos[__b2p] = true; } catch (ePos) {}
            }
            
            var __b2Dup = __b2ExistingSlides[__b2SrcIndex].duplicate();
            __b2Dup.move(index);
            var __b2DupEls = __b2Dup.getPageElements();
            for (var __b2q = __b2DupEls.length - 1; __b2q >= 0; __b2q--) {
              if (__b2KeepPos[__b2q]) continue; 
              try { __b2DupEls[__b2q].remove(); } catch (eRm) {}
            }
            slide = __b2Dup;
            
            __NATIVE_SECTION_ACTIVE = true;
            __NATIVE_DECOR_ACTIVE = __b2Rec.dm === true;
            __NATIVE_CHART_ACTIVE = false;
            __CURRENT_SLIDE_USES_TEMPLATE = true; 
            Logger.log(`[B2] 同一デッキ内の同カテゴリ既存スライド(index=${__b2SrcIndex}, category=${__b2Category})を複製し、装飾を維持して挿入土台にしました。`);
          }
        }
      } catch (eB2) {
        slide = null;
        
        
        try { if (typeof __b2Dup !== 'undefined' && __b2Dup) __b2Dup.remove(); } catch (eRmDup) {}
        __NATIVE_SECTION_ACTIVE = false;
        __NATIVE_DECOR_ACTIVE = false;
        __NATIVE_CHART_ACTIVE = false;
        __CURRENT_SLIDE_USES_TEMPLATE = false;
        Logger.log(`[B2] 同一デッキ複製に失敗→従来経路にフォールバック: ${(eB2 && eB2.message) || eB2}`);
      }
    }

    
    if (!slide) {
      
      loadTemplateSlides(settings);
      
      
      slide = createBaseSlide(presentation, data.type, index);
    }

    
    updateDynamicColors(settings);
    CONFIG.COLORS.primary_color = settings.primaryColor || CONFIG.COLORS.primary_color;
    CONFIG.COLORS.text_primary = settings.largeFontColor || '#333333';
    CONFIG.COLORS.text_small_font = settings.smallFontColor || '#1F2937';
    if (settings.backgroundColor && settings.backgroundColor.toUpperCase() !== '#FFFFFF') {
      CONFIG.COLORS.background_white = settings.backgroundColor;
    } else {
      CONFIG.COLORS.background_white = '#FFFFFF';
    }
    CONFIG.FOOTER_TEXT = settings.footerText;
    CONFIG.FONTS.family = settings.fontFamily || CONFIG.FONTS.family;
    CONFIG.LOGOS.header = settings.headerLogoUrl;
    CONFIG.LOGOS.closing = settings.closingLogoUrl;
    CONFIG.BACKGROUND_IMAGES.title = settings.titleBgUrl;
    CONFIG.BACKGROUND_IMAGES.closing = settings.closingBgUrl;
    CONFIG.BACKGROUND_IMAGES.section = settings.sectionBgUrl;
    CONFIG.BACKGROUND_IMAGES.main = settings.mainBgUrl;

    
    __CREDIT_IMAGE_BLOB = null;
    if (settings.creditImageBase64) {
        try {
            const parts = settings.creditImageBase64.split(',');
            const mimeType = parts[0].match(/:(.*?);/)[1];
            const decodedData = Utilities.base64Decode(parts[1]);
            __CREDIT_IMAGE_BLOB = Utilities.newBlob(decodedData, mimeType, 'credit.png');
        } catch(e) {}
    }

    const layout = createLayoutManager(presentation.getPageWidth(), presentation.getPageHeight(), getContentMarginsPx(settings));
    restReset(presentation.getId()); 

    

    
    const generator = slideGenerators[data.type];
    if (generator) {
      
      generator(slide, data, layout, index + 1, settings, 'update');
    }
    
    try {
      restFlush(); 
    } catch (e) {
      Logger.log(`[REST] flush失敗(挿入)。挿入スライドを削除して中止: ${e && e.message}`);
      try { slide.remove(); } catch (eRm) {}
      try { restReset(presentation.getId()); } catch (eRs) {}
      return { status: 'error', message: 'ページ挿入に失敗しました。もう一度お試しください: ' + (e && e.message) };
    }

    
    if (data.notes) {
        const cleanedNotes = cleanSpeakerNotes(data.notes);
        slide.getNotesPage().getSpeakerNotesShape().getText().setText(cleanedNotes);
    }

    
    
    try { PropertiesService.getScriptProperties().deleteProperty(_nativeDecoKey(presentationId)); } catch (eDecoDel) {}

    addToUserHistory(presentationId);

    return { status: 'success', message: 'スライドを追加しました。' };

  } catch (e) {
    Logger.log(`insertSlideAt Error: ${e.message}`);
    return { status: 'error', message: e.message };
  }
}




function renderSingleImageInArea(slide, layout, area, imageUrl, caption = '', position = 'left', imageInfo = null, imageUpdateOption = 'update') {
  if (!imageUrl) return;
  
  if (imageUpdateOption !== 'update') {
    Logger.log('[renderSingleImageInArea] Skipping image insertion due to imageUpdateOption.');
    return null; 
  }
  
  try {
    const imageData = insertImageFromUrlOrFileId(imageUrl);
    if (!imageData) return null;
    
    const img = slide.insertImage(imageData);
    
    const __iw = img.getWidth(), __ih = img.getHeight();



    
    if (imageInfo === 'chart') {
      
      const imgAspect = __iw / __ih;
      const baseTop = area.top;
      const baseCenterX = area.left + area.width / 2;
      const newBottom = layout.contentBottom_pt - layout.pxToPt(5);
      const newH = newBottom - baseTop;
      const newW = newH * imgAspect;
      const newLeft = baseCenterX - (newW / 2);
      img.setWidth(newW).setHeight(newH)
         .setLeft(newLeft)
         .setTop(baseTop);

    } else if (imageInfo === 'svg_code') {
      
      
      
      const scaleFactor = 0.85;
      const scale = Math.min(area.width / __iw, area.height / __ih) * scaleFactor;

      const w = __iw * scale;
      const h = __ih * scale;

      
      img.setWidth(w).setHeight(h)
         .setLeft(area.left + (area.width - w) / 2)
         .setTop(area.top + (area.height - h) / 2);

    } else {
      
      const scale = Math.min(area.width / __iw, area.height / __ih);
      const w = __iw * scale;
      const h = __ih * scale;
      img.setWidth(w).setHeight(h)
         .setLeft(area.left + (area.width - w) / 2)
         .setTop(area.top + (area.height - h) / 2);
    }
    




    
    if (caption && caption.trim()) { 
      const imgHeight = img.getHeight();
      const imgTop = img.getTop();
      const imageBottom = imgTop + imgHeight;

      const captionMargin = layout.pxToPt(8);
      const captionHeight = layout.pxToPt(30);
      
      let captionRect;

      if (imageInfo === 'chart') {
        
        let captionTop = imageBottom + captionMargin;
        if ((captionTop + captionHeight) > layout.contentBottom_pt) {
          captionTop = layout.contentBottom_pt - captionHeight - layout.pxToPt(2);
        }
        captionRect = { left: img.getLeft(), top: captionTop, width: img.getWidth(), height: captionHeight };
      
      } else {
        
        const rectPath = `imageTextSlide.${position}ImageCaption`;
        
        captionRect = layout.getRect(rectPath);
      }
      

      const captionShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 
        captionRect.left, captionRect.top, captionRect.width, captionRect.height);
      
      captionShape.getFill().setTransparent();
      captionShape.getBorder().setTransparent();
      
      setStyledText(captionShape, caption.trim(), {
        size: CONFIG.FONTS.sizes.small,
        color: CONFIG.COLORS.neutral_gray,
        align: SlidesApp.ParagraphAlignment.CENTER
      });
      try {
        captionShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
      } catch (e) {}
    }
       
    return img;
  } catch (e) {
    Logger.log(`Image insertion failed: ${e.message}. URL: ${imageUrl}`);
    return null;
  }
}


function renderImageMirrored(slide, layout, textAreaRect, imageUrl, maxWidthPt, maxHeightPt, imageUpdateOption) {
  if (imageUpdateOption !== 'update') {
    Logger.log('[renderImageMirrored] Skipping image insertion due to imageUpdateOption.');
    return null; 
  }
  
  if (!imageUrl) {
    Logger.log('[renderImageMirrored] No image URL provided.');
    return null;
  }
  
  if (!textAreaRect || !maxWidthPt || !maxHeightPt) {
    Logger.log('[renderImageMirrored] Invalid arguments (textAreaRect, maxWidthPt, or maxHeightPt missing).');
    return null;
  }

  try {
    
    const pageCenterX = layout.contentCenterX_pt;

    
    const textCenterX = textAreaRect.left + textAreaRect.width / 2;
    const textCenterY = textAreaRect.top + textAreaRect.height / 2;
    
    
    const mirroredCenterX = pageCenterX + (pageCenterX - textCenterX);
    const mirroredCenterY = textCenterY; 

    
    const imageData = insertImageFromUrlOrFileId(imageUrl);
    if (!imageData) return null;
    const img = slide.insertImage(imageData);
    
    const __iw = img.getWidth(), __ih = img.getHeight();

    
    const scale = Math.min(maxWidthPt / __iw, maxHeightPt / __ih);
    const w = __iw * scale;
    const h = __ih * scale;

    
    const finalLeft = mirroredCenterX - (w / 2);
    const finalTop = mirroredCenterY - (h / 2);
    
    
    img.setWidth(w).setHeight(h)
       .setLeft(finalLeft)
       .setTop(finalTop);
       
    Logger.log(`[renderImageMirrored] Image placed. Mirrored Center: (${mirroredCenterX.toFixed(1)}, ${mirroredCenterY.toFixed(1)}). Final Size: (${w.toFixed(1)}x${h.toFixed(1)})`);

    return img;
    
  } catch (e) {
    Logger.log(`[renderImageMirrored] Image insertion failed: ${e.message}. URL: ${imageUrl}`);
    return null;
  }
}






function estimateTextWidthPt(text, fontSizePt) {
  const multipliers = {
    space: 0.3,    
    ascii: 0.62,   
    uppercase: 0.8, 
    japanese: 1.0, 
    other: 0.85    
  };
  const totalMultiplier = String(text || '').split('').reduce((acc, char) => {

    if (char === ' ') {
      return acc + multipliers.space; 
    }
    else if (char === 'I' || char === 'i') {
      return acc + multipliers.space; 
    }
    
    else if (char.match(/[A-HJ-Z]/)) { 
      return acc + multipliers.uppercase; 
    }
    
    else if (char.match(/[ -~]/)) { 
      return acc + multipliers.ascii;    
    }
    
    else if (char.match(/[\u3000-\u303F\u3040-\u309F\u30A0-\uFF00-\uFFEF]/)) {
      return acc + multipliers.japanese; 
    }
     
    else if (char.match(/[\u4E00-\u9FAF]/)) {
        return acc + multipliers.japanese; 
    }
    else {
      return acc + multipliers.other; 
    }
  }, 0);
  const calculatedWidth = totalMultiplier * fontSizePt;
  return calculatedWidth + 10; 
}

function offsetRect(rect, dx, dy) {
  return {
    left: rect.left + (dx || 0),
    top: rect.top + (dy || 0),
    width: rect.width,
    height: rect.height
  };
}


function drawStandardTitleHeader(slide, layout, key, title, settings, preCalculatedWidthPt = null, imageUpdateOption = 'update') {
  
  
  if (imageUpdateOption === 'update') {
    const logoRect = safeGetRect(layout, `${key}.headerLogo`);
    const __t_logo = Date.now();
    try {
      
      
      const useRestLogo = !!(__HEADER_LOGO_SRC && __REST_PRESENTATION_ID && __SLIDES_API_OK);
      if (CONFIG.LOGOS.header && logoRect && useRestLogo) {
        restAddImage(slide.getObjectId(),
          { left: logoRect.left, top: logoRect.top,
            width: logoRect.width, height: logoRect.width * __HEADER_LOGO_SRC.aspect },
          __HEADER_LOGO_SRC.url);
      } else if (CONFIG.LOGOS.header && logoRect) {
        
        if (__CACHED_HEADER_LOGO_GROUP) {
          try {
            const copiedGroup = slide.insertGroup(__CACHED_HEADER_LOGO_GROUP);
            copiedGroup.setLeft(logoRect.left).setTop(logoRect.top);
          } catch(e) {
            __CACHED_HEADER_LOGO_GROUP = null;
          }
        }
        
        if (!__CACHED_HEADER_LOGO_GROUP) {
          const imageData = insertImageFromUrlOrFileId(CONFIG.LOGOS.header);
          if (imageData) {
            const logo = slide.insertImage(imageData);
            const asp = logo.getHeight() / logo.getWidth();
            logo.setWidth(logoRect.width).setHeight(logoRect.width * asp);
            logo.setLeft(logoRect.left).setTop(logoRect.top);
            const group = slide.group([logo]);
            __CACHED_HEADER_LOGO_GROUP = group;
          }
        }
      }
    } catch (e) {
      Logger.log(`Header logo error: ${e && e.message ? e.message : e}`);
    }
    if (CONFIG.LOGOS.header && logoRect) perfAdd('hdr:logo', Date.now() - __t_logo);
  }

  
  const initialFontSize = CONFIG.FONTS.sizes.contentTitle; 
  let finalFontSize = initialFontSize; 
  

  
  if (title && String(title).trim() !== '') {
      const titleRect = safeGetRect(layout, `${key}.title`);
      if (titleRect) {
          
          const optimalHeight = layout.pxToPt(initialFontSize + 8);
          const cmToPt = 28.3465;
          const verticalShiftPt = 0.3 * cmToPt;
          const adjustedTop = titleRect.top + verticalShiftPt;

          
          
          let titleDone = false;
          if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
            try {
              const titleRectRest = { left: titleRect.left, top: adjustedTop, width: titleRect.width, height: optimalHeight };
              finalFontSize = perfTime('hdr:title', () => _restStyledAutofitBox(slide, titleRectRest, title,
                { size: initialFontSize, bold: true, fontType: 'large' },
                SlidesApp.ContentAlignment.MIDDLE, preCalculatedWidthPt, {}));
              titleDone = true;
            } catch (e) {
              Logger.log(`[REST] title失敗→SlidesAppフォールバック (Key: ${key}): ${e.message}`);
            }
          }
          if (!titleDone) {
          
          const titleShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX,
            titleRect.left,
            adjustedTop,
            titleRect.width,
            optimalHeight
          );

          
          setStyledText(titleShape, title, {
            size: initialFontSize,
            bold: true,
            fontType: 'large'
          });
          try {
            titleShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
          } catch (e) {}

          
          try {
            const adjustmentResult = adjustShapeText_External(titleShape, preCalculatedWidthPt);
            Logger.log(`Standard Header Title Adjustment (Key: ${key}): ${adjustmentResult.log}`);
            if (adjustmentResult.finalSize !== null) {
              finalFontSize = adjustmentResult.finalSize;
            }
          } catch (e) {
            Logger.log(`Error during standard header title adjustment (Key: ${key}): ${e.message}`);
          }
          } 
      } else {
          Logger.log(`[rect-missing] key=${key}.title`);
      }
  }

  
  if (settings.showTitleUnderline && title && String(title).trim() !== '') {
    const uRect = safeGetRect(layout, `${key}.titleUnderline`);
    if (!uRect) {
      Logger.log(`[rect-missing] key=${key}.titleUnderline`);
      return;
    }

    let underlineWidthPt;

    
    if (finalFontSize < initialFontSize && finalFontSize >= 10 && preCalculatedWidthPt !== null && preCalculatedWidthPt > 0) {
        underlineWidthPt = preCalculatedWidthPt * (finalFontSize / initialFontSize);
    }
    else if (preCalculatedWidthPt !== null && preCalculatedWidthPt > 0) {
      underlineWidthPt = preCalculatedWidthPt;
    }
    else {
      underlineWidthPt = estimateTextWidthPt(title, initialFontSize);
    }
    
    const maxUnderlineWidth = layout.contentRight_pt - uRect.left - layout.pxToPt(25);
    const finalWidth = Math.min(underlineWidthPt + 10, maxUnderlineWidth);

    createPillShapeUnderline(slide, uRect.left, uRect.top, finalWidth, uRect.height, settings);
  }
} 






function estimateTextHeightPt(text, widthPt, fontSizePt, lineHeight) {
  var paragraphs = String(text).split(/\r?\n/);
  
  var charsPerLine = Math.max(1, Math.floor(widthPt / (fontSizePt * 0.95)));
  var lines = 0;
  for (var i = 0; i < paragraphs.length; i++) {
    var s = paragraphs[i].replace(/\s+/g, ' ').trim();
    
    var len = s.length || 1;
    lines += Math.ceil(len / charsPerLine);
  }
  var lineH = fontSizePt * (lineHeight || 1.2);
  return Math.max(lineH, lines * lineH);
}


function drawSubheadIfAny(slide, layout, key, subhead, preCalculatedWidthPt = null) {
  if (!subhead) return 0;

  subhead = String(subhead).replace(/[\r\n]+/g, '');

  const rect = safeGetRect(layout, `${key}.subhead`);
  if (!rect) {
    Logger.log(`[drawSubheadIfAny] Could not get rect for ${key}.subhead`);
    return 0;
  }
  
  
  let subheadDone = false;
  if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
    try {
      _restStyledAutofitBox(slide, rect, subhead,
        { size: CONFIG.FONTS.sizes.subhead, fontType: 'large' },
        null, preCalculatedWidthPt, {});
      subheadDone = true;
    } catch (e) {
      Logger.log(`[REST] subhead失敗→SlidesAppフォールバック (Key: ${key}): ${e.message}`);
    }
  }
  if (!subheadDone) {
  const box = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, rect.left, rect.top, rect.width, rect.height);
  setStyledText(box, subhead, {
    size: CONFIG.FONTS.sizes.subhead,
    
    fontType: 'large'
  });

  try {
    
    const adjustmentResult = adjustShapeText_External(box, preCalculatedWidthPt);
    Logger.log(`Subhead Adjustment (Key: ${key}): ${adjustmentResult.log}`);
  } catch (e) {
    Logger.log(`Error during subhead adjustment (Key: ${key}): ${e.message}`);
  }
  } 

  return layout.pxToPt(36); 
}


function drawBottomBar(slide, layout, settings) {
  
  if (__NATIVE_DECOR_ACTIVE) return;
  const barRect = layout.getRect('bottomBar');

  
  if (settings.enableGradient) {
    if (__CACHED_GRADIENT_BOTTOM_BAR) {
      try {
        
        const copiedGroup = slide.insertGroup(__CACHED_GRADIENT_BOTTOM_BAR);
        copiedGroup.setLeft(barRect.left).setTop(barRect.top);
        return; 
      } catch (e) {
        
        __CACHED_GRADIENT_BOTTOM_BAR = null;
      }
    }
    
    
    
    const group = createGradientRectangle(slide, barRect.left, barRect.top, barRect.width, barRect.height, [settings.gradientStart, settings.gradientEnd]);
    if (group) {
      __CACHED_GRADIENT_BOTTOM_BAR = group; 
    }
  } else {
    
    applyFill(slide, barRect.left, barRect.top, barRect.width, barRect.height, settings);
  }
}

function addCucFooter(slide, layout, pageNum, settings) {
  
  if (CONFIG.FOOTER_TEXT && CONFIG.FOOTER_TEXT.trim() !== '') {
    const __ft = Date.now();
    const leftRect = layout.getRect('footer.leftText');
    
    _insertTextBox(slide, leftRect, CONFIG.FOOTER_TEXT, {
      size: CONFIG.FONTS.sizes.footer,
      
      fontType: 'large'
    }, SlidesApp.ContentAlignment.MIDDLE);
    perfAdd('foot:footertext', Date.now() - __ft);
  } 

  if (__CREDIT_IMAGE_BLOB || __CREDIT_TEXT) {
    perfTime('foot:credit', () => drawCreditImage(slide, layout, __CREDIT_IMAGE_BLOB, CREDIT_IMAGE_LINK));
  }

  if (pageNum > 0 && settings && settings.showPageNumber) {
    const __pn = Date.now();
    const rightRect = layout.getRect('footer.rightPage');
    
    _insertTextBox(slide, rightRect, String(pageNum), {
      size: CONFIG.FONTS.sizes.footer,
      color: CONFIG.COLORS.primary_color,
      align: SlidesApp.ParagraphAlignment.END
    }, SlidesApp.ContentAlignment.MIDDLE);
    perfAdd('foot:pagenum', Date.now() - __pn);
  }
}

function drawBottomBarAndFooter(slide, layout, pageNum, settings) {
  if (settings.showBottomBar) {
    perfTime('bar:bottom', () => drawBottomBar(slide, layout, settings));
  }
  addCucFooter(slide, layout, pageNum, settings);
}





function applyTextStyle(textRange, opt) {
  const style = textRange.getTextStyle();

  
  let defaultColor;
  if (opt.fontType === 'large') {
    defaultColor = CONFIG.COLORS.text_primary; 
  } else {
    
    defaultColor = CONFIG.COLORS.text_small_font; 
  }
  

  style.setFontFamily(CONFIG.FONTS.family)
       .setForegroundColor(opt.color || defaultColor) 
       .setFontSize(opt.size || CONFIG.FONTS.sizes.body)
       .setBold(opt.bold || false);

  if (opt.align) {
    try {
      textRange.getParagraphs().forEach(p => {
        p.getRange().getParagraphStyle().setParagraphAlignment(opt.align);
      });
    } catch (e) {}
  }
}

function setStyledText(shapeOrCell, rawText, baseOpt) {
  const parsed = parseInlineStyles(rawText || '');
  const tr = shapeOrCell.getText().setText(parsed.output);
  applyTextStyle(tr, baseOpt || {});
  applyStyleRanges(tr, parsed.ranges);
}

function setBulletsWithInlineStyles(shape, points) {
  const joiner = '\n\n';
  let combined = '';
  const ranges = [];
  (points || []).forEach((pt, idx) => {
    const parsed = parseInlineStyles(String(pt || ''));
    
    const bullet = parsed.output;  
    if (idx > 0) combined += joiner;
    const start = combined.length;
    combined += bullet;
    parsed.ranges.forEach(r => ranges.push({
      start: start + r.start,  
      end: start + r.end,
      bold: r.bold,
      color: r.color
    }));
  });
  const tr = shape.getText().setText(combined || '—');
  applyTextStyle(tr, {
    size: CONFIG.FONTS.sizes.body
  });
  
  try {
    tr.getParagraphs().forEach(p => {
      p.getRange().getParagraphStyle().setLineSpacing(100).setSpaceBelow(6);
      
      
    });
  } catch (e) {}
  applyStyleRanges(tr, ranges);
}


function checkSpacing(s, out, i, nextCharIndex) {
  let prefix = '';
  let suffix = '';

  
  
  if (out.length > 0 && !/\s$/.test(out)) {
    prefix = ' ';
  }

  
  
  
  if (nextCharIndex < s.length && !/^\s/.test(s[nextCharIndex])) {
    suffix = ' ';
  }
  
  return { prefix, suffix };
}

function parseInlineStyles(s) {
  const ranges = [];
  let out = '';
  let i = 0;
  
  while (i < s.length) {
    
    if (s[i] === '*' && s[i + 1] === '*' && 
        s[i + 2] === '[' && s[i + 3] === '[') {
      const contentStart = i + 4;
      const close = s.indexOf(']]**', contentStart);
      if (close !== -1) {
        let content = s.substring(contentStart, close); 
        const nextCharIndex = close + 4;
        
        
        const { prefix, suffix } = checkSpacing(s, out, i, nextCharIndex);

        out += prefix; 
        const start = out.length;
        
        content += suffix; 
        out += content;
        
        const end = out.length - suffix.length; 

        const rangeObj = {
          start,
          end,
          bold: true,
          color: CONFIG.COLORS.primary_color,
        };
        ranges.push(rangeObj);
        i = nextCharIndex;
        continue;
      }
    }
    
    
    if (s[i] === '[' && s[i + 1] === '[') {
      const close = s.indexOf(']]', i + 2);
      if (close !== -1) {
        let content = s.substring(i + 2, close); 
        const nextCharIndex = close + 2;
        
        
        const { prefix, suffix } = checkSpacing(s, out, i, nextCharIndex);

        out += prefix;
        const start = out.length;
        
        content += suffix;
        out += content;
        
        const end = out.length - suffix.length;

        const rangeObj = {
          start,
          end,
          bold: true,
          color: CONFIG.COLORS.primary_color,
        };
        ranges.push(rangeObj);
        i = nextCharIndex;
        continue;
      }
    }
    
    
    if (s[i] === '*' && s[i + 1] === '*') {
      const close = s.indexOf('**', i + 2);
      if (close !== -1) {
        let content = s.substring(i + 2, close); 
        
        
        if (content.indexOf('[[') === -1) {
          const nextCharIndex = close + 2;

          
          const { prefix, suffix } = checkSpacing(s, out, i, nextCharIndex);

          out += prefix;
          const start = out.length;
          
          content += suffix;
          out += content;
          
          const end = out.length - suffix.length;

          ranges.push({
            start,
            end,
            bold: true,
          });
          i = nextCharIndex;
          continue;
        } else {
          
          i += 2;
          continue;
        }
      }
    }
    
    out += s[i];
    i++;
  }
  
  return {
    output: out,
    ranges
  };
}


function cleanSpeakerNotes(notesText) {
  if (!notesText) return '';
  
  let cleaned = notesText;
  
  
  cleaned = cleaned.replace(/\*\*([^*]+)\*\*/g, '$1');
  
  
  cleaned = cleaned.replace(/\[\[([^\]]+)\]\]/g, '$1');
  
  
  cleaned = cleaned.replace(/\*([^*]+)\*/g, '$1');
  
  
  cleaned = cleaned.replace(/_([^_]+)_/g, '$1');
  
  
  cleaned = cleaned.replace(/~~([^~]+)~~/g, '$1');
  
  
  cleaned = cleaned.replace(/`([^`]+)`/g, '$1');
  
  return cleaned;
}

function applyStyleRanges(textRange, ranges) {
  ranges.forEach(r => {
    try {
      const sub = textRange.getRange(r.start, r.end);
      if (!sub) return;
      const st = sub.getTextStyle();
      if (r.bold) st.setBold(true);
      if (r.color) st.setForegroundColor(r.color);
      if (r.size) st.setFontSize(r.size);
    } catch (e) {}
  });
}

function isAgendaTitle(title) {
  return /(agenda|アジェンダ|目次|本日お伝えすること)/i.test(String(title || ''));
}

function buildAgendaFromSlideData() {
  return __SLIDE_DATA_FOR_AGENDA.filter(d => d && d.type === 'section' && d.title).map(d => d.title.trim());
}


function drawCompareBox(slide, layout, left, top, width, height, title, items, settings, isLeft = false) { 
  
  const th = layout.pxToPt(40); 
  const compareColors = generateCompareColors(settings.primaryColor); 
  const headerColor = isLeft ? compareColors.left : compareColors.right; 

  
  
  let __cbShapesDone = false;
  if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
    try {
      restAddShape(slide.getObjectId(), { left: left, top: top, width: width, height: height }, 'RECTANGLE', { fillRgb: hexToRgb01(CONFIG.COLORS.background_gray), outlineRgb: hexToRgb01(CONFIG.COLORS.lane_border), outlineWeight: 1 });
      restAddShape(slide.getObjectId(), { left: left, top: top, width: width, height: th }, 'RECTANGLE', { fillRgb: hexToRgb01(headerColor), outlineRgb: hexToRgb01(headerColor), outlineWeight: 1 });
      __cbShapesDone = true;
    } catch (eCb) { Logger.log('[PhaseA] compareBox図形REST失敗→SlidesApp: ' + eCb.message); }
  }
  if (!__cbShapesDone) {
    const box = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, left, top, width, height);
    box.getFill().setSolidFill(CONFIG.COLORS.background_gray);
    box.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.lane_border);
    const titleBarBg = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, left, top, width, th);
    titleBarBg.getFill().setSolidFill(headerColor);
    titleBarBg.getBorder().getLineFill().setSolidFill(headerColor);
  }

  
  const pad = layout.pxToPt(12); 
  const bodyRectCmp = { left: left + pad, top: top + th + pad, width: width - pad * 2, height: height - th - pad * 2 };
  
  let __cbDone = false;
  if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
    try {
      _restStyledAutofitBox(slide, { left: left, top: top, width: width, height: th }, title,
        { size: CONFIG.FONTS.sizes.laneTitle, bold: true, color: CONFIG.COLORS.background_gray, align: SlidesApp.ParagraphAlignment.CENTER },
        null, null, {});
      _restBulletsAutofitBox(slide, bodyRectCmp, items, { size: CONFIG.FONTS.sizes.body }, {});
      __cbDone = true;
    } catch (e) { Logger.log('[REST] compareBox失敗→SlidesAppフォールバック: ' + e.message); }
  }
  if (!__cbDone) {
  const titleTextShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, left, top, width, th); 
  titleTextShape.getFill().setTransparent();
  titleTextShape.getBorder().setTransparent();
  setStyledText(titleTextShape, title, { 
    size: CONFIG.FONTS.sizes.laneTitle,
    bold: true,
    color: CONFIG.COLORS.background_gray,
    align: SlidesApp.ParagraphAlignment.CENTER
  });

  
  const body = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX,
    bodyRectCmp.left, bodyRectCmp.top, bodyRectCmp.width, bodyRectCmp.height
  );
  setBulletsWithInlineStyles(body, items); 

  try {
    
    
    const adjustmentLog = adjustShapeText_External(body, null);
    Logger.log(`Compare Slide Box (${isLeft ? 'Left' : 'Right'}) Adjustment: ${adjustmentLog.log}`);
  } catch(e) {
    Logger.log(`Error during Compare Slide Box (${isLeft ? 'Left' : 'Right'}) adjustment: ${e.message}`);
  }
  } 

}


function drawArrowBetweenRects(slide, a, b, arrowH, arrowGap, settings) {
  const fromX = a.left + a.width;
  const fromY = a.top + a.height / 2;
  const toX = b.left;
  const toY = b.top + b.height / 2;

  
  if (toX - fromX <= 0) return;

  const line = slide.insertLine(SlidesApp.LineCategory.STRAIGHT, fromX, fromY, toX, toY);
  line.getLineFill().setSolidFill(settings.primaryColor);
  line.setWeight(1.5);
  line.setEndArrow(SlidesApp.ArrowStyle.FILL_ARROW);
}


function drawNumberedItems(slide, layout, area, items, settings) {
  
  createContentCushion(slide, area, settings, layout);
  
  const n = Math.max(1, items.length);
  const topPadding = layout.pxToPt(30);
  const bottomPadding = layout.pxToPt(10);
  const drawableHeight = area.height - topPadding - bottomPadding;
  const gapY = drawableHeight / Math.max(1, n - 1);
  const cx = area.left + layout.pxToPt(44);
  const top0 = area.top + topPadding;

  const line = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, cx - layout.pxToPt(1), top0 + layout.pxToPt(6), layout.pxToPt(2), gapY * (n - 1));
  line.getFill().setSolidFill(CONFIG.COLORS.faint_gray);
  line.getBorder().setTransparent();

  for (let i = 0; i < n; i++) {
    const cy = top0 + gapY * i;
    const sz = layout.pxToPt(28);
    const numBox = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, cx - sz/2, cy - sz/2, sz, sz);
    numBox.getFill().setSolidFill(settings.primaryColor);
    numBox.getBorder().setTransparent();
    const num = numBox.getText(); num.setText(String(i + 1));
    applyTextStyle(num, { size: 12, bold: true, color: CONFIG.COLORS.background_white, align: SlidesApp.ParagraphAlignment.CENTER });

    
    let cleanText = String(items[i] || '');
    cleanText = cleanText.replace(/^\s*\d+(\.\s*|\s+)/, '');

    const niRect = { left: cx + layout.pxToPt(28), top: cy - layout.pxToPt(16), width: area.width - layout.pxToPt(70), height: layout.pxToPt(32) };
    
    let __niDone = false;
    if (__REST_TEXT_ACTIVE && __REST_PRESENTATION_ID) {
      try {
        _restStyledAutofitBox(slide, niRect, cleanText, { size: CONFIG.FONTS.sizes.processStep }, SlidesApp.ContentAlignment.MIDDLE, null, { noAutofit: true });
        __niDone = true;
      } catch (e) { Logger.log('[REST] numberedItem失敗→SlidesAppフォールバック: ' + e.message); }
    }
    if (!__niDone) {
    const txt = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, niRect.left, niRect.top, niRect.width, niRect.height);
    setStyledText(txt, cleanText, { size: CONFIG.FONTS.sizes.processStep });
    try { txt.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch(e){}
    }
  }
}


function setBoldTextSize(shapeOrTextRange, size = 16) {
  let textRange;
  try {
    
    if (shapeOrTextRange && typeof shapeOrTextRange.getText === 'function') {
      
      textRange = shapeOrTextRange.getText();
    } else if (shapeOrTextRange && typeof shapeOrTextRange.getRuns === 'function') {
      
      textRange = shapeOrTextRange;
    } else {
      Logger.log('Error: setBoldTextSizeにはShapeまたはTextRangeを指定してください。');
      return;
    }

    
    if (!textRange || textRange.isEmpty()) {
      return;
    }

    
    const runs = textRange.getRuns();

    
    runs.forEach(run => {
      const style = run.getTextStyle();
      
      if (style.isBold()) {
        
        style.setFontSize(size);
      }
    });

  } catch (e) {
    Logger.log(`Error in setBoldTextSize: ${e.message}`);
  }
}



function setParenthesizedTextSize(shapeOrTextRange, size) {
  let textRange;
  try {
    if (shapeOrTextRange && typeof shapeOrTextRange.getText === 'function') {
      textRange = shapeOrTextRange.getText();
    } else if (shapeOrTextRange && typeof shapeOrTextRange.getRuns === 'function') {
      textRange = shapeOrTextRange;
    } else {
      Logger.log('Error: setParenthesizedTextSizeにはShapeまたはTextRangeを指定してください。');
      return;
    }

    if (!textRange || textRange.isEmpty()) {
      return;
    }

    const fullText = textRange.asString();
    const matches = []; 
    const regex = /(\(|（)([^()（）]+)(\)|）)/g;
    let match;
    
    
    while ((match = regex.exec(fullText)) !== null) {
        matches.push(match);
    }

    
    
    for (let i = matches.length - 1; i >= 0; i--) {
        match = matches[i];
        
        const startIndex = match.index;
        const endIndex = match.index + match[0].length;
        const originalText = match[0]; 
        
        
        let newText = originalText;
        
        
        if (match[1] === '（') {
            
            newText = ' (' + newText.substring(1); 
        }
        if (match[3] === '）') {
            
            newText = newText.substring(0, newText.length - 1) + ') '; 
        }

        if (startIndex < endIndex) {
            try {
                
                const subRange = textRange.getRange(startIndex, endIndex);
                
                
                subRange.setText(newText);
                
                
                
                const newEndIndex = startIndex + newText.length;
                const replacedRange = textRange.getRange(startIndex, newEndIndex);
                
                replacedRange.getTextStyle().setFontSize(size);

                Logger.log(`Set size ${size} for text "${newText}" at [${startIndex}-${newEndIndex}]`);

            } catch (rangeError) {
                Logger.log(`Error processing range [${startIndex}-${endIndex}] for text "${originalText}": ${rangeError.message}`);
            }
        }
    }

  } catch (e) {
    Logger.log(`Error in setParenthesizedTextSize: ${e.message}`);
  }
}







function drawCreditTextRest(slide, layout, creditPosPx) {
  try {
    
    const widthPx = 320;
    const rightPx = (creditPosPx.right != null) ? creditPosPx.right : 55;
    const leftPx = Math.max(0, CONFIG.BASE_PX.W - rightPx - widthPx);
    const topPx = (creditPosPx.top != null) ? creditPosPx.top : 518;
    const rect = {
      left:   layout.offsetX_pt + layout.pxToPt(leftPx) * layout.scaleX,
      top:    layout.offsetY_pt + layout.pxToPt(topPx) * layout.scaleY,
      width:  layout.pxToPt(widthPx) * layout.scaleX,
      height: layout.pxToPt(18) * layout.scaleY
    };
    restAddTextBox(slide.getObjectId(), rect, __CREDIT_TEXT, {
      size: CONFIG.FONTS.sizes.footer, 
      bold: true,
      fontFamily: 'Verdana',
      colorRgb: __CREDIT_TEXT_COLOR || hexToRgb01('#9AA0A6'),
      align: 'END',            
      contentAlignment: 'MIDDLE'
    });
  } catch (e) {
    Logger.log('[drawCreditTextRest] クレジットテキスト配置に失敗: ' + e.message);
  }
}


function _rgb01ToHexLocal(c) {
  if (!c || typeof c !== 'object') return (typeof c === 'string' && c.charAt(0) === '#') ? c : '#9AA0A6';
  const h = function (x) { var v = Math.max(0, Math.min(255, Math.round((x || 0) * 255))).toString(16); return v.length < 2 ? '0' + v : v; };
  return '#' + h(c.red) + h(c.green) + h(c.blue);
}



function drawCreditTextSlidesApp(slide, layout, creditPosPx) {
  try {
    if (!__CREDIT_TEXT) return;
    const widthPx = 320;
    const rightPx = (creditPosPx.right != null) ? creditPosPx.right : 55;
    const leftPx = Math.max(0, CONFIG.BASE_PX.W - rightPx - widthPx);
    const topPx = (creditPosPx.top != null) ? creditPosPx.top : 518;
    const left = layout.offsetX_pt + layout.pxToPt(leftPx) * layout.scaleX;
    const top = layout.offsetY_pt + layout.pxToPt(topPx) * layout.scaleY;
    const width = layout.pxToPt(widthPx) * layout.scaleX;
    const height = layout.pxToPt(18) * layout.scaleY;
    const shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, left, top, width, height);
    const tr = shape.getText();
    tr.setText(__CREDIT_TEXT);
    const ts = tr.getTextStyle();
    ts.setFontSize(CONFIG.FONTS.sizes.footer).setBold(true);
    try { ts.setFontFamily('Verdana'); } catch (e) {}
    try { ts.setForegroundColor(_rgb01ToHexLocal(__CREDIT_TEXT_COLOR)); } catch (e) {}
    try { tr.getParagraphs().forEach(function (p) { p.getRange().getParagraphStyle().setParagraphAlignment(SlidesApp.ParagraphAlignment.END); }); } catch (e) {}
    try { shape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch (e) {}
  } catch (e) {
    Logger.log('[drawCreditTextSlidesApp] クレジットテキスト(SlidesApp)配置に失敗: ' + e.message);
  }
}


function drawCreditImage(slide, layout, creditImageBlob, creditLink) {
  try {
    const creditPosPx = CONFIG.POS_PX.footer.creditImage;
    if (!creditPosPx) return;

    
    
    if (__CREDIT_TEXT) {
      if (__REST_PRESENTATION_ID && __SLIDES_API_OK) {
        drawCreditTextRest(slide, layout, creditPosPx);
      } else {
        drawCreditTextSlidesApp(slide, layout, creditPosPx);
      }
      return;
    }

    if (!creditImageBlob) return;

    
    
    
    const newHeight = layout.pxToPt(creditPosPx.height) * layout.scaleY;
    const rightMarginPt = layout.pxToPt(creditPosPx.right) * layout.scaleX;
    const topPt = layout.offsetY_pt + layout.pxToPt(creditPosPx.top) * layout.scaleY;

    
    
    
    if (__CREDIT_DIMS && __CREDIT_IMAGE_URL && __REST_PRESENTATION_ID && __SLIDES_API_OK) {
      const newWidth = __CREDIT_DIMS.width;
      const newLeft = (layout.contentRight_pt - rightMarginPt) - newWidth;
      const __cr = Date.now();
      const objId = restAddImage(slide.getObjectId(), { left: newLeft, top: topPt, width: newWidth, height: newHeight }, __CREDIT_IMAGE_URL);
      __CREDIT_REST_REFS.push({ slide: slide, objectId: objId });
      perfAdd('credit:rest', Date.now() - __cr);
      return;
    }

    
    const __ct = Date.now();
    const img = slide.insertImage(creditImageBlob);
    perfAdd('credit:insert', Date.now() - __ct);

    if (!__CREDIT_DIMS) {
      
      const aspect = img.getWidth() / img.getHeight();
      __CREDIT_DIMS = { width: newHeight * aspect, height: newHeight };
    }
    const newWidth = __CREDIT_DIMS.width;
    const newLeft = (layout.contentRight_pt - rightMarginPt) - newWidth;

    const __cs = Date.now();
    img.setLeft(newLeft)
       .setTop(topPt)
       .setWidth(newWidth)
       .setHeight(newHeight)
       .setLinkUrl(creditLink); 
    perfAdd('credit:setter', Date.now() - __cs);

  } catch (e) {
    Logger.log(`[drawCreditImage] クレジット画像の挿入に失敗: ${e.message}`);
  }
}


function safeGetRect(layout, path) {
  try {
    const rect = layout.getRect(path);
    if (rect && 
        (typeof rect.left === 'number' || rect.left === undefined) && 
        typeof rect.top === 'number' && 
        typeof rect.width === 'number' && 
        typeof rect.height === 'number') {
      
      
      
      if (rect.left === undefined) {
        Logger.log(`[safeGetRect] Warning: rect.left is undefined for path ${path}`);
        return null;
      }
      
      return rect;
    }
    
    if (!path.includes('headerLogo')) {
      Logger.log(`[safeGetRect] Invalid rect for path ${path}:`, JSON.stringify(rect));
    }
    return null;
  } catch (e) {
    
    if (!path.includes('headerLogo')) {
      Logger.log(`[safeGetRect] Error for path ${path}:`, e.message);
    }
    return null;
  }
}


function findContentRect(layout, key) {
  const candidates = [
    'body',        
    'area',        
    'gridArea',    
    'lanesArea',   
    'pyramidArea', 
    'stepArea',    
    'singleRow',   
    'twoColLeft',  
    'leftBox',     
    'leftText'     
  ];
  for (const name of candidates) {
    const r = safeGetRect(layout, `${key}.${name}`);
    if (r && r.top != null) return r;
  }
  return null;
}

function adjustColorBrightness(hex, factor) {
  const c = hex.replace('#', '');
  const rgb = parseInt(c, 16);
  let r = (rgb >> 16) & 0xff,
    g = (rgb >> 8) & 0xff,
    b = (rgb >> 0) & 0xff;
  r = Math.min(255, Math.round(r * factor));
  g = Math.min(255, Math.round(g * factor));
  b = Math.min(255, Math.round(b * factor));
  return '#' + (0x1000000 + (r << 16) + (g << 8) + b).toString(16).slice(1);
}

function setMainSlideBackground(slide, layout, imageUpdateOption = 'update') {
  
  
  setBackgroundImageFromUrl(slide, layout, CONFIG.BACKGROUND_IMAGES.main, CONFIG.COLORS.background_white, imageUpdateOption);
  
  
}

function setBackgroundImageFromUrl(slide, layout, imageUrl, fallbackColor, imageUpdateOption = 'update') {
  
  if (__CURRENT_SLIDE_USES_TEMPLATE) {
    return;
  }
  
  
  
  
  
  
  
  
  
    
    perfTime('bg:fill', () => slide.getBackground().setSolidFill(fallbackColor));
  if (imageUpdateOption === 'update') { 
    if (!imageUrl) return;
    try {
      const image = insertImageFromUrlOrFileId(imageUrl);
      if (!image) return;

      
      perfTime('bg:image', () => slide.insertImage(image).setWidth(layout.pageW_pt).setHeight(layout.pageH_pt).setLeft(0).setTop(0).sendToBack());
    } catch (e) {
      Logger.log(`Background image failed: ${e.message}. URL: ${imageUrl}`);
    }
  }
}


function readImageSizeFromBytes(blob) {
  try {
    const b = blob && blob.getBytes ? blob.getBytes() : null;
    if (!b || b.length < 24) return null;
    const u = (i) => b[i] & 0xFF;
    
    if (u(0) === 0x89 && u(1) === 0x50 && u(2) === 0x4E && u(3) === 0x47) {
      const w = (u(16) << 24) | (u(17) << 16) | (u(18) << 8) | u(19);
      const h = (u(20) << 24) | (u(21) << 16) | (u(22) << 8) | u(23);
      return (w > 0 && h > 0) ? { width: w, height: h } : null;
    }
    
    if (u(0) === 0x47 && u(1) === 0x49 && u(2) === 0x46 && u(3) === 0x38) {
      const w = u(6) | (u(7) << 8), h = u(8) | (u(9) << 8);
      return (w > 0 && h > 0) ? { width: w, height: h } : null;
    }
    
    if (u(0) === 0xFF && u(1) === 0xD8) {
      let i = 2;
      while (i + 9 < b.length) {
        if (u(i) !== 0xFF) { i++; continue; }
        const marker = u(i + 1);
        if (marker === 0xD8 || marker === 0x01 || (marker >= 0xD0 && marker <= 0xD7)) { i += 2; continue; }
        const len = (u(i + 2) << 8) | u(i + 3);
        if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC) {
          const h = (u(i + 5) << 8) | u(i + 6);
          const w = (u(i + 7) << 8) | u(i + 8);
          return (w > 0 && h > 0) ? { width: w, height: h } : null;
        }
        if (len <= 0) break;
        i += 2 + len;
      }
    }
  } catch (e) {  }
  return null;
}


function prepareRestImageSource(rawUrlOrId, tempFileName) {
  try {
    const raw = String(rawUrlOrId || '').trim();
    if (!raw) return null;
    const blob = insertImageFromUrlOrFileId(raw); 
    const size = readImageSizeFromBytes(blob);
    if (!size) return null;                       
    let url = null;
    if (/^https?:\/\//i.test(raw)) {
      url = raw;
    } else if (blob && typeof blob !== 'string' && blob.getBytes) {
      const f = DriveApp.createFile(blob.copyBlob().setName(tempFileName));
      f.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
      __REST_IMAGE_TEMP_IDS.push(f.getId());
      url = 'https://drive.google.com/uc?export=view&id=' + f.getId();
    }
    return url ? { url: url, aspect: size.height / size.width } : null;
  } catch (e) {
    Logger.log(`[prepareRestImageSource] REST用URLの用意に失敗(SlidesApp経路へ): ${e.message}`);
    return null;
  }
}


function insertImageFromUrlOrFileId(urlOrFileId) {
  if (!urlOrFileId) return null;
  
  
  if (typeof __IMAGE_BLOB_CACHE !== 'undefined' && __IMAGE_BLOB_CACHE &&
      Object.prototype.hasOwnProperty.call(__IMAGE_BLOB_CACHE, urlOrFileId)) {
    perfAdd('imageCacheHit', 0); 
    return __IMAGE_BLOB_CACHE[urlOrFileId];
  }
  
  function extractFileIdFromUrl(url) {
    const patterns = [
      /\/file\/d\/([a-zA-Z0-9_-]+)/,
      /id=([a-zA-Z0-9_-]+).*file/,
      /file\/([a-zA-Z0-9_-]+)/
    ];
    for (const pattern of patterns) {
      const match = url.match(pattern);
      if (match && match[1]) return match[1];
    }
    return null;
  }
  
  
  const fileIdPattern = /^[a-zA-Z0-9_-]{25,}$/;
  
  
  const extractedFileId = extractFileIdFromUrl(urlOrFileId);
  if (extractedFileId && fileIdPattern.test(extractedFileId)) {
    
    try {
      const blob = perfTime('imageFetch(drive)', () => DriveApp.getFileById(extractedFileId).getBlob());
      __IMAGE_BLOB_CACHE[urlOrFileId] = blob;
      return blob;
    } catch (e) {
      Logger.log(`Drive file access failed: ${e.message}. FileID: ${extractedFileId}`);
      return null;
    }
  } else if (fileIdPattern.test(urlOrFileId)) {
    
    try {
      const blob = perfTime('imageFetch(drive)', () => DriveApp.getFileById(urlOrFileId).getBlob());
      __IMAGE_BLOB_CACHE[urlOrFileId] = blob;
      return blob;
    } catch (e) {
      Logger.log(`Drive file access failed: ${e.message}. FileID: ${urlOrFileId}`);
      return null;
    }
  } else if (urlOrFileId.startsWith('data:image/')) {
    
    try {
      const parts = urlOrFileId.split(',');
      if (parts.length !== 2) throw new Error('Invalid Base64 format.');
      
      const mimeType = parts[0].match(/:(.*?);/)[1];
      const base64Data = parts[1];
      const decodedData = Utilities.base64Decode(base64Data);

      const blob = Utilities.newBlob(decodedData, mimeType);
      __IMAGE_BLOB_CACHE[urlOrFileId] = blob;
      return blob;
    } catch (e) {
      Logger.log(`Base64 image decoding failed: ${e.message}`);
      return null; 
    }
  } else {
    
    
    
    try {
      
      let resp = null;
      let code = 0;
      for (let __attempt = 0; __attempt < 2; __attempt++) {
        try {
          resp = perfTime('imageFetch(url)', () => UrlFetchApp.fetch(urlOrFileId, { muteHttpExceptions: true, followRedirects: true }));
          code = resp.getResponseCode();
        } catch (eFetch) {
          
          if (__attempt === 0) {
            Logger.log(`URL画像の事前取得で例外。300ms後に再試行: ${eFetch.message}。URL: ${urlOrFileId}`);
            Utilities.sleep(300);
            continue;
          }
          throw eFetch;
        }
        
        if (code >= 500 && __attempt === 0) {
          Logger.log(`URL画像の事前取得が5xx(code=${code})。300ms後に再試行: ${urlOrFileId}`);
          Utilities.sleep(300);
          continue;
        }
        break;
      }
      const headers = resp.getHeaders() || {};
      const ct = String(headers['Content-Type'] || headers['content-type'] || '').toLowerCase();
      if (code >= 200 && code < 300 && ct.indexOf('image/') === 0) {
        const blob = resp.getBlob();
        __IMAGE_BLOB_CACHE[urlOrFileId] = blob;
        return blob;
      }
      Logger.log(`URL画像の事前取得をスキップ (code=${code}, type=${ct})。URL文字列で続行: ${urlOrFileId}`);
    } catch (e) {
      Logger.log(`URL画像の事前取得失敗: ${e.message}。URL文字列で続行: ${urlOrFileId}`);
    }
    
    __IMAGE_BLOB_CACHE[urlOrFileId] = urlOrFileId;
    return urlOrFileId;
  }
}

function normalizeImages(arr) {
  return (arr || []).map(v => typeof v === 'string' ? {
    url: v
  } : (v && v.url ? v : null)).filter(Boolean).slice(0, 6);
}

function renderImagesInArea(slide, layout, area, images, imageUpdateOption = 'update') {
  if (!images || !images.length) return;
  if (imageUpdateOption !== 'update') {
    Logger.log('[renderImagesInArea] Skipping image insertion due to imageUpdateOption.');
    return; 
  }
  const n = Math.min(6, images.length);
  let cols = n === 1 ? 1 : (n <= 4 ? 2 : 3);
  const rows = Math.ceil(n / cols);
  const gap = layout.pxToPt(10);
  const cellW = (area.width - gap * (cols - 1)) / cols,
    cellH = (area.height - gap * (rows - 1)) / rows;
  for (let i = 0; i < n; i++) {
    const r = Math.floor(i / cols),
      c = i % cols;
    try {
      
      const imageData = insertImageFromUrlOrFileId(images[i].url);
      const img = slide.insertImage(imageData);
      
      const __iw = img.getWidth(), __ih = img.getHeight();
      const scale = Math.min(cellW / __iw, cellH / __ih);
      const w = __iw * scale,
        h = __ih * scale;
      img.setWidth(w).setHeight(h).setLeft(area.left + c * (cellW + gap) + (cellW - w) / 2).setTop(area.top + r * (cellH + gap) + (cellH - h) / 2);
    } catch (e) { Logger.log('[renderImagesInArea] 画像挿入失敗(' + i + '): ' + (e && e.message)); }
  }
}

function createGradientRectangle(slide, x, y, width, height, colors) {
  const numStrips = Math.max(20, Math.floor(width / 5));
  const stripWidth = width / numStrips;
  const startColor = hexToRgb(colors[0]),
    endColor = hexToRgb(colors[1]);
  if (!startColor || !endColor) return null;
  const shapes = [];
  for (let i = 0; i < numStrips; i++) {
    const ratio = i / (numStrips - 1);
    const r = Math.round(startColor.r + (endColor.r - startColor.r) * ratio);
    const g = Math.round(startColor.g + (endColor.g - startColor.g) * ratio);
    const b = Math.round(startColor.b + (endColor.b - startColor.b) * ratio);
    const strip = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, x + (i * stripWidth), y, stripWidth + 0.5, height);
    strip.getFill().setSolidFill(r, g, b);
    strip.getBorder().setTransparent();
    shapes.push(strip);
  }
  if (shapes.length > 1) {
    return slide.group(shapes);
  }
  return shapes[0] || null;
}

function applyFill(slide, x, y, width, height, settings) {
  if (settings.enableGradient) {
    createGradientRectangle(slide, x, y, width, height, [settings.gradientStart, settings.gradientEnd]);
  } else {
    const shape = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, x, y, width, height);
    shape.getFill().setSolidFill(settings.primaryColor);
    shape.getBorder().setTransparent();
  }
}


function createPillShapeUnderline(slide, x, y, width, height, settings) {
  
  if (__NATIVE_DECOR_ACTIVE) return;
  const diameter = height;
  const radius = height / 2;
  const rectWidth = Math.max(0, width - diameter);

  
  if (width < diameter) {
    const centerCircle = slide.insertShape(SlidesApp.ShapeType.ELLIPSE, x, y, diameter, diameter);
    const color = settings.enableGradient ? settings.gradientStart : settings.primaryColor;
    centerCircle.getFill().setSolidFill(color);
    centerCircle.getBorder().setTransparent();
    return;
  }

  
  const leftCircle = slide.insertShape(SlidesApp.ShapeType.ELLIPSE, x, y, diameter, diameter);
  const rightCircle = slide.insertShape(SlidesApp.ShapeType.ELLIPSE, x + width - diameter, y, diameter, diameter);
  
  if (settings.enableGradient) {
    leftCircle.getFill().setSolidFill(settings.gradientStart);
    rightCircle.getFill().setSolidFill(settings.gradientEnd);
  } else {
    const color = settings.primaryColor;
    leftCircle.getFill().setSolidFill(color);
    rightCircle.getFill().setSolidFill(color);
  }
  leftCircle.getBorder().setTransparent();
  rightCircle.getBorder().setTransparent();

  let middleShape = null;

  
  if (rectWidth > 0) {
    if (settings.enableGradient) {
      
      
      
      if (!__CACHED_GRADIENT_UNDERLINE_RECT) {
        const MAX_MASTER_WIDTH = 300; 
        
        const masterGroup = createGradientRectangle(slide, 2000, 0, MAX_MASTER_WIDTH, height, [settings.gradientStart, settings.gradientEnd]);
        if (masterGroup) {
          __CACHED_GRADIENT_UNDERLINE_RECT = masterGroup;
        }
      }

      
      if (__CACHED_GRADIENT_UNDERLINE_RECT) {
        try {
          
          middleShape = slide.insertGroup(__CACHED_GRADIENT_UNDERLINE_RECT);
          
          
          middleShape.setWidth(rectWidth);
          middleShape.setHeight(height); 
          middleShape.setLeft(x + radius);
          middleShape.setTop(y);
          
        } catch (e) {
          
          middleShape = createGradientRectangle(slide, x + radius, y, rectWidth, height, [settings.gradientStart, settings.gradientEnd]);
        }
      } else {
        
        middleShape = createGradientRectangle(slide, x + radius, y, rectWidth, height, [settings.gradientStart, settings.gradientEnd]);
      }

    } else {
      
      middleShape = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, x + radius, y, rectWidth, height);
      middleShape.getFill().setSolidFill(settings.primaryColor);
      middleShape.getBorder().setTransparent();
    }
  }

  
  const shapesToGroup = [leftCircle, rightCircle];
  if (middleShape) {
    shapesToGroup.push(middleShape);
  }
  
  if (shapesToGroup.length > 1) {
    slide.group(shapesToGroup);
  }
}





function clearLegacyUserProperties() {
  try {
    
    const properties = PropertiesService.getUserProperties().getProperties();
    
    
    const legacyKeys = [
      'primaryColor',
      'gradientStart', 
      'gradientEnd',
      'fontFamily',
      'showTitleUnderline',
      'showBottomBar',
      'enableGradient',
      'footerText',
      'headerLogoUrl',
      'closingLogoUrl',
      'titleBgUrl',
      'sectionBgUrl',
      'mainBgUrl',
      'closingBgUrl',
      'driveFolderUrl',
      'driveFolderId'
    ];
    
    
    const keysToDelete = [];
    legacyKeys.forEach(key => {
      if (properties.hasOwnProperty(key)) {
        keysToDelete.push(key);
      }
    });
    
    if (keysToDelete.length > 0) {
      
      const userProperties = PropertiesService.getUserProperties();
      keysToDelete.forEach(key => {
        userProperties.deleteProperty(key);
      });
      Logger.log(`削除されたレガシープロパティ: ${keysToDelete.join(', ')}`);
      return {
        status: 'success',
        message: `${keysToDelete.length}個のレガシープロパティを削除しました。`,
        deletedKeys: keysToDelete
      };
    } else {
      return {
        status: 'info',
        message: '削除対象のレガシープロパティは見つかりませんでした。'
      };
    }
    
  } catch (e) {
    Logger.log(`レガシープロパティ削除エラー: ${e.message}`);
    return {
      status: 'error',
      message: `レガシープロパティの削除中にエラーが発生しました: ${e.message}`
    };
  }
}


function saveGemUrl(url) {
  try {
    const trimmedUrl = url ? url.trim() : '';
    const gemBaseUrl = 'https://gemini.google.com/gem/';

    
    if (!trimmedUrl) {
      throw new Error("URLを入力してください。");
    }
    
    
    
    if (!trimmedUrl.startsWith('https://')) {
      throw new Error("URLは https:// で始まる必要があります。");
    }

    
    PropertiesService.getScriptProperties().setProperty('geminiGemUrl', trimmedUrl);

    
    try {
      _ensureFirstAdmin_();
    } catch (admErr) {
      Logger.log('最初の管理者の自動登録に失敗: ' + admErr.message);
    }

    
    if (!trimmedUrl.startsWith(gemBaseUrl)) {
      
      return { status: 'warning', message: 'URLは保存されましたが、Gemini Gem (https://gemini.google.com/gem/...) の形式ではありません。' };
    } else if (trimmedUrl.length === gemBaseUrl.length) {
      
      return { status: 'warning', message: 'URLが保存されましたが、特定のGemが指定されていません。' };
    } else {
      
      return { status: 'success', message: 'Gemini Gem URLを保存しました。' };
    }

  } catch (e) {
    Logger.log(`Gem URL保存エラー: ${e.message}`);
    
    return { status: 'error', message: e.message };
  }
}





function _getCurrentUserEmail_() {
  try {
    var e = Session.getEffectiveUser().getEmail();
    return e ? String(e).trim().toLowerCase() : '';
  } catch (err) {
    return '';
  }
}


function _getAdminEmails_() {
  try {
    var raw = PropertiesService.getScriptProperties().getProperty('adminEmails');
    if (!raw) return [];
    var arr = JSON.parse(raw);
    return Array.isArray(arr) ? arr.map(function (x) { return String(x).trim().toLowerCase(); }).filter(Boolean) : [];
  } catch (err) {
    return [];
  }
}


function _setAdminEmails_(emails) {
  var clean = (emails || [])
    .map(function (x) { return String(x).trim().toLowerCase(); })
    .filter(function (x) { return x && x.indexOf('@') > 0; });
  var uniq = [];
  clean.forEach(function (x) { if (uniq.indexOf(x) < 0) uniq.push(x); });
  
  var lock = null;
  try {
    lock = LockService.getScriptLock();
    if (!lock.tryLock(5000)) {
      Logger.log('_setAdminEmails_ ロック取得失敗(5s)。ロック無しで続行');
      lock = null;
    }
  } catch (eLk) {
    Logger.log('_setAdminEmails_ ロック取得例外。ロック無しで続行: ' + eLk.message);
    lock = null;
  }
  try {
    PropertiesService.getScriptProperties().setProperty('adminEmails', JSON.stringify(uniq));
  } finally {
    if (lock) {
      try { lock.releaseLock(); } catch (eRl) {}
    }
  }
  return uniq;
}


function _ensureFirstAdmin_() {
  var admins = _getAdminEmails_();
  if (admins.length > 0) return admins;
  var me = _getCurrentUserEmail_();
  if (!me) return admins;
  return _setAdminEmails_([me]);
}


function getAdminInfo() {
  var me = _getCurrentUserEmail_();
  var admins = _getAdminEmails_();
  var noAdmins = admins.length === 0;
  var isAdmin = admins.indexOf(me) >= 0;
  return {
    email: me,
    isAdmin: isAdmin,
    adminEmails: admins,
    
    
    noAdmins: noAdmins,
    canManage: noAdmins || isAdmin
  };
}


function setAdminEmails(emails) {
  var info = getAdminInfo();
  if (!info.canManage) {
    return { ok: false, error: '管理者権限がありません。' };
  }
  var clean = (emails || [])
    .map(function (x) { return String(x).trim().toLowerCase(); })
    .filter(function (x) { return x && x.indexOf('@') > 0; });
  var uniq = [];
  clean.forEach(function (x) { if (uniq.indexOf(x) < 0) uniq.push(x); });
  if (uniq.length === 0) {
    return { ok: false, error: '管理者を1人以上登録してください。' };
  }
  _setAdminEmails_(uniq);
  return { ok: true, adminEmails: uniq };
}


function getSharedPresets() {
  try {
    var raw = PropertiesService.getScriptProperties().getProperty('sharedPresets');
    if (!raw) return {};
    var obj = JSON.parse(raw);
    return (obj && typeof obj === 'object') ? obj : {};
  } catch (err) {
    return {};
  }
}


function sharePreset(name, settings) {
  var info = getAdminInfo();
  if (!info.canManage) {
    return { ok: false, error: '管理者権限がありません。' };
  }
  var presetName = name ? String(name).trim() : '';
  if (!presetName || presetName === '_default') {
    return { ok: false, error: 'デフォルト以外のプリセットを指定してください。' };
  }
  if (!settings || typeof settings !== 'object') {
    return { ok: false, error: '共有する設定データがありません。' };
  }
  var shared = getSharedPresets();
  shared[presetName] = { settings: settings, owner: info.email };
  PropertiesService.getScriptProperties().setProperty('sharedPresets', JSON.stringify(shared));
  return { ok: true, sharedPresets: shared };
}


function unsharePreset(name) {
  var info = getAdminInfo();
  if (!info.canManage) {
    return { ok: false, error: '管理者権限がありません。' };
  }
  var presetName = name ? String(name).trim() : '';
  var shared = getSharedPresets();
  if (Object.prototype.hasOwnProperty.call(shared, presetName)) {
    delete shared[presetName];
    PropertiesService.getScriptProperties().setProperty('sharedPresets', JSON.stringify(shared));
  }
  return { ok: true, sharedPresets: shared };
}




function computeAutofitFontSize(text, baseFontSizePt, boxWidthPt, boxHeightPt, preCalculatedWidthPt, spaceBelowPt) {
  var PAD_TB = 7.5, PAD_LR = 10.0, LH = 1.1, CW = 1.0; 
  text = (text == null) ? '' : String(text);
  if (text.length === 0) return baseFontSizePt;
  var baseFS = Math.floor(baseFontSizePt * 2) / 2; 
  if (!(baseFS > 0)) return baseFontSizePt;
  var preW = (typeof preCalculatedWidthPt === 'number' && preCalculatedWidthPt >= 0) ? preCalculatedWidthPt : null;
  var spBelow = (typeof spaceBelowPt === 'number' && spaceBelowPt > 0) ? spaceBelowPt : 0;
  var textAreaW = boxWidthPt - PAD_LR * 2;
  var textAreaH = boxHeightPt - PAD_TB * 2;
  var paragraphs = text.split('\n');
  var paragraphCount = paragraphs.length;
  var isShortBox = boxHeightPt <= baseFS * 2; 

  function effCount(s) {
    var c = 0;
    for (var i = 0; i < s.length; i++) { c += /[^\x00-\x7F｡-ﾟ]/.test(s[i]) ? 1.0 : 0.6; }
    return c;
  }
  
  if (textAreaW > 0 && textAreaH > 0) {
    if (baseFS * baseFS * text.length <= textAreaW * textAreaH * 0.4) return baseFS;
  }
  
  function overflowAt(fs, emptyFS) {
    if (textAreaW <= 0 || textAreaH <= 0) return true;
    if (isShortBox) {
      var w;
      if (preW != null) { w = (fs === baseFS) ? preW : preW * (fs / baseFS); }
      else { w = effCount(text) * (fs * CW); }
      return (w > textAreaW) || (paragraphCount > 1);
    }
    var total = 0;
    for (var pi = 0; pi < paragraphs.length; pi++) {
      var p = paragraphs[pi];
      var isEmpty = p.trim().length === 0;
      var cfs = Math.max(1, isEmpty ? emptyFS : fs);
      var lineH = cfs * LH + 0.5;
      var charW = cfs * CW;
      var maxChars = charW > 0 ? Math.floor(textAreaW / charW) : 0;
      var lines = isEmpty ? 1 : Math.max(1, (maxChars > 0 ? Math.ceil(effCount(p) / maxChars) : 1));
      total += lines * lineH;
      if (pi > 0) total += spBelow; 
    }
    return total > textAreaH;
  }
  if (isShortBox) {
    if (!overflowAt(baseFS)) return baseFS;
    if (baseFS < 10) return 10; 
    for (var ts = baseFS - 0.5; ts >= 10; ts -= 0.5) { if (!overflowAt(ts)) return ts; }
    return 10;
  }
  
  if (!overflowAt(baseFS, baseFS)) return baseFS;   
  if (!overflowAt(baseFS, 1)) return baseFS;          
  if (baseFS < 10) return 10;
  for (var ts2 = baseFS - 0.5; ts2 >= 10; ts2 -= 0.5) { if (!overflowAt(ts2, 1)) return ts2; }
  return 10;
}

function adjustShapeText_External(shape, preCalculatedWidthPt = null, widthOverride = null, heightOverride = null) {

  
  
  const PADDING_TOP_BOTTOM = 7.5; 
  const PADDING_LEFT_RIGHT = 10.0; 
  
  const LINE_HEIGHT_FACTOR = 1.1;
  
  const AVG_CHAR_WIDTH_FACTOR = 1.0;

  
  let __memoBoxW = null, __memoBoxH = null;
  function __getBoxW(wOverride) {
    if (wOverride !== null && wOverride !== undefined) return wOverride;
    if (__memoBoxW === null) __memoBoxW = shape.getWidth();
    return __memoBoxW;
  }
  function __getBoxH(hOverride) {
    if (hOverride !== null && hOverride !== undefined) return hOverride;
    if (__memoBoxH === null) __memoBoxH = shape.getHeight();
    return __memoBoxH;
  }

  
  let __textCache = null;
  function __getTextCache() {
    if (__textCache !== null) return __textCache;
    const tr = shape.getText();
    const fullText = tr.asString();
    const rawParas = tr.getParagraphs();
    const paraInfos = [];
    for (const p of rawParas) {
      const r = p.getRange();
      const t = r.asString();
      const ps = r.getParagraphStyle();
      const sa = (ps && typeof ps.getSpaceAbove === 'function' ? ps.getSpaceAbove() : 0) || 0;
      const sb = (ps && typeof ps.getSpaceBelow === 'function' ? ps.getSpaceBelow() : 0) || 0;
      paraInfos.push({
        text: t,
        isEmpty: t.trim().length === 0,
        effectiveCount: getEffectiveCharCount(t),
        spaceAbove: sa,
        spaceBelow: sb
      });
    }
    __textCache = { fullText: fullText, paragraphs: paraInfos, paragraphCount: paraInfos.length };
    return __textCache;
  }

  
  let __memoInitialNonEmptyFS = undefined;
  function __getInitialNonEmptyFontSize() {
    if (__memoInitialNonEmptyFS === undefined) __memoInitialNonEmptyFS = getNonEmptyFontSize(shape);
    return __memoInitialNonEmptyFS;
  }

  

  
  function getEffectiveCharCount(text) {
    let count = 0;
    if (!text) return 0; 
    for (let i = 0; i < text.length; i++) {
      const char = text[i];
      
      if (char.match(/[^\x00-\x7F\uFF61-\uFF9F]/)) {
        count += 1.0; 
      } else {
        count += 0.6; 
      }
    }
    return count;
  }

  
  function _isShapeShortBox(shape, baseFontSize, heightOverride = null) {
    if (!baseFontSize || baseFontSize === 0) {
      return false; 
    }
    
    const boxHeight = __getBoxH(heightOverride);
    
    return (boxHeight <= (baseFontSize * 2));
  }

  
  function getNonEmptyFontSize(shape) {
    try {
      const textRange = shape.getText();
      if (!textRange || textRange.isEmpty()) return null; 

      const paragraphs = textRange.getParagraphs(); 
      for (const paragraph of paragraphs) {
        
        if (paragraph.getRange().asString().trim().length > 0) {
          const run = paragraph.getRange().getRuns()[0]; 
          if (run && run.getTextStyle()) { 
            const fontSize = run.getTextStyle().getFontSize(); 
            if (typeof fontSize === 'number') { 
                 return Math.floor(fontSize * 2) / 2; 
            }
          }
        }
      }
      
      const firstRun = textRange.getRuns()[0];
      if (firstRun && firstRun.getTextStyle()) { 
        const fontSize = firstRun.getTextStyle().getFontSize();
        if (typeof fontSize === 'number') { 
             return Math.floor(fontSize * 2) / 2;
        }
      }
      return null; 
    } catch (e) {
      Logger.log("getNonEmptyFontSize エラー: " + e.message + " Shape ID: " + (shape ? shape.getObjectId() : 'N/A'));
      return null; 
    }
  }


  
  function isTextOverflowing(shape, nonEmptyFontSizeOverride = null, preCalculatedWidthPt = null, originalBaseFontSize = null, widthOverride = null, heightOverride = null) {
    let details = "--- 簡易計算の詳細 ---\n\n"; 
    try {
      
      const __useTextCache = (nonEmptyFontSizeOverride !== null);
      const __cache = __useTextCache ? __getTextCache() : null;
      let textRange = null;
      let originalText, paragraphs = null, paragraphCount;
      if (__useTextCache) {
        originalText = __cache.fullText;      
        paragraphCount = __cache.paragraphCount; 
      } else {
        textRange = shape.getText(); 
        originalText = textRange.asString(); 
      }
      if (originalText.length === 0) { 
        return { isOverflow: false, details: "テキストが空です。" }; 
      }
      if (!__useTextCache) {
        paragraphs = textRange.getParagraphs(); 
        paragraphCount = paragraphs.length; 
      }

      
      let currentBaseFontSize; 
      if (nonEmptyFontSizeOverride !== null) {
        currentBaseFontSize = nonEmptyFontSizeOverride; 
        details += `基準フォントサイズ: ${currentBaseFontSize.toFixed(1)} pt (縮小テスト中)\n`; 
      } else {
        currentBaseFontSize = getNonEmptyFontSize(shape); 
        if (currentBaseFontSize === null) { 
          const firstRun = textRange.getRuns()[0];
          if (firstRun && firstRun.getTextStyle()) { 
              const fs = firstRun.getTextStyle().getFontSize();
              if(typeof fs === 'number') {
                  currentBaseFontSize = Math.floor(fs * 2) / 2;
              } else {
                 currentBaseFontSize = 1; 
              }
          } else {
             currentBaseFontSize = 1; 
          }
        }
        details += `基準フォントサイズ: ${currentBaseFontSize.toFixed(1)} pt (空でない段の代表値)\n`; 
      }

      
      if (!currentBaseFontSize || currentBaseFontSize <= 0) { 
        details += "基準フォントサイズが取得できないか無効なため、判定不能です。";
        return { isOverflow: false, details: details }; 
      }

      
      
      const boxWidth = __getBoxW(widthOverride);
      const boxHeight = __getBoxH(heightOverride);
      
      details += `ボックスサイズ (W): ${boxWidth.toFixed(2)} pt\n`; 
      details += `ボックスサイズ (H): ${boxHeight.toFixed(2)} pt\n`; 
      details += `推定余白 (上下): ${PADDING_TOP_BOTTOM} pt\n`; 
      details += `推定余白 (左右): ${PADDING_LEFT_RIGHT} pt\n\n`; 

      const textAreaWidth = boxWidth - (PADDING_LEFT_RIGHT * 2); 
      const textAreaHeight = boxHeight - (PADDING_TOP_BOTTOM * 2); 
      details += `推定テキストエリア内寸 (W): ${textAreaWidth.toFixed(2)} pt\n`; 
      details += `推定テキストエリア内寸 (H): ${textAreaHeight.toFixed(2)} pt\n`; 

      
      if (textAreaWidth <= 0 || textAreaHeight <= 0) {
        details += "判定: 内寸が0以下のため「はみ出し」";
        return { isOverflow: true, details: details }; 
      }

      
      const initialSizeForModeCheck = originalBaseFontSize !== null ? originalBaseFontSize : currentBaseFontSize;
      
      const isShortBox = _isShapeShortBox(shape, initialSizeForModeCheck, heightOverride);
      details += `ボックスタイプ: ${isShortBox ? "単一行モード" : "標準（複数行）モード"} (判定基準FS: ${initialSizeForModeCheck.toFixed(1)}pt)\n\n`; 

      
      let isOverflow; 
      if (isShortBox) {
        
        const fontSizeToUse = currentBaseFontSize; 

        let calculatedTextWidthPt = -1; 

        
        if (nonEmptyFontSizeOverride !== null && preCalculatedWidthPt !== null && preCalculatedWidthPt >= 0 && originalBaseFontSize !== null && originalBaseFontSize > 0) {
          calculatedTextWidthPt = preCalculatedWidthPt * (fontSizeToUse / originalBaseFontSize); 
          details += `使用するテキスト幅: ${calculatedTextWidthPt.toFixed(2)} pt (${fontSizeToUse.toFixed(1)}pt で事前計算値をスケーリング)\n`;
        }
        
        else if (preCalculatedWidthPt !== null && preCalculatedWidthPt >= 0) {
          calculatedTextWidthPt = preCalculatedWidthPt;
          details += `使用するテキスト幅: ${calculatedTextWidthPt.toFixed(2)} pt (事前計算値)\n`;
        }
        
        else {
          const estimatedFullCharWidth = fontSizeToUse * AVG_CHAR_WIDTH_FACTOR; 
          const totalEffectiveCount = getEffectiveCharCount(originalText); 
          calculatedTextWidthPt = totalEffectiveCount * estimatedFullCharWidth; 
          details += `使用するテキスト幅: ${calculatedTextWidthPt.toFixed(2)} pt (${fontSizeToUse.toFixed(1)}pt で文字数ベース推定)\n`;
        }

        isOverflow = calculatedTextWidthPt > textAreaWidth; 
        const hasManualLineBreaks = (paragraphCount > 1); 
        isOverflow = isOverflow || hasManualLineBreaks; 

        details += `\n--- 判定 (単一行モード) ---\n`; 
        details += `(使用フォントサイズ: ${fontSizeToUse.toFixed(1)} pt)\n`; 
        details += `(計算/推定テキスト幅: ${calculatedTextWidthPt.toFixed(2)} pt)\n`; 
        details += `(テキストエリア内寸幅: ${textAreaWidth.toFixed(2)} pt)\n`; 
        details += `手動改行あり: ${hasManualLineBreaks}\n`; 
        details += `幅オーバー: ${calculatedTextWidthPt > textAreaWidth}\n`; 
        details += `結果: ${isOverflow ? "はみ出し" : "収まっている"}`; 

      } else {
        
        details += `\n--- 判定 (標準モード・段落ごと) ---\n`; 
        let calculatedTotalHeight = 0;

        for (let paraIndex = 0; paraIndex < paragraphCount; paraIndex++) {
            
            let paraRange, paraText, isParaEmpty, spaceAbove, spaceBelow, __cachedEffCount;
            if (__useTextCache) {
                const __pc = __cache.paragraphs[paraIndex];
                paraRange = null;
                paraText = __pc.text;
                isParaEmpty = __pc.isEmpty;
                spaceAbove = __pc.spaceAbove;
                spaceBelow = __pc.spaceBelow;
                __cachedEffCount = __pc.effectiveCount;
            } else {
                paraRange = paragraphs[paraIndex].getRange();
                paraText = paraRange.asString();
                isParaEmpty = paraText.trim().length === 0;
                __cachedEffCount = null;
                const paragraphStyle = paraRange.getParagraphStyle();
                spaceAbove = (paragraphStyle && typeof paragraphStyle.getSpaceAbove === 'function' ? paragraphStyle.getSpaceAbove() : 0) || 0;
                spaceBelow = (paragraphStyle && typeof paragraphStyle.getSpaceBelow === 'function' ? paragraphStyle.getSpaceBelow() : 0) || 0;
            }

            let currentFontSize;
            if (isParaEmpty) {
              if (nonEmptyFontSizeOverride !== null) {
                currentFontSize = 1; 
              } else {
                const run = paraRange.getRuns()[0];
                let fs = run && run.getTextStyle() ? run.getTextStyle().getFontSize() : null;
                currentFontSize = (typeof fs === 'number') ? Math.floor(fs * 2) / 2 : 1;
              }
            } else { 
              if (nonEmptyFontSizeOverride !== null) {
                currentFontSize = nonEmptyFontSizeOverride; 
              } else {
                const run = paraRange.getRuns()[0];
                let fs = run && run.getTextStyle() ? run.getTextStyle().getFontSize() : null;
                currentFontSize = (typeof fs === 'number') ? Math.floor(fs * 2) / 2 : currentBaseFontSize;
              }
            }
             currentFontSize = Math.max(1, currentFontSize);
            details += `  段落${paraIndex + 1}: ${isParaEmpty ? "(空)" : ""} FS=${currentFontSize.toFixed(1)}pt\n`; 

            const estimatedLineHeight = ( currentFontSize * LINE_HEIGHT_FACTOR ) + 0.5; 
            const estimatedFullCharWidth = currentFontSize * AVG_CHAR_WIDTH_FACTOR; 
            const maxEffectiveCharsPerLine = (estimatedFullCharWidth > 0) ? Math.floor(textAreaWidth / estimatedFullCharWidth) : 0;
            let linesInThisParagraph = 0; 
            if (isParaEmpty) {
                linesInThisParagraph = 1; 
            } else {
                const effectiveCount = (__cachedEffCount !== null) ? __cachedEffCount : getEffectiveCharCount(paraText);
                linesInThisParagraph = (maxEffectiveCharsPerLine > 0) ? Math.ceil(effectiveCount / maxEffectiveCharsPerLine) : 1;
                 linesInThisParagraph = Math.max(1, linesInThisParagraph);
            }

            const paragraphSpacing = spaceAbove + spaceBelow;

            const calculatedLinesHeight = linesInThisParagraph * estimatedLineHeight;
            calculatedTotalHeight += calculatedLinesHeight;
            details += `    -> ${linesInThisParagraph}行 * ${estimatedLineHeight.toFixed(2)}pt = ${calculatedLinesHeight.toFixed(2)}pt (文字数ベース推定)\n`;
            if (paraIndex > 0) {
              calculatedTotalHeight += paragraphSpacing;
              details += `    -> 段落間隔 ${paragraphSpacing.toFixed(2)}pt 加算\n`;
            }
        } 

        details += `\n算出された必要な総高さ: ${calculatedTotalHeight.toFixed(2)} pt\n`; 
        isOverflow = calculatedTotalHeight > textAreaHeight;
        details += `必要な高さ (${calculatedTotalHeight.toFixed(2)} pt) > エリアの高さ (${textAreaHeight.toFixed(2)} pt)\n`; 
        details += `結果: ${isOverflow ? "はみ出し" : "収まっている"}`; 
      }

      return { isOverflow: isOverflow, details: details }; 
    } catch (e) {
      Logger.log("isTextOverflowing エラー: " + e.message + " Shape ID: " + (shape ? shape.getObjectId() : 'N/A')); 
      return { isOverflow: false, details: "エラーが発生しました: " + e.message };
    }
  } 

  
  function minimizeEmptyParagraphs(shape) {
    let modified = false; 
    try {
      const textRange = shape.getText(); 
      if (!textRange || textRange.isEmpty()) return false; 
      const paragraphs = textRange.getParagraphs(); 
      for (const paragraph of paragraphs) { 
        const paraRange = paragraph.getRange(); 
        if (paraRange.asString().trim().length === 0) {
          const style = paraRange.getTextStyle(); 
          if (style && style.getFontSize() !== 1) {
            style.setFontSize(1); 
            modified = true; 
          }
        }
      }
    } catch (e) {
      Logger.log("minimizeEmptyParagraphs エラー: " + e.message + " Shape ID: " + (shape ? shape.getObjectId() : 'N/A'));
    }
    return modified; 
  }

  
  function applyNonEmptyFontSize(shape, size) {
    try {
      const textRange = shape.getText(); 
      if (!textRange || textRange.isEmpty()) return; 
      const paragraphs = textRange.getParagraphs(); 
      for (const paragraph of paragraphs) { 
        const paraRange = paragraph.getRange(); 
        if (paraRange.asString().trim().length > 0) {
           const style = paraRange.getTextStyle(); 
           if (style) { 
               style.setFontSize(size);
           }
        }
      }
    } catch (e) {
      Logger.log("applyNonEmptyFontSize エラー: " + e.message + " Shape ID: " + (shape ? shape.getObjectId() : 'N/A'));
    }
  }

  
  function findOptimalFontSize(shape, startFontSize, preCalculatedWidthPt = null, originalBaseFontSize = null, widthOverride = null, heightOverride = null) {
    if (typeof startFontSize !== 'number' || startFontSize < 10) {
         Logger.log(`findOptimalFontSize: 無効な開始サイズ ${startFontSize}pt のため検索をスキップ`);
        return null; 
    }
    
    
    const MIN_FONT_SIZE = (widthOverride !== null) ? 1 : 10;

    for (let testSize = startFontSize - 0.5; testSize >= MIN_FONT_SIZE; testSize -= 0.5) {
      
      const result = isTextOverflowing(shape, testSize, preCalculatedWidthPt, originalBaseFontSize, widthOverride, heightOverride);
      if (!result.isOverflow) { 
        return testSize; 
      }
    }
    
    
    return null; 
  }


  

  let logDetails = ""; 
  let finalAppliedFontSize = null; 

  try {
    
    if (!shape || typeof shape.getText !== 'function' || shape.getText().isEmpty()) { 
      return { log: "テキストが空のため、処理をスキップしました。", finalSize: null }; 
    }

    
    logDetails += "--- 事前チェック (面積比較) ---\n"; 
    const textRange = shape.getText(); 
    const originalText = textRange.asString(); 
    const totalCharCount = originalText.length;
    const initialBaseFontSize = __getInitialNonEmptyFontSize();
    finalAppliedFontSize = initialBaseFontSize;

    if (initialBaseFontSize === null) { 
      logDetails += "エラー: 基準フォントサイズが取得できず、事前チェックをスキップします。\n\n";
    } else {
      
      const boxWidth = __getBoxW(widthOverride);
      const boxHeight = __getBoxH(heightOverride);
      
      const textAreaWidth = boxWidth - (PADDING_LEFT_RIGHT * 2);
      const textAreaHeight = boxHeight - (PADDING_TOP_BOTTOM * 2);
      const textAreaArea = textAreaWidth * textAreaHeight;

      if (textAreaArea <= 0) { 
        logDetails += "警告: テキストエリアの内寸面積が0以下です。詳細チェックに進みます。\n\n";
      } else {
        const estimatedCharArea = initialBaseFontSize * initialBaseFontSize;
        const estimatedTotalCharArea = estimatedCharArea * totalCharCount;
        const thresholdArea = textAreaArea * 0.4; 

        logDetails += `基準フォントサイズ: ${initialBaseFontSize.toFixed(1)} pt\n`; 
        logDetails += `総文字数: ${totalCharCount} 文字\n`;
        logDetails += `推定1文字の面積 (FS*FS): ${estimatedCharArea.toFixed(2)} pt^2\n`;
        logDetails += `推定テキスト総面積: ${estimatedTotalCharArea.toFixed(2)} pt^2\n`;
        logDetails += `推定テキストエリア内寸面積: ${textAreaArea.toFixed(2)} pt^2\n`;
        logDetails += `判定閾値 (エリアの40%): ${thresholdArea.toFixed(2)} pt^2\n`;
        
        if (estimatedTotalCharArea <= thresholdArea) {
          logDetails += "結果: テキスト総面積がエリアの40%以下のため、詳細チェックをスキップします。✅\n";
          return { log: logDetails, finalSize: initialBaseFontSize }; 
        } else {
          logDetails += "結果: テキスト総面積がエリアの40%を超えているため、詳細チェックに進みます。\n\n";
        }
      }
    }
    


    
    
    const initialResult = isTextOverflowing(shape, null, preCalculatedWidthPt, initialBaseFontSize, widthOverride, heightOverride);
    logDetails += "--- 詳細診断 (実行前の状態) --- \n" + initialResult.details + "\n\n"; 

    
    if (!initialResult.isOverflow) {
      logDetails += "--- 実行結果 --- \nはみ出しは検出されませんでした。調整は不要です。 ✅"; 
      return { log: logDetails, finalSize: initialBaseFontSize }; 
    }

    
    logDetails += "--- 調整実行 --- \nはみ出しを検出しました 🚨\n\n";
    const currentActualFontSize = __getInitialNonEmptyFontSize();
     if (currentActualFontSize === null) { 
        logDetails += "エラー: 現在のフォントサイズが取得できず、調整を実行できません。";
        return { log: logDetails, finalSize: null}; 
    }

    
    const isShortBox = _isShapeShortBox(shape, initialBaseFontSize || currentActualFontSize, heightOverride);

    
    if (isShortBox) {
      
      logDetails += "（単一行モードとして処理します）\n"; 
      
      const optimalSize = findOptimalFontSize(shape, currentActualFontSize, preCalculatedWidthPt, initialBaseFontSize, widthOverride, heightOverride);

      if (optimalSize !== null) { 
        shape.getText().getTextStyle().setFontSize(optimalSize);
        finalAppliedFontSize = optimalSize; 
        logDetails += `調整： 全体のフォントサイズを ${optimalSize.toFixed(1)} pt に縮小しました。\n`; 

        
        const finalResult = isTextOverflowing(shape, optimalSize, preCalculatedWidthPt, initialBaseFontSize, widthOverride, heightOverride);
        logDetails += "\n--- 調整後の計算詳細 ---\n" + (finalResult.details || "計算失敗"); 
      } else {
        
        const MIN_FONT_SIZE = (widthOverride !== null) ? 1 : 10;
        shape.getText().getTextStyle().setFontSize(MIN_FONT_SIZE); 
        finalAppliedFontSize = MIN_FONT_SIZE; 
        logDetails += `調整： フォントサイズ ${MIN_FONT_SIZE} pt でもテキストが収まりませんでした。\n`; 
      }
    } else {
      
      logDetails += "（標準モードとして処理します）\n"; 
      const modifiedEmpty = minimizeEmptyParagraphs(shape);
      if (modifiedEmpty) {
        logDetails += "ステップ1: 空の段落のフォントサイズを 1pt に縮小しました。\n"; 
      } else {
        logDetails += "ステップ1: 対象となる空の段落はありませんでした。\n"; 
      }

      
      const resultAfterStep1 = isTextOverflowing(shape, null, preCalculatedWidthPt, initialBaseFontSize, widthOverride, heightOverride);
      logDetails += "ステップ1実行後の再計算結果：\n" + (resultAfterStep1.details || "計算失敗") + "\n\n"; 

      if (!resultAfterStep1.isOverflow) {
        logDetails += "--- 実行結果 --- \n空の段落の縮小のみで、はみ出しが解消しました。✅"; 
        return { log: logDetails, finalSize: initialBaseFontSize }; 
      }

      logDetails += "ステップ2: 文字を含む段落のフォントサイズを縮小します。\n"; 
      const currentNonEmptySizeAfterStep1 = getNonEmptyFontSize(shape); 
       if (currentNonEmptySizeAfterStep1 === null) { 
           logDetails += "エラー: 現在のフォントサイズが取得できず、ステップ2を実行できません。";
           return { log: logDetails, finalSize: null}; 
       }

      
      const optimalSize = findOptimalFontSize(shape, currentNonEmptySizeAfterStep1, preCalculatedWidthPt, initialBaseFontSize, widthOverride, heightOverride);

      if (optimalSize !== null) { 
        applyNonEmptyFontSize(shape, optimalSize);
        finalAppliedFontSize = optimalSize; 
        logDetails += `調整： 文字のある段落を ${optimalSize.toFixed(1)} pt に縮小しました。\n`; 

        
        const finalResult = isTextOverflowing(shape, null, preCalculatedWidthPt, initialBaseFontSize, widthOverride, heightOverride);
        logDetails += "\n--- 調整後の計算詳細 ---\n" + (finalResult.details || "計算失敗"); 
      } else {
        
        const MIN_FONT_SIZE = (widthOverride !== null) ? 1 : 10;
        applyNonEmptyFontSize(shape, MIN_FONT_SIZE); 
        finalAppliedFontSize = MIN_FONT_SIZE; 
        logDetails += `調整： 文字のある段落を ${MIN_FONT_SIZE}pt にしてもテキストが収まりませんでした。\n`; 
      }
    }

    return { log: logDetails, finalSize: finalAppliedFontSize };

  } catch (e) {
    
    if (e.message.includes("getWidth is not a function") || e.message.includes("getHeight is not a function") || e.message.includes("shape.getWidth is not a function")) {
        return { log: logDetails + "\n\n--- 致命的なエラー --- \n" + "TableCellが渡されましたが、幅または高さのOverride引数がありませんでした。 " + e.message, finalSize: null };
    }
    Logger.log("adjustShapeText_External 致命的エラー: " + e.message + " Shape ID: " + (shape ? shape.getObjectId() : 'N/A') + "\nStack: " + e.stack); 
    return { log: logDetails + "\n\n--- 致命的なエラー --- \n" + e.message, finalSize: null };
  }
} 











const METADATA_BOX_TITLE = 'METADATA_STORAGE_BOX'; 
const TARGET_LAYOUT_ID = 'p10'; 
const MAX_HISTORY_COUNT = 5;


function saveGenerationState(presentationId, slideData, settings) {
  Logger.log(`[Metadata Debug] --- saveGenerationState (Latest Only & Cleaned) 開始 (ID: ${presentationId}) ---`);
  try {
    if (!presentationId) return;
    
    const presentation = SlidesApp.openById(presentationId);
    const box = findOrSetupMetadataBox(presentation);
    
    if (!box) return;

    
    let safeSlideData = [];
    let safeSettings = {};

    try {
       
       safeSettings = JSON.parse(JSON.stringify(settings));
       delete safeSettings.creditImageBase64;
       delete safeSettings.triangleArrows;

       
       safeSlideData = JSON.parse(JSON.stringify(slideData));
       
       safeSlideData.forEach(slide => {
         
         if (slide.ghostImageBase64) delete slide.ghostImageBase64;
         
         
         if (slide.type === 'splitImage' && slide.image) {
             
             
             if (slide.image.backgroundImage && slide.image.backgroundImage.length > 100) {
                 slide.image.backgroundImage = "[Saved]"; 
             }

             
             if (slide.image.layers && Array.isArray(slide.image.layers)) {
                 slide.image.layers.forEach(layer => {
                     
                     if (layer.data && layer.data.length > 100) delete layer.data;
                     if (layer.dataBase64 && layer.dataBase64.length > 100) delete layer.dataBase64;
                     
                     
                     if (layer.key && layer.key.length > 500) { 
                         layer.key = "[Saved]"; 
                     }
                     
                     
                 });
             }
             
             
             
             
         }
       });

    } catch (cleanError) {
       Logger.log(`[Metadata Debug] ❌ データの軽量化中にエラー: ${cleanError.message}`);
       return;
    }
    
    
    const newEntry = {
      timestamp: new Date().getTime(),
      dateStr: Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy/MM/dd HH:mm:ss'),
      slideData: safeSlideData,
      settings: safeSettings
    };
    
    
    const history = [newEntry];
    
    
    try {
      const METADATA_SIZE_LIMIT = 500000; 
      let jsonString = JSON.stringify({ history: history });

      
      if (jsonString.length > METADATA_SIZE_LIMIT) {
        const __origSize = jsonString.length;
        try {
          safeSlideData.forEach(function (slide) {
            if (slide && slide.type === 'splitImage' && slide.image
                && typeof slide.image.data === 'string' && slide.image.data.length > 100) {
              slide.image.data = '[TooLarge]';
            }
          });
          jsonString = JSON.stringify({ history: history });
          Logger.log('[Metadata] 履歴が大きすぎるため原画像を除いて保存(' + __origSize + '→' + jsonString.length + ')');
        } catch (eShrink) {
          Logger.log('[Metadata] 原画像除去中にエラー(元データで書き込みを試みます): ' + eShrink.message);
        }
      }

      
      box.setDescription(jsonString);
      
      const checkId = box.getObjectId();
      Logger.log(`[Metadata Debug] ✅ メタデータの保存に成功 (文字数: ${jsonString.length})`);
      
    } catch (writeError) {
      Logger.log(`[Metadata Debug] ❌ 書き込み失敗: ${writeError.message}`);
    }
    
  } catch (e) {
    Logger.log(`[Metadata Debug] ❌ 全体エラー: ${e.message}`);
  }
}






function loadLatestGenerationState(presentationId) {
  Logger.log(`[Metadata Debug] --- loadLatestGenerationState 開始 (ID: ${presentationId}) ---`);
  try {
    if (!presentationId) throw new Error("IDが指定されていません");
    
    const presentation = SlidesApp.openById(presentationId);
    const box = findOrSetupMetadataBox(presentation);
    
    const description = box.getDescription();
    if (!description || description.trim() === "") {
      throw new Error("履歴データが空です。");
    }
    
    const parsed = JSON.parse(description);
    if (!parsed.history || parsed.history.length === 0) {
      throw new Error("有効な履歴が見つかりません。");
    }
    
    
    const latestState = parsed.history[parsed.history.length - 1];
    Logger.log(`[Metadata Debug] ✅ 最新の履歴データを取得しました (日付: ${latestState.dateStr})`);
    
    return {
      status: 'success',
      data: latestState,
      url: presentation.getUrl()
    };
    
  } catch (e) {
    Logger.log(`[Metadata Debug] ❌ 読み込みエラー: ${e.message}`);
    return {
      status: 'error',
      message: e.message
    };
  }
}


function findOrSetupMetadataBox(presentation) {
  Logger.log(`[Metadata Debug] ボックス探索開始... ターゲットレイアウト: '${TARGET_LAYOUT_ID}', ターゲットタイトル: '${METADATA_BOX_TITLE}'`);

  const layouts = presentation.getLayouts();
  let targetLayout = null;

  
  for (const layout of layouts) {
    if (layout.getObjectId() === TARGET_LAYOUT_ID) {
      targetLayout = layout;
      Logger.log(`[Metadata Debug] ✅ レイアウト '${TARGET_LAYOUT_ID}' が見つかりました。`);
      break;
    }
  }

  
  if (!targetLayout) {
    Logger.log(`[Metadata Debug] ⚠️ レイアウト '${TARGET_LAYOUT_ID}' が見つかりません。layouts[0] を使用します。`);
    targetLayout = layouts[0];
  }

  const shapes = targetLayout.getShapes();
  Logger.log(`[Metadata Debug] レイアウト上のシェイプ数: ${shapes.length}`);
  
  
  
  
  for (let i = 0; i < shapes.length; i++) {
    
    
    
    if (shapes[i].getTitle() === METADATA_BOX_TITLE) {
      Logger.log(`[Metadata Debug] ✅ 戦略1: 既存のID付きボックスが見つかりました (Index: ${i})`);
      
      
      try {
        const _b = shapes[i];
        const _txt = (_b.getText && _b.getText().asString) ? _b.getText().asString() : '';
        if (_txt.indexOf('システム用データ') !== -1) {
          _b.getText().setText('');
          try { _b.getBorder().setTransparent(); } catch (e) {}
          Logger.log('[Metadata Debug] 旧視認ラベルを非表示化しました。');
        }
      } catch (e) { Logger.log('[Metadata Debug] ラベル非表示化スキップ: ' + e.message); }
      return shapes[i];
    }
  }

  
  
  
  Logger.log(`[Metadata Debug] 戦略1でボックスが見つかりませんでした。戦略2: 流用可能なテキストボックスを探します...`);
  for (let i = 0; i < shapes.length; i++) {
    const shape = shapes[i];
    
    
    if (shape.getShapeType() === SlidesApp.ShapeType.TEXT_BOX) {
      Logger.log(`[Metadata Debug] ✅ 戦略2: テキストボックスが見つかりました (Index: ${i})。これをメタデータ保存用に設定します。`);
      
      
      shape.setTitle(METADATA_BOX_TITLE);
      
      
      
      
      
      
      
      
      
      
      
      
      
      

      return shape;
    }
  }

  
  
  
  Logger.log(`[Metadata Debug] 戦略2も失敗しました。戦略3: 新規ボックスを作成します。`);
  return createMetadataBox(targetLayout);
}


function createMetadataBox(layout) {
  const box = layout.insertShape(SlidesApp.ShapeType.TEXT_BOX, 0, 0, 10, 10);
  box.setTitle(METADATA_BOX_TITLE);
  
  
  
  try { box.getBorder().setTransparent(); } catch (e) {}
  Logger.log(`[Metadata Debug] ✅ 新規ボックスを作成しました(非表示)。`);
  return box;
}










function addToUserHistory(presentationId) {
  Logger.log(`[Debug] addToUserHistory called with ID: ${presentationId}`);

  if (!presentationId) return;

  try {
    const userProps = PropertiesService.getUserProperties();
    const HISTORY_KEY = 'slide_history_v1';
    let history = [];
    
    const json = userProps.getProperty(HISTORY_KEY);
    if (json) {
      try { history = JSON.parse(json); } catch(e) { history = []; }
    }

    const file = DriveApp.getFileById(presentationId);
    const lastUpdated = file.getLastUpdated();
    
    
    let editorEmail = 'Unknown';
    try {
      const user = file.getLastModifyingUser();
      if (user) {
        editorEmail = user.getEmail();
      } else {
        
        const owner = file.getOwner();
        if (owner) {
          editorEmail = owner.getEmail();
        }
      }
    } catch (e) {
      Logger.log('ユーザー情報の取得に失敗: ' + e.message);
      
      try { editorEmail = file.getOwner().getEmail(); } catch(e2) {}
    }
    



    const formattedDate = Utilities.formatDate(lastUpdated, Session.getScriptTimeZone(), 'yyyy/MM/dd HH:mm');
    

    const meta = {
      id: presentationId,
      name: file.getName(),
      lastModified: formattedDate, 
      timestamp: lastUpdated.getTime(), 
      editor: editorEmail,
      url: file.getUrl(),
      icon: file.getMimeType() === MimeType.GOOGLE_SLIDES ? 'slide' : 'file'
    };



    
    history = history.filter(item => item.id !== presentationId);
    history.unshift(meta);
    if (history.length > 10) history = history.slice(0, 10);

    userProps.setProperty(HISTORY_KEY, JSON.stringify(history));
    return meta;

  } catch (e) {
    Logger.log(`履歴保存エラー: ${e.message}`);
    return null;
  }
}


function removeFromUserHistory(presentationId) {
  try {
    if (!presentationId) return;
    const HISTORY_KEY = 'slide_history_v1';
    const userProps = PropertiesService.getUserProperties();
    const raw = userProps.getProperty(HISTORY_KEY);
    if (!raw) return;
    let history = JSON.parse(raw);
    if (!Array.isArray(history)) return;
    const filtered = history.filter(item => item && item.id !== presentationId);
    if (filtered.length !== history.length) {
      userProps.setProperty(HISTORY_KEY, JSON.stringify(filtered));
    }
  } catch (e) {
    Logger.log('履歴除去エラー(無視): ' + e.message);
  }
}





function getUserHistory() {
  Logger.log('[Debug] getUserHistory called'); 

  try {
    const userProps = PropertiesService.getUserProperties();
    const HISTORY_KEY = 'slide_history_v1';
    const json = userProps.getProperty(HISTORY_KEY);
    
    Logger.log(`[Debug] Retrieved history JSON: ${json ? json.substring(0, 100) + '...' : 'null'}`); 

    if (!json) {
      Logger.log('[Debug] History is empty or null.');
      return [];
    }
    
    let parsed = [];
    try {
      parsed = JSON.parse(json);
      Logger.log(`[Debug] Successfully parsed history. Items count: ${parsed.length}`); 
    } catch (parseError) {
      Logger.log(`[Debug] JSON parse error in getUserHistory: ${parseError.message}`); 
      return [];
    }

    return parsed;

  } catch (e) {
    Logger.log(`[Debug] CRITICAL ERROR in getUserHistory: ${e.message}`); 
    return [];
  }
}







function processImagesAndUpload(imagesToUpload, slideDataString, settings) {
  
  if (!imagesToUpload || imagesToUpload.length === 0) {
    return {
      updatedSlideDataString: slideDataString,
      imageFolderId: null,
      error: null
    };
  }

  try {
    
    const parentFolderId = settings.driveFolderId || 'root';
    const parentFolder = DriveApp.getFolderById(parentFolderId);
    const tempFolderName = `temp_images_${new Date().getTime()}`;
    const tempFolder = parentFolder.createFolder(tempFolderName);
    const tempFolderId = tempFolder.getId();

    const imageIdMap = {}; 

    
    imagesToUpload.forEach((image, index) => {
      
      const base64Data = image.data.split(',')[1];
      const decodedData = Utilities.base64Decode(base64Data);
      
      const contentType = image.data.match(/^data:(.*?);/)[1] || 'image/jpeg';
      
      const fileName = `image_${index}_${image.key.substring(0,20).replace(/[^a-zA-Z0-9]/g,'_')}.png`;
      const blob = Utilities.newBlob(decodedData, contentType, fileName);
      
      const file = tempFolder.createFile(blob);
      const fileId = file.getId();
      
      
      imageIdMap[image.key] = fileId; 
    });

    Logger.log("imageIdMap created: " + JSON.stringify(Object.keys(imageIdMap))); 

    
    let slideData = JSON.parse(slideDataString);
    const updatedSlideData = slideData.map(slide => {
      
      
      let currentImageKey = null;

      const targetKeys = ['image', 'backgroundImage', 'foregroundImage'];
      
      targetKeys.forEach(key => {
        if (slide[key]) {
           if (typeof slide[key] === 'string') {
              
              currentImageKey = slide[key]; 
              
              
           } else if (typeof slide[key] === 'object' && slide[key].data) {
              
              try { currentImageKey = JSON.stringify(slide[key]); } catch(e){}
           }
           
           
           if (currentImageKey && imageIdMap[currentImageKey]) {
             slide[key] = imageIdMap[currentImageKey];
           }
        }
        if (key === 'image' && slide.type === 'splitImage' && slide.image && slide.image.layers) {
            
            if (slide.image.backgroundImage && imageIdMap[slide.image.backgroundImage]) {
                slide.image.backgroundImage = imageIdMap[slide.image.backgroundImage];
            }
            
            slide.image.layers.forEach(layer => {
                if (layer.key && imageIdMap[layer.key]) {
                    layer.key = imageIdMap[layer.key];
                }
            });
        }
      });

      if (slide.image) {
          if (typeof slide.image === 'string') {
              
              currentImageKey = slide.image;
          } else if (typeof slide.image === 'object' && slide.image !== null) {
              
              try {
                currentImageKey = JSON.stringify(slide.image);
              } catch (e) {
                Logger.log("Error stringifying slide.image object: " + e);
                
              }
          }
      }

      Logger.log("Processing slide, currentImageKey: " + (currentImageKey ? currentImageKey.substring(0,50) + "..." : "null")); 

      
      if (currentImageKey && imageIdMap[currentImageKey]) {
          Logger.log("Found match for key! Replacing with fileId: " + imageIdMap[currentImageKey]); 
          return { ...slide, image: imageIdMap[currentImageKey] };
      } else {
          if (currentImageKey) {
            Logger.log("No match found for key in imageIdMap."); 
          }
          
          return slide; 
      }
      

    });

    
    const finalJsonString = JSON.stringify(updatedSlideData);
    Logger.log("Updated slide data string (first 200 chars): " + finalJsonString.substring(0, 200)); 
    return {
      updatedSlideDataString: finalJsonString,
      imageFolderId: tempFolderId, 
      error: null
    };

  } catch (e) {
    Logger.log("Error in processImagesAndUpload: " + e + "\nStack: " + e.stack); 
    return {
      updatedSlideDataString: slideDataString,
      imageFolderId: null,
      error: 'サーバーサイドで画像のアップロードまたは処理中にエラーが発生しました: ' + e.message
    };
  }
}


function deleteFolderById(folderId) {
  try {
    if (folderId) {
      const folder = DriveApp.getFolderById(folderId);
      folder.setTrashed(true);
      Logger.log('Successfully moved folder to trash: ' + folderId);
    }
  } catch (e) {
    Logger.log('Failed to delete folder ' + folderId + '. Error: ' + e.message);
    
  }
}




function _activationKey_(email) {
  var normalized = String(email).trim().toLowerCase();
  var digest = Utilities.computeDigest(
    Utilities.DigestAlgorithm.SHA_256,
    normalized,
    Utilities.Charset.UTF_8
  );
  var hex = '';
  for (var i = 0; i < digest.length; i++) {
    var b = (digest[i] + 256) % 256;
    var h = b.toString(16);
    if (h.length < 2) h = '0' + h;
    hex += h;
  }
  return 'act_' + hex.substring(0, 16);
}


function checkUserActivation() {
  const SCRIPT_PROPERTY_KEY = 'ACTIVATED_USER_LIST';
  let userEmail = '';
  let isActivated = false;

  try {
    
    userEmail = Session.getEffectiveUser().getEmail();

    const scriptProperties = PropertiesService.getScriptProperties();

    
    const newKey = _activationKey_(userEmail);
    if (scriptProperties.getProperty(newKey)) {
      isActivated = true;
    } else {
      
      const jsonString = scriptProperties.getProperty(SCRIPT_PROPERTY_KEY);
      if (jsonString) {
        const activatedUsers = JSON.parse(jsonString);
        if (Array.isArray(activatedUsers) && activatedUsers.includes(userEmail)) {
          isActivated = true;
          
          try {
            scriptProperties.setProperty(newKey, '1');
          } catch (eMig) {
            Logger.log('checkUserActivation 新キー移行失敗: ' + eMig.message);
          }
        }
      }
    }
  } catch (e) {
    Logger.log('checkUserActivation エラー: ' + e.message);
    
    isActivated = false;
  }

  
  return {
    isActivated: isActivated,
    userEmail: userEmail
  };
}


function activateUser(userEmail) {
  if (!userEmail) {
    return { status: 'error', message: 'ユーザーIDがありません。' };
  }

  var lock = null;
  try {
    const scriptProperties = PropertiesService.getScriptProperties();
    
    const newKey = _activationKey_(userEmail);

    
    try {
      lock = LockService.getScriptLock();
      if (!lock.tryLock(5000)) {
        Logger.log('activateUser ロック取得失敗(5s)。ロック無しで続行: ' + userEmail);
        lock = null;
      }
    } catch (eLk) {
      Logger.log('activateUser ロック取得例外。ロック無しで続行: ' + eLk.message);
      lock = null;
    }

    if (scriptProperties.getProperty(newKey)) {
      Logger.log('ユーザーは既にアクティベート済みです: ' + userEmail);
    } else {
      scriptProperties.setProperty(newKey, '1');
      Logger.log('新規ユーザーをアクティベートしました: ' + userEmail);
    }

    return { status: 'success', message: 'アクティベートが完了しました。' };

  } catch (e) {
    Logger.log('activateUser エラー: ' + e.message);
    return { status: 'error', message: 'サーバーエラー: ' + e.message };
  } finally {
    if (lock) {
      try { lock.releaseLock(); } catch (eRl) {}
    }
  }
}




function convertJsonToSvgBatch(jsonStrings) {
  if (!Array.isArray(jsonStrings)) {
    
    Logger.log('convertJsonToSvgBatch Error: Input is not an array.');
    
    return []; 
    
    
  }
  
  const results = jsonStrings.map(jsonText => {
    
    try {
      
      const svgString = jsonToSVG(jsonText);
      
      
      return {
        key: jsonText,   
        svg: svgString, 
        error: null
      };
    } catch (e) {
      
      Logger.log(`convertJsonToSvgBatch Error for key ${jsonText.substring(0, 50)}...: ${e.message}`);
      
      return {
        key: jsonText,
        svg: null,
        error: e.message 
      };
    }
  });
  
  
  return results; 
}




function jsonToSVG(jsonText) {
  let config;
  try {
    
    config = JSON.parse(jsonText);
  } catch (e) {
    Logger.log(`JSON Parse Error: ${e.message}`);
    throw new Error(`無効なJSONが提供されました: ${e.message}`);
  }

  
  if (!config || !config.chartType) {
    throw new Error("無効なJSON構造です: 'chartType' キーが見つかりません。");
  }

  const chartType = config.chartType;

  try {
    
    const template = getTemplate(chartType);
    
    
    const svgOutput = injectJsonIntoSvg(template, jsonText);
    
    
    return svgOutput;

  } catch (e) {
    Logger.log(`Error in jsonToSVG: ${e.message}`);
    throw e; 
  }
}


function injectJsonIntoSvg(template, jsonText) {
  
  const regex = /(<script id="chart-json-data"[^>]*>)([\s\S]*?)(<\/script>)/;
  
  if (!regex.test(template)) {
    throw new Error("SVGテンプレートにターゲットのscriptタグ (id='chart-json-data') が見つかりません。");
  }

  
  
  
  
  const replacedSvg = template.replace(regex, `$1\n    ${jsonText}\n    $3`);
  
  return replacedSvg;
}


function getTemplate(chartType) {
  switch (chartType) {
    case "combo":
      return COMBO_CHART_TEMPLATE;
    case "multi-line":
      return MULTI_LINE_CHART_TEMPLATE;
    case "stacked-bar":
      return STACKED_BAR_CHART_TEMPLATE;
    case "bar":
      return BAR_CHART_TEMPLATE;
    case "100-stacked-bar":
      return PERCENT_STACKED_BAR_CHART_TEMPLATE;
    case "line":
      return LINE_CHART_TEMPLATE;
    case "donut":
      return DONUT_CHART_TEMPLATE;
    default:
      throw new Error(`不明な chart type です: '${chartType}'`);
  }
}


function getTriangleArrowSvgTemplate() {
  try {
    
    return TRIANGLE_ARROW_SVG_TEMPLATE;
  } catch (e) {
    Logger.log(`Error retrieving TRIANGLE_ARROW_SVG_TEMPLATE: ${e.message}`);
    
    return ''; 
  }
}




const TRIANGLE_ARROW_SVG_TEMPLATE = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   width="500px"
   height="500px"
   viewBox="0 0 500 500"
   version="1.1"
   id="svg1"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <defs id="defs1">
    <style id="variable-controller">
      :root {
        /* ■■■ 基本形状: 終点の座標 (始点からの相対px) ■■■ */
        /* (Y座標は上が正になるよう変更されました) */
        --end-x: {{END_X}};
        --end-y: {{END_Y}};

        /* ■■■ 基本形状: 曲がりの強さ（-100〜100で制限されます）■■■ */
        --curve-intensity: {{CURVE_INTENSITY}};
        /* ■■■ 基本形状: 回転角度 (度) ■■■ */
        --rotation: {{ROTATION}};
        /* ■■■ 基本形状: 反転 (0 = しない, 1 = する) ■■■ */
        --flip-x: {{FLIP_X}};
        --flip-y: {{FLIP_Y}};

        /* ■■■ NEW: ゴーストエフェクト設定 ■■■ */
        --ghost-base-frequency: {{GHOST_BASE_FREQUENCY}};
        /* colorType: 'gray', 'red', 'green', 'blue', 'fullcolor' */
        --ghost-color-type: '{{GHOST_COLOR_TYPE}}';
        --ghost-alpha-slope: {{GHOST_ALPHA_SLOPE}};
        --ghost-contrast-slope: {{GHOST_CONTRAST_SLOPE}};
        --ghost-front-color: '{{GHOST_FRONT_COLOR}}';
        --ghost-front-alpha: {{GHOST_FRONT_ALPHA}};
      }
    </style>
    <filter id="ghostNoiseFilter"
       filterUnits="userSpaceOnUse"
       x="0" y="0" width="500" height="500">
      <feTurbulence id="ghostTurbulence"
          type="fractalNoise"
          baseFrequency="{{GHOST_BASE_FREQUENCY}}"
          numOctaves="3"
          seed="0"
          result="noise"/>
      <feColorMatrix id="ghostColorMatrix" in="noise" type="matrix"
          values="0.33 0.33 0.33 0 0 0.33 0.33 0.33 0 0 0.33 0.33 0.33 0 0 0 0 0 1 0"
          result="coloredNoise"/>
      <feComposite id="ghostComposite" in="coloredNoise" in2="SourceAlpha" operator="in" result="maskedNoise"/>
      <feComponentTransfer in="maskedNoise" result="finalTexture">
          <feFuncR id="ghostFuncR" type="linear" slope="{{GHOST_CONTRAST_SLOPE}}" intercept="0"/>
          <feFuncG id="ghostFuncG" type="linear" slope="{{GHOST_CONTRAST_SLOPE}}" intercept="0"/>
          <feFuncB id="ghostFuncB" type="linear" slope="{{GHOST_CONTRAST_SLOPE}}" intercept="0"/>
          <feFuncA id="ghostFuncA" type="linear" slope="{{GHOST_ALPHA_SLOPE}}" intercept="0"/>
      </feComponentTransfer>
      <feMerge>
        <feMergeNode in="finalTexture"/>
      </feMerge>
    </filter>
    <mask id="arrowMask">
      <g id="arrow-mask-layer" style="fill:white; stroke:white;">
        <path
           id="arrow-head-mask"
           d="M 0 -43.30127 L -25 0 H 25 Z"
           style="fill-opacity:1; stroke:none;"
           transform="translate(103.28583, 117.1908) rotate(0)" />
        <path
           id="arrow-stem-curve-mask"
           d=""
           fill="none"
           stroke-width="21.566"
           stroke-linecap="butt"
           stroke-linejoin="bevel"
           stroke-opacity="1" />
      </g>
    </mask>
  </defs>
  <g id="layer1">
    <g id="arrow-ghost-layer" filter="url(#ghostNoiseFilter)">
        <path
           id="arrow-head-ghost"
           d="M 0 -43.30127 L -25 0 H 25 Z"
           style="fill:#000000;fill-opacity:1;stroke:none;"
           transform="translate(103.28583, 117.1908) rotate(0)" />
        <path
           id="arrow-stem-curve-ghost"
           d=""
           fill="none"
           stroke="#000000"
           stroke-width="21.566"
           stroke-linecap="butt"
           stroke-linejoin="bevel"
           stroke-opacity="1" />
    </g>
    <g id="arrow-front-layer">
        <path
           id="arrow-head-front"
           d="M 0 -43.30127 L -25 0 H 25 Z"
           style="fill:{{GHOST_FRONT_COLOR}};fill-opacity:{{GHOST_FRONT_ALPHA}};stroke:none;"
           transform="translate(103.28583, 117.1908) rotate(0)" />
        <path
           id="arrow-stem-curve-front"
           d=""
           fill="none"
           stroke="{{GHOST_FRONT_COLOR}}"
           stroke-width="21.566"
           stroke-linecap="butt"
           stroke-linejoin="bevel"
           stroke-opacity="{{GHOST_FRONT_ALPHA}}" />
    </g>
  </g>
  <script>
    // <![CDATA[
      (function() {
        const root = document.documentElement;
        const arrowHeadGhost = document.getElementById('arrow-head-ghost');
        const curvePathGhost = document.getElementById('arrow-stem-curve-ghost');
        const arrowHeadFront = document.getElementById('arrow-head-front');
        const curvePathFront = document.getElementById('arrow-stem-curve-front');
        const feTurbulence = document.getElementById('ghostTurbulence');
        const feColorMatrix = document.getElementById('ghostColorMatrix');
        const feComposite = document.getElementById('ghostComposite');
        const feFuncR = document.getElementById('ghostFuncR');
        const feFuncG = document.getElementById('ghostFuncG');
        const feFuncB = document.getElementById('ghostFuncB');
        const feFuncA = document.getElementById('ghostFuncA');
        const startX = 100; const startY = 100;
        const canvasMin = 0; const canvasMax = 500;
        const margin = 10;
        const endPointMin = canvasMin + margin; const endPointMax = canvasMax - margin;

        function getCssVarNum(name, fallback) { const value = getComputedStyle(root).getPropertyValue(name).trim(); const parsedValue = parseFloat(value); return isNaN(parsedValue) ? fallback : parsedValue; }
        function getCssVarString(name, fallback) { const value = getComputedStyle(root).getPropertyValue(name).trim(); return value.replace(/^['"]|['"]$/g, '') || fallback; }

        function updatePath() {
          const relativeX_base = getCssVarNum('--end-x', 0);
          let relativeY_base = getCssVarNum('--end-y', 60);
          let intensity_base = getCssVarNum('--curve-intensity', 0);
          relativeY_base = relativeY_base * -1;
          intensity_base = Math.max(-100, Math.min(intensity_base, 100));
          const rotation = getCssVarNum('--rotation', 0);
          const flipX = (getCssVarNum('--flip-x', 0) === 1) ? -1 : 1;
          const flipY = (getCssVarNum('--flip-y', 0) === 1) ? -1 : 1;

          const baseFrequency = getCssVarNum('--ghost-base-frequency', 0.01);
          const colorType = getCssVarString('--ghost-color-type', 'gray');
          const alphaSlope = getCssVarNum('--ghost-alpha-slope', 0.50);
          const contrastSlope = getCssVarNum('--ghost-contrast-slope', 1.0);
          const frontColor = getCssVarString('--ghost-front-color', '#000000');
          const frontAlpha = getCssVarNum('--ghost-front-alpha', 0.15);

          const rX_flipped = relativeX_base * flipX;
          const rY_flipped = relativeY_base * flipY;
          const intensity = intensity_base * flipX * flipY;
          const rad = rotation * Math.PI / 180;
          const cos_r = Math.cos(rad); const sin_r = Math.sin(rad);
          const relativeX = rX_flipped * cos_r - rY_flipped * sin_r;
          const relativeY = rX_flipped * sin_r + rY_flipped * cos_r;
          let endX = startX + relativeX; let endY = startY + relativeY;
          endX = Math.max(endPointMin, Math.min(endX, endPointMax)); endY = Math.max(endPointMin, Math.min(endY, endPointMax));
          const midX = (startX + endX) / 2; const midY = (startY + endY) / 2;
          const vx = endX - startX; const vy = endY - startY; const len = Math.sqrt(vx * vx + vy * vy);
          let controlX, controlY;
          if (len === 0 || intensity === 0) { controlX = midX; controlY = midY; } else { const normX = -vy / len; const normY = vx / len; controlX = midX + normX * intensity; controlY = midY + normY * intensity; }
          const d = \`M \${startX},\${startY} Q \${controlX},\${controlY} \${endX},\${endY}\`;
          curvePathGhost.setAttribute('d', d); curvePathFront.setAttribute('d', d);
          // 矢印の先端（三角形）の角度を計算 (修正: 制御点から終点へのベクトルではなく、始点から制御点へのベクトルを使用)
          const tangentVx = controlX - startX;
          const tangentVy = controlY - startY;
          // 始点に向かう接線ベクトルに基づいて角度を計算
          const angleRad = Math.atan2(startY - controlY, startX - controlX); // Y座標の差、X座標の差
          const angleDeg = (angleRad * 180 / Math.PI) + 90; // 角度に変換し、90度加算して先端を進行方向に向ける
          const transform = \`translate(\${startX}, \${startY}) rotate(\${angleDeg})\`; // 始点を基準に回転
          arrowHeadGhost.setAttribute('transform', transform); arrowHeadFront.setAttribute('transform', transform);

          feTurbulence.setAttribute('baseFrequency', baseFrequency);
          feTurbulence.setAttribute('seed', Math.floor(Math.random() * 1000));
          let matrixValues = "0.33 0.33 0.33 0 0 0.33 0.33 0.33 0 0 0.33 0.33 0.33 0 0 0 0 0 1 0";
          if (colorType !== 'fullcolor') {
              if (colorType === 'red') matrixValues = "1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0";
              else if (colorType === 'green') matrixValues = "0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0";
              else if (colorType === 'blue') matrixValues = "0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0";
              feColorMatrix.setAttribute('values', matrixValues);
              feColorMatrix.style.display = 'block'; feComposite.setAttribute('in', 'coloredNoise');
          } else {
              feColorMatrix.style.display = 'none'; feComposite.setAttribute('in', 'noise');
          }
          const contrastIntercept = (0.5 * (1 - contrastSlope)).toFixed(2);
          feFuncR.setAttribute('slope', contrastSlope); feFuncR.setAttribute('intercept', contrastIntercept);
          feFuncG.setAttribute('slope', contrastSlope); feFuncG.setAttribute('intercept', contrastIntercept);
          feFuncB.setAttribute('slope', contrastSlope); feFuncB.setAttribute('intercept', contrastIntercept);
          feFuncA.setAttribute('slope', alphaSlope);
          arrowHeadFront.style.fill = frontColor; arrowHeadFront.style.fillOpacity = frontAlpha;
          curvePathFront.style.stroke = frontColor; curvePathFront.style.strokeOpacity = frontAlpha;
        }
        updatePath();
      })();
    // ]]>
  </script>
</svg>`;





const COMBO_CHART_TEMPLATE = `
<svg width="600" height="510" viewBox="25 25 580 510" xmlns="http://www.w3.org/2000/svg" font-family="'Noto Sans JP', sans-serif" id="combo-chart-svg">

    <script id="chart-json-data" type="application/json">
        {
            "chartType": "combo",
            "data": {
                "title": "サンプル複合グラフ",
                "subtitle": "（系列データの比較）",
                "source": "出典：サンプルデータソース",
                "legendBarLabel": "系列 A",
                "legendLineLabel": "系列 B",
                "yAxisLeftLabel": "（数量）",
                "yAxisRightLabel": "（割合）",
                "colors": {
                    "bar": {
                        "start": "#e68a9c",
                        "end": "#b469b8"
                    },
                    "line": "#6b5ce0"
                },
                "items": [
                    {
                        "label": "項目 1",
                        "barValue": 245,
                        "lineValue": 16
                    },
                    {
                        "label": "項目 2",
                        "barValue": 270,
                        "lineValue": 19
                    },
                    {
                        "label": "項目 3",
                        "barValue": 310,
                        "lineValue": 21
                    },
                    {
                        "label": "項目 4",
                        "barValue": 290,
                        "lineValue": 18
                    },
                    {
                        "label": "項目 5",
                        "barValue": 300,
                        "lineValue": 23
                    }
                ],
                "layout": {
                    "width": 600,
                    "height": 510,
                    "marginTop": 180,
                    "marginBottom": 50,
                    "marginLeft": 70,
                    "marginRight": 70
                },
                "barOptions": {
                    "barToSlotRatio": 0.55,
                    "labelPosition": "auto"
                },
                "lineOptions": {
                    "markerRadius": 5
                },
                "yAxisLeft": {
                    "max": 400,
                    "min": 0,
                    "tickCount": 4
                },
                "yAxisRight": {
                    "max": 25,
                    "min": 15,
                    "tickCount": 4,
                    "unit": "%"
                },
                "animation": 1
            }
        }
    </script>

    <defs>
        <filter id="shadow">
            <feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2" />
        </filter>
         <filter id="text-shadow-dark" x="-50%" y="-50%" width="200%" height="200%">
            <feDropShadow dx="0" dy="0" stdDeviation="0.9" flood-color="#000000" flood-opacity="0.9" in="SourceAlpha" result="shadow" />
            <feMerge>
                <feMergeNode in="shadow" />
                <feMergeNode in="SourceGraphic" />
            </feMerge>
        </filter>
         <filter id="text-halo-white" x="-50%" y="-50%" width="200%" height="200%">
            <feDropShadow dx="0" dy="0" stdDeviation="2.5" flood-color="white" flood-opacity="0.95" in="SourceAlpha" result="shadow" />
            <feMerge>
                <feMergeNode in="shadow" />
                <feMergeNode in="SourceGraphic" />
            </feMerge>
        </filter>
     </defs>

    <style>
        <![CDATA[
        @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&amp;family=Roboto:wght@400;700&amp;display=swap');
        /* 基本スタイル (デフォルト Noto Sans JP) */
        .title {
            font-size: 24px;
            font-weight: 500;
            text-anchor: middle;
            fill: #5f6368;
        }

        .subtitle {
            font-size: 16px;
            text-anchor: middle;
            fill: #5f6368;
        }

        .source {
            font-size: 11px;
            fill: #70757a;
            text-anchor: end;
        }

        .legend-item {
            font-size: 13px;
            fill: #202124;
        }

        .axis-label {
            font-size: 13px;
            fill: #5f6368;
            text-anchor: middle;
        }

        .grid-line {
            stroke: #e0e0e0;
            stroke-dasharray: 2 2;
            stroke-width: 0.8;
        }

        /* 数字用のフォント指定 */
        .y-axis-value {
            font-family: 'Roboto', sans-serif;
            font-size: 12px;
            fill: #757575;
        }

        /* Anchor applied dynamically if needed */
        .data-label-bar-inside {
            font-family: 'Roboto', sans-serif;
            font-size: 14px;
            font-weight: bold;
            fill: white; /* 淡色背景でも見えるように白固定 */
            text-anchor: middle;
            filter: url(#text-shadow-dark);
        }

        .data-label-bar-outside {
            font-family: 'Roboto', sans-serif;
            font-size: 14px;
            font-weight: bold;
            fill: #5f6368;
            text-anchor: middle;
            filter: url(#text-halo-white);
        }

        /* .data-label-line のフォントはJS内でRobotoに設定されます */

        /* 動T的スタイル（bar, line, marker, data-label-line） */
        ]]>
    </style>

    <g id="title-group"></g>
    <g id="legend-group"></g>
    <g id="y-axes-group"></g>
    <g id="x-axis-group"></g>
    <g id="bar-chart-group"></g>
    <g id="line-chart-group"></g>
    <g id="source-group"></g>

    <script type="text/javascript">
        // <![CDATA[

        // =================================================================
        // ロジックコード
        // =================================================================

        const svgNS = "http://www.w3.org/2000/svg";
        function createSVGElement(name, attributes, textContent) {
            const el = document.createElementNS(svgNS, name);
            for (const key in attributes) el.setAttribute(key, attributes[key]);
            if (textContent !== undefined && textContent !== null) el.textContent = textContent;
            return el;
        }

        function truncateText(text, maxWidth, className) {
            const svgRoot = document.getElementById('combo-chart-svg');
            const tempText = createSVGElement('text', {
                class: className,
                style: 'visibility: hidden;'
            });
            svgRoot.appendChild(tempText);
            tempText.textContent = text;
            if (tempText.getComputedTextLength() <= maxWidth) {
                svgRoot.removeChild(tempText);
                return text;
            }
            let truncatedText = text;
            while (truncatedText.length > 0) {
                truncatedText = truncatedText.slice(0, -1);
                tempText.textContent = truncatedText + '...';
                if (tempText.getComputedTextLength() <= maxWidth) {
                    svgRoot.removeChild(tempText);
                    return truncatedText + '...';
                }
            }
            svgRoot.removeChild(tempText);
            return '...';
        }

        // =================================================================
        // ▼▼▼【ここから追加】色計算用のヘルパー関数 ▼▼▼
        // =================================================================
        
        /** HEX (例: #FF0000) を RGB (例: {r: 255, g: 0, b: 0}) に変換 */
        function hexToRgb(hex) {
          let shorthandRegex = /^#?([a-f0-9])([a-f0-9])([a-f0-9])$/i;
          hex = hex.replace(shorthandRegex, function(m, r, g, b) {
            return r + r + g + g + b + b;
          });
          let result = /^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex);
          return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
          } : null;
        }

        /** RGB (0-255) を HSL (H: 0-360, S: 0-100, L: 0-100) に変換 */
        function rgbToHsl(r, g, b) {
          r /= 255; g /= 255; b /= 255;
          let max = Math.max(r, g, b), min = Math.min(r, g, b);
          let h, s, l = (max + min) / 2;
          l = Math.round(l * 100); // %に変換

          if (max == min) {
            h = s = 0; // achromatic
          } else {
            let d = max - min;
            s = l > 50 ? d / (2 - max - min) : d / (max + min);
            s = Math.round(s * 100); // %に変換
            switch (max) {
              case r: h = (g - b) / d + (g < b ? 6 : 0); break;
              case g: h = (b - r) / d + 2; break;
              case b: h = (r - g) / d + 4; break;
            }
            h /= 6;
          }
          h = Math.round(h * 360); // 角度に変換
          return [h, s, l];
        }

        /** HSL (H: 0-360, S: 0-100, L: 0-100) を RGB (0-255) に変換 */
        function hslToRgb(h, s, l) {
          let r, g, b;
          h /= 360; // 0-1 に
          s /= 100; // 0-1 に
          l /= 100; // 0-1 に

          if (s == 0) {
            r = g = b = l; // achromatic
          } else {
            function hue2rgb(p, q, t) {
              if (t < 0) t += 1;
              if (t > 1) t -= 1;
              if (t < 1/6) return p + (q - p) * 6 * t;
              if (t < 1/2) return q;
              if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
              return p;
            }
            let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
            let p = 2 * l - q;
            r = hue2rgb(p, q, h + 1/3);
            g = hue2rgb(p, q, h);
            b = hue2rgb(p, q, h - 1/3);
          }
          return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
        }

        /** RGB (0-255) を HEX (例: #FF0000) に変換 */
        function rgbToHex(r, g, b) {
          function componentToHex(c) {
            let hex = c.toString(16);
            return hex.length == 1 ? "0" + hex : hex;
          }
          return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
        }
        // =================================================================
        // ▲▲▲【ここまで追加】▲▲▲
        // =================================================================

        function correctTickCount(range, currentTickCount) {
            if (range <= 0) return 1;
            if (currentTickCount <= 0) currentTickCount = 1;
            if (range % currentTickCount === 0) {
                return currentTickCount;
            }
            let lowerCount = 1;
            for (let i = currentTickCount - 1; i > 1; i--) {
                if (range % i === 0) {
                    lowerCount = i;
                    break;
                }
            }
            let upperCount = range;
            for (let i = currentTickCount + 1; i < range; i++) {
                if (range % i === 0) {
                    upperCount = i;
                    break;
                }
            }
            if ((upperCount - currentTickCount) <= (currentTickCount - lowerCount)) {
                return upperCount;
            } else {
                return lowerCount;
            }
        }

        function buildChart(config) {
            const svgRoot = document.getElementById('combo-chart-svg');
            const defs = svgRoot.querySelector('defs');
            const style = svgRoot.querySelector('style');

            const existingGrads = defs.querySelectorAll('linearGradient');
            existingGrads.forEach(grad => grad.remove());
            let styleContent = style.textContent;
            styleContent = styleContent.replace(/[.]bar {[^}]*}/g, '');
            styleContent = styleContent.replace(/[.]line {[^}]*}/g, '');
            styleContent = styleContent.replace(/[.]marker {[^}]*}/g, '');
            styleContent = styleContent.replace(/[.]data-label-line {[^}]*}/g, '');
            style.textContent = styleContent.trim();

            const gradId = 'barGradient';
            let gradient = createSVGElement('linearGradient', {
                id: gradId, x1: "0", x2: "0", y1: "0", y2: "1"
             });
            gradient.appendChild(createSVGElement('stop', {
                offset: '0%', 'stop-color': config.colors.bar.start
            }));
            gradient.appendChild(createSVGElement('stop', {
                offset: '100%', 'stop-color': config.colors.bar.end
            }));
            defs.appendChild(gradient);

            const nl = String.fromCharCode(10);
            const stylesArray = [
                "",
                "            .bar { fill: url(#" + gradId + ");",
                "            filter: url(#shadow); }",
                 // ▼▼▼【変更点】 line の stroke 色を使用 ▼▼▼
                "            .line { fill: none; stroke: " + config.colors.line + ";",
                "            stroke-width: 3; stroke-linejoin: round; filter: url(#shadow); }",
                // ▼▼▼【変更点】 marker の fill 色を使用 ▼▼▼
                "            .marker { fill: " + config.colors.marker + ";",
                "            stroke: white; stroke-width: 2; filter: url(#shadow); }",
                 // ▼▼▼【変更点】 lineLabel の fill 色を使用 ▼▼▼
                "            .data-label-line { font-family: 'Roboto', sans-serif;",
                "            font-size: 13px; font-weight: bold; fill: " + config.colors.lineLabel + "; text-anchor: middle; filter: url(#text-halo-white); }",
                 "        "
            ];
            let dynamicStyles = stylesArray.join(nl);
            style.textContent += dynamicStyles;

            const groups = {
                title: document.getElementById('title-group'),
                legend: document.getElementById('legend-group'),
                yAxes: document.getElementById('y-axes-group'),
                xAxis: document.getElementById('x-axis-group'),
                 barChart: document.getElementById('bar-chart-group'),
                lineChart: document.getElementById('line-chart-group'),
                source: document.getElementById('source-group')
            };
            for (const key in groups) {
                while (groups[key].firstChild) groups[key].removeChild(groups[key].firstChild);
            }

            const chartAreaWidth = config.layout.width - config.layout.marginLeft - config.layout.marginRight;
            const chartAreaHeight = config.layout.height - config.layout.marginTop - config.layout.marginBottom;
            const bottomY = config.layout.marginTop + chartAreaHeight;



            // --- Y-Axis Left (Bar) Calculation ---
            const maxBarValue = Math.max(...config.items.map(d => d.barValue), config.yAxisLeft.max);
            const minBarValue = Math.min(...config.items.map(d => d.barValue), config.yAxisLeft.min); // ★追加★
            const leftTickCount = config.yAxisLeft.tickCount > 0 ? config.yAxisLeft.tickCount : 1;
            const preliminaryLeftRange = Math.max(1, config.yAxisLeft.max - config.yAxisLeft.min);
            let leftTickInterval = preliminaryLeftRange / leftTickCount;

            let effectiveYAxisLeftMin, effectiveYAxisLeftMax;

            if (minBarValue < config.yAxisLeft.min) { // ★追加★ マイナス値の処理
                const limit = Math.max(Math.abs(minBarValue), maxBarValue);
                if (leftTickInterval <= 0 || isNaN(leftTickInterval)) leftTickInterval = Math.max(1, limit / 5);
                effectiveYAxisLeftMax = Math.ceil(limit / leftTickInterval) * leftTickInterval;
                effectiveYAxisLeftMin = -effectiveYAxisLeftMax;
                if (config.yAxisLeft.min > effectiveYAxisLeftMin) effectiveYAxisLeftMin = config.yAxisLeft.min;
                if (config.yAxisLeft.max < effectiveYAxisLeftMax) effectiveYAxisLeftMax = config.yAxisLeft.max;
            } else { // 従来の処理 (プラス値のみ)
                effectiveYAxisLeftMin = config.yAxisLeft.min;
                if (leftTickInterval <= 0 || isNaN(leftTickInterval)) leftTickInterval = Math.max(1, (maxBarValue - effectiveYAxisLeftMin) / 5) || 1;
                effectiveYAxisLeftMax = Math.max(config.yAxisLeft.max, Math.ceil(maxBarValue / leftTickInterval) * leftTickInterval);
            }
            
            if (effectiveYAxisLeftMax <= effectiveYAxisLeftMin) {
                effectiveYAxisLeftMax = effectiveYAxisLeftMin + leftTickInterval * leftTickCount;
                if (effectiveYAxisLeftMax <= effectiveYAxisLeftMin) effectiveYAxisLeftMax = effectiveYAxisLeftMin + 1;
            }
            // --- End Y-Axis Left ---




            if (effectiveYAxisLeftMax === effectiveYAxisLeftMin) effectiveYAxisLeftMax += leftTickInterval;

            const rightRangeOriginal = config.yAxisRight.max - config.yAxisRight.min;
            let rightTickCount = config.yAxisRight.tickCount > 0 ? config.yAxisRight.tickCount : 1;
            if (rightRangeOriginal > 0) {
                rightTickCount = correctTickCount(rightRangeOriginal, rightTickCount);
            }
            const rightTickInterval = (rightRangeOriginal > 0 && rightTickCount > 0) ?
                (rightRangeOriginal) / rightTickCount : 1;
            const maxLineValue = Math.max(...config.items.map(d => d.lineValue), config.yAxisRight.max);
            const minLineValue = Math.min(...config.items.map(d => d.lineValue), config.yAxisRight.min);
            let effectiveYAxisRightMin = config.yAxisRight.min;
            if (minLineValue < config.yAxisRight.min) {
                effectiveYAxisRightMin = Math.floor(minLineValue / rightTickInterval) * rightTickInterval;
            }
            let effectiveYAxisRightMax = config.yAxisRight.max;
            if (maxLineValue > config.yAxisRight.max) {
                effectiveYAxisRightMax = Math.ceil(maxLineValue / rightTickInterval) * rightTickInterval;
            }
            if (effectiveYAxisRightMax <= effectiveYAxisRightMin) effectiveYAxisRightMax = effectiveYAxisRightMin + rightTickInterval * (rightTickCount || 1);

            const leftYRange = effectiveYAxisLeftMax - effectiveYAxisLeftMin;
            const rightYRange = effectiveYAxisRightMax - effectiveYAxisRightMin;

            const xScale = (index) => config.layout.marginLeft + (chartAreaWidth / config.items.length) * (index + 0.5);
            const yScaleLeft = (value) => bottomY - (leftYRange > 0 ? chartAreaHeight * ((value - effectiveYAxisLeftMin) / leftYRange) : chartAreaHeight / 2);
            const yScaleRight = (value) => bottomY - (rightYRange > 0 ? chartAreaHeight * ((value - effectiveYAxisRightMin) / rightYRange) : chartAreaHeight / 2);


            // ▼▼▼【ここから追加】▼▼▼
        // 1. グラフの「位置」用 (0.0 -> 1.0)
        const animProgress = (config.animation == null) 
            ? 1.0 
            : Math.max(0, Math.min(1, Number(config.animation)));

        // 2. データラベルの「透明度」用 (0.7 -> 1.0 でフェードイン)
        let animOpacity;
        const fadeStart = 0.7; // 0.7 からフェードイン開始
        if (animProgress <= fadeStart) {
            animOpacity = 0;
        } else {
            // 0.7 (0%) から 1.0 (100%) の間で 0 -> 1 に変化
            animOpacity = (animProgress - fadeStart) / (1.0 - fadeStart);
        }
        // ▲▲▲【ここまで追加】▲▲▲

            groups.title.appendChild(createSVGElement('text', { x: 312, y: 65, class: 'title' }, config.title));
            groups.title.appendChild(createSVGElement('text', { x: 312, y: 90, class: 'subtitle' }, config.subtitle));
            groups.source.appendChild(createSVGElement('text', { x: 605, y: 525, class: 'source' }, config.source));
            const legendY = config.layout.marginTop - 60;

            const legendAGroup = createSVGElement('g', { transform: 'translate(' + config.layout.marginLeft + ', ' + legendY + ')' });
            legendAGroup.appendChild(createSVGElement('rect', { x: 0, y: -6, width: 12, height: 12, rx: 2, class: 'bar' })); // Uses .bar style for legend
            legendAGroup.appendChild(createSVGElement('text', { x: 18, y: 0, class: 'legend-item', 'dominant-baseline': 'middle' }, config.legendBarLabel));
            groups.legend.appendChild(legendAGroup);

            const legendBGroup = createSVGElement('g', { transform: 'translate(' + (config.layout.width - config.layout.marginRight) + ', ' + legendY + ')' });
             // ▼▼▼【変更点】 legend の line/marker も style で指定した色を使う ▼▼▼
            legendBGroup.appendChild(createSVGElement('line', { x1: 0, y1: 0, x2: -12, y2: 0, stroke: config.colors.line, 'stroke-width': 2 })); // Use line color
            legendBGroup.appendChild(createSVGElement('circle', { cx: -6, cy: 0, r: 3, fill: config.colors.marker, stroke: 'white', 'stroke-width': 1 })); // Use marker color
             // ▲▲▲
            legendBGroup.appendChild(createSVGElement('text', { x: -20, y: 0, class: 'legend-item', 'dominant-baseline': 'middle', 'text-anchor': 'end' }, config.legendLineLabel));
            groups.legend.appendChild(legendBGroup);

            // Left Y-Axis
            for (let i = 0; i <= leftTickCount; i++) {
                const tickValue = effectiveYAxisLeftMin + (leftYRange / leftTickCount) * i;
                const y = yScaleLeft(tickValue);
                if (i > 0 && i <= leftTickCount) {
                    groups.yAxes.appendChild(createSVGElement('line', { x1: config.layout.marginLeft, y1: y, x2: config.layout.width - config.layout.marginRight, y2: y, class: 'grid-line' }));
                }
                groups.yAxes.appendChild(createSVGElement('text', { x: config.layout.marginLeft - 10, y, class: 'y-axis-value', 'text-anchor': 'end', 'dominant-baseline': 'middle' }, Math.round(tickValue)));
            }
            groups.yAxes.appendChild(createSVGElement('text', { x: 30, y: config.layout.marginTop - 20, class: 'y-axis-value' }, config.yAxisLeftLabel));
            // Right Y-Axis
            for (let i = 0; i <= rightTickCount; i++) {
                const tickValue = effectiveYAxisRightMin + (rightYRange / rightTickCount) * i;
                const y = yScaleRight(tickValue);
                groups.yAxes.appendChild(createSVGElement('text', { x: config.layout.width - config.layout.marginRight + 10, y: y, class: 'y-axis-value', 'text-anchor': 'start', 'dominant-baseline': 'middle' }, Math.round(tickValue) + (config.yAxisRight.unit || '')));
            }
            groups.yAxes.appendChild(createSVGElement('text', { x: 585, y: config.layout.marginTop - 20, class: 'y-axis-value', 'text-anchor': 'end' }, config.yAxisRightLabel));
            // X-Axis
            const slotWidth = chartAreaWidth / config.items.length;
            config.items.forEach((item, index) => {
                const labelContent = truncateText(item.label, slotWidth * 0.9, 'axis-label');
                groups.xAxis.appendChild(createSVGElement('text', { x: xScale(index), y: bottomY + 20, class: 'axis-label' }, labelContent));
            });
            groups.xAxis.appendChild(createSVGElement('line', { x1: config.layout.marginLeft, y1: bottomY, x2: config.layout.width - config.layout.marginRight, y2: bottomY, stroke: '#bdbdbd' }));

            // ▼▼▼【ここから追加】▼▼▼
            // 0基線を描画 (Y軸が0をまたぐ場合)
            if (effectiveYAxisLeftMin < 0 && effectiveYAxisLeftMax > 0) {
                const yZero = yScaleLeft(0);
                groups.yAxes.appendChild(createSVGElement('line', { x1: config.layout.marginLeft, y1: yZero, x2: config.layout.width - config.layout.marginRight, y2: yZero, stroke: '#bdbdbd', 'stroke-width': 1 }));
            }
            // ▲▲▲【ここまで追加】▲▲▲            
            


            const barWidth = slotWidth * config.barOptions.barToSlotRatio;
            const barLabelYPositions = [];
            const yZero = yScaleLeft(Math.max(0, effectiveYAxisLeftMin));




// ▼▼▼【ここから差し替え】▼▼▼
            
            config.items.forEach((item, index) => {
                const x = xScale(index) - barWidth / 2;
                
                // 1. アニメーション適用後の高さを計算
                let finalBarY, finalBarHeight;
                let animatedBarY, animatedBarHeight;
                
                if (item.barValue >= 0) {
                    finalBarY = yScaleLeft(item.barValue);
                    finalBarHeight = Math.max(0, yZero - finalBarY);
                    animatedBarHeight = finalBarHeight * animProgress;
                    animatedBarY = yZero - animatedBarHeight;
                } else {
                    finalBarY = yZero;
                    finalBarHeight = Math.max(0, yScaleLeft(item.barValue) - yZero);
                    animatedBarHeight = finalBarHeight * animProgress;
                    animatedBarY = yZero;
                }

                // 2. 棒を描画 (高さが1px以上の場合)
                if (animatedBarHeight >= 1) {
                    groups.barChart.appendChild(createSVGElement('rect', { 
                        x: x, 
                        y: animatedBarY, // アニメーション後のY
                        width: barWidth, 
                        height: animatedBarHeight, // アニメーション後の高さ
                        class: 'bar', 
                        rx: 4, 
                        ry: 4 
                    }));
                }

                // 3. ラベル位置の計算 (アニメーション後の高さ/Y座標基準)
                const isOutside = animatedBarHeight < 30; // アニメーション後の高さで判定
                let labelY, labelClass;
                const labelMarginInside = 20;
                const labelMarginOutside = 10;

                if (isOutside) {
                    labelClass = 'data-label-bar-outside';
                    labelY = (item.barValue >= 0) ?
                        animatedBarY - labelMarginOutside : animatedBarY + animatedBarHeight + labelMarginOutside + 8;
                } else {
                    labelClass = 'data-label-bar-inside';
                    labelY = (item.barValue >= 0) ?
                        animatedBarY + labelMarginInside : animatedBarY + animatedBarHeight - labelMarginInside;
                }

                // ラベルが0基線やグラフの底を突き抜けないように調整 (アニメーションY基準)
                if (!isOutside && item.barValue >= 0 && labelY > yZero - 5) labelY = yZero - 5;
                if (!isOutside && item.barValue < 0 && labelY < yZero + 15) labelY = yZero + 15;
                // (グラフ上端・下端の調整はアニメーションY基準では不要なため削除)

                barLabelYPositions.push({ index: index, y: labelY }); // 折れ線ラベルとの衝突判定用に格納

                // 4. ラベルを描画 (透明度を適用)
                groups.barChart.appendChild(createSVGElement('text', { 
                    x: xScale(index), 
                    y: labelY, // アニメーション後のY
                    class: labelClass,
                    style: 'opacity: ' + animOpacity // 0.7-1.0でフェードイン
                }, item.barValue)); // 表示する値は最終値

            });
            // ▲▲▲【ここまで差し替え】▲▲▲







            // Line, Markers, and Line Labels
            
            // 1. Y軸基線 (折れ線グラフ用)
            // (config.yAxisRight.min が 0 より大きい場合も考慮)
            const yZeroLine = yScaleRight(Math.max(effectiveYAxisRightMin, 0));
            
            // 2. 各点の X, animatedY, finalY を計算
            let allLineValuesSame = false;
            if (config.items.length > 0) {
                const firstValue = config.items[0].lineValue;
                allLineValuesSame = config.items.every(item => item.lineValue === firstValue);
            }

            // 2. 各点の X, animatedY, finalY を計算
            const pointsData = config.items.map((item, index) => {
                const x = xScale(index);
                const finalY = yScaleRight(item.lineValue);
                
                // グラフ位置のアニメーション
                let animatedY = yZeroLine + (finalY - yZeroLine) * animProgress;

                // 判定フラグがtrueの場合のみ、「ゆらぎ」を追加
                if (allLineValuesSame) {
                    const jiggle = (index % 2 === 0) ? 0.01 : -0.01;
                    animatedY += jiggle;
                }
                
                return {
                    x: x,
                    finalY: finalY,
                    animatedY: animatedY, // 条件に応じてゆらぎが追加されたY
                    value: item.lineValue
                };
            });

            // 3. 線を描画 (animatedY を使用)
            const points = pointsData.map(d => d.x + ',' + d.animatedY).join(' ');
groups.lineChart.appendChild(createSVGElement('polyline', { class: 'line', points }));

            // 4. マーカーとラベルを描画 (animatedY に追従)
            pointsData.forEach((data, index) => {
                const x = data.x;
                const y = data.animatedY; // アニメーション後のY座標
                
                // マーカー描画
                groups.lineChart.appendChild(createSVGElement('circle', { 
                    cx: x, 
                    cy: y, // アニメーション後のY
                    r: config.lineOptions.markerRadius, 
                    class: 'marker' 
                }));

                let lineLabelY = y - 20; // デフォルトはマーカーの上

                // 棒グラフのラベルY座標と衝突チェック
const correspondingBarLabel = barLabelYPositions.find(pos => pos.index === index);
                if (correspondingBarLabel && Math.abs(lineLabelY - correspondingBarLabel.y) < 20) {
                    let belowY = y + 25; // 下に配置
if (belowY < bottomY - 5) { 
                         lineLabelY = belowY;
                    }
                }

                // ラベル描画 (透明度を適用)
                groups.lineChart.appendChild(createSVGElement('text', { 
                    x: x, 
                    y: lineLabelY, // アニメーション後のY (追従)
                    class: 'data-label-line',
                    style: 'opacity: ' + animOpacity // 0.7-1.0でフェードイン
                }, data.value + (config.yAxisRight.unit || '')));
            });
            // ▲▲▲【ここまで差し替え】▲▲▲


}







        // Execute chart build
        try {
            const configJson = document.getElementById('chart-json-data').textContent;
            if (!configJson || configJson.trim() === '') {
                throw new Error("JSON data is empty.");
            }
            let fullConfig;
            try {
                fullConfig = JSON.parse(configJson);
            } catch (parseError) {
                throw new Error('JSON parsing error: ' + parseError.message);
            }

            if (!fullConfig || typeof fullConfig !== 'object' || !fullConfig.data) {
                throw new Error("Invalid JSON structure: Missing 'data' key.");
            }
            const config = fullConfig.data;

            // layout を固定値で上書き
            config.layout = { 
                "width": 600, 
                "height": 450, 
                "marginTop": 180, 
                "marginBottom": 0, 
                "marginLeft": 70, 
                "marginRight": 70 
            };
            // ▲▲▲【ここまで追加】▲▲▲

            // ▼▼▼【以下を追加】▼▼▼
            // オプションのデフォルト値を設定
            config.barOptions = config.barOptions || {};
            config.barOptions.barToSlotRatio = (config.barOptions.barToSlotRatio == null) ? 0.55 : config.barOptions.barToSlotRatio;
            config.barOptions.labelPosition = config.barOptions.labelPosition || "auto";
            
            config.lineOptions = config.lineOptions || {};
            config.lineOptions.markerRadius = (config.lineOptions.markerRadius == null) ? 5 : config.lineOptions.markerRadius;
            // ▲▲▲【ここまで追加】▲▲▲

            
            // ▼▼▼【ここから変更】 "colors" が文字列だった場合の処理を分岐 ▼▼▼
            if (config.colors && typeof config.colors === 'string') {
                
                const colorKeyword = config.colors.toLowerCase();
                let generatedColorObject;

                // ▼▼▼【分岐1】#gemini の場合 ▼▼▼
                if (colorKeyword === '#gemini') {
                    generatedColorObject = {
                        bar: { start: "#E68A9C", end: "#9F63D0" }, // Pink -> Purple
                        line: "#616AD8",  // Blue line
                        marker: "#616AD8", // Blue marker
                        lineLabel: "#4D46AE" // Darker blue label
                    };
                
                // ▼▼▼【分岐2: 高コントラスト版】#night の場合 ▼▼▼
                } else if (colorKeyword === '#night') {
                    var NIGHT_COLORS = [
                        { start: "#8a2be2", end: "#4169e1" }, // 0: Purple
                        { start: '#8f83d6', end: '#5a5188' }, // 1: Blue-Purple
                        { start: '#7891dd', end: '#4456a0' }, // 2: Blue
                        { start: '#FF8F00', end: '#FF8F00' }, // 3: Teal
                        { start: '#54c5d5', end: '#267b87' }  // 4: Cyan
                    ];
                    generatedColorObject = {
                        bar: { start: NIGHT_COLORS[0].start, end: NIGHT_COLORS[0].end }, // Purple bar
                        line: NIGHT_COLORS[3].start, // Teal line (use start for slightly brighter)
                        marker: NIGHT_COLORS[3].start, // Teal marker
                        lineLabel: NIGHT_COLORS[3].end // Darker Teal label
                    };

                // ▼▼▼【分岐3: ★最終調整★】HEXコード（単色・超低彩度/超明 + 低彩度/明 + 低彩度/暗 + 低彩度/超暗）の場合 ▼▼▼
              } else {
                    // -----------------------------------------
                    // ▼▼▼【ここから差し替え (無彩色対応)】▼▼▼
                    // -----------------------------------------

                    const inputColorHex = config.colors;
                    const rgb = hexToRgb(inputColorHex);
                    if (!rgb) {
                        throw new Error("Invalid HEX color string in 'colors': " + inputColorHex);
                    }
                    let [h_orig, s_orig, l_orig] = rgbToHsl(rgb.r, rgb.g, rgb.b); // 元のHSL値

                    // --- 1. 基本設定（クランプ値など） ---
                    const MIN_L = 25; // 明るさの下限値
                    const MAX_L = 75; // 明るさの上限値
                    const ACHROMATIC_THRESHOLD_S = 5; // これ以下の彩度は「無彩色」とみなす
                    const MIN_S_FOR_COLOR = 30; // 有彩色の場合の最低彩度

                    // --- 2. 基準となる H, S, L を決定 ---
                    let h_base = h_orig;
                    let s_base;
                    let l_base = Math.max(MIN_L, Math.min(MAX_L, l_orig)); // 明るさをクランプ

                    // ★★★ 無彩色判定 ★★★
                    if (s_orig < ACHROMATIC_THRESHOLD_S) {
                        // 元が白、黒、グレーの場合
                        s_base = 0; // 彩度を 0 に固定 (グレーにするため)
                    } else {
                        // 元が有彩色の場合
                        // 彩度が低すぎる有彩色（くすんだ色）は、最低彩度を保証
                        s_base = Math.max(s_orig, MIN_S_FOR_COLOR);
                    }

                    // --- 3. 各パーツの色を生成 ---
                    // 基準色 (h_base, s_base, l_base) をHEXに変換
                    const hex_base_color = rgbToHex(...hslToRgb(h_base, s_base, l_base));

                    // 棒グラフ (End)
                    const hex_bar_end = hex_base_color;

                    // 棒グラフ (Start): 基準より明るく
                    let l_bar_start = Math.min(100, l_base + 15);
                    // 基準が明るい場合は、彩度を少し下げて白飛びを防ぐ (無彩色の場合はs_base=0のまま)
                    let s_bar_start = (l_base > MAX_L - 10) ? s_base * 0.8 : s_base;
                    const hex_bar_start = rgbToHex(...hslToRgb(h_base, s_bar_start, l_bar_start));

                    // 折れ線・マーカー
                    const hex_line_main = hex_base_color;

                    // 折れ線ラベル: 基準より暗く
                    let l_line_label = Math.max(0, l_base - 25); // 暗く
                    // 基準が暗い場合は、彩度を少し上げる (無彩色の場合はs_base=0のまま)
                    let s_line_label = (l_base < MIN_L + 10) ? Math.min(100, s_base * 1.2) : s_base;
                    const hex_line_label = rgbToHex(...hslToRgb(h_base, s_line_label, l_line_label));

                    generatedColorObject = {
                        bar: { start: hex_bar_start, end: hex_bar_end },
                        line: hex_line_main,
                        marker: hex_line_main,
                        lineLabel: hex_line_label
                    };
                    // -----------------------------------------
                    // ▲▲▲【ここまで差し替え】▲▲▲
                    // -----------------------------------------
                }



                
                // 最終的な色設定を config に上書き
                config.colors = generatedColorObject;
                
            } else if (!config.colors || typeof config.colors !== 'object' || !config.colors.bar || !config.colors.line) {
                 // "colors" がオブジェクトでも文字列でもない場合 / 必要なキーがない場合
                 throw new Error("'colors' must be a valid color object {bar, line, ...} or a single color string.");
            }
            // ★ lineLabel/marker が未定義の場合のフォールバック (gemini/night用)
             if (config.colors && typeof config.colors === 'object') {
                 if (!config.colors.marker) config.colors.marker = config.colors.line; // Default marker to line color
                 if (!config.colors.lineLabel) {
                     // Default label to a darker version of line/marker color, or fallback to line color
                     const markerRgb = hexToRgb(config.colors.marker);
                     if (markerRgb) {
                         const markerHsl = rgbToHsl(markerRgb.r, markerRgb.g, markerRgb.b);
                         const label_l = Math.max(10, markerHsl[2] - 25);
                         const labelRgb = hslToRgb(markerHsl[0], markerHsl[1], label_l);
                         config.colors.lineLabel = rgbToHex(labelRgb[0], labelRgb[1], labelRgb[2]);
                     } else {
                        config.colors.lineLabel = config.colors.line; // Fallback
                     }
                 }
            }
            // ▲▲▲【ここまで変更】▲▲▲

            if (!config || typeof config !== 'object' || !config.items || !Array.isArray(config.items)) {
                throw new Error("Invalid JSON structure: Missing required fields like 'items' inside 'data'.");
            }
            buildChart(config);
        } catch (e) {
            console.error("Chart Error:", e);
            const svgRoot = document.getElementById('combo-chart-svg');
            const existingError = svgRoot.querySelector('.error-message');
            if (existingError) existingError.remove();
            const textEl = createSVGElement('text', {
                x: svgRoot.viewBox.baseVal.width / 2 + svgRoot.viewBox.baseVal.x,
                y: svgRoot.viewBox.baseVal.height / 2 + svgRoot.viewBox.baseVal.y,
                'text-anchor': 'middle',
                fill: 'red',
                'font-family': "'Noto Sans JP', sans-serif", // Ensure error uses correct font
                class: 'error-message'
            });
            textEl.textContent = e.message.includes('JSON') ? 'JSONデータにエラーがあります。' : 'グラフ描画エラーが発生しました。';
            if (e.message.includes('JSON parsing error')) {
                textEl.textContent += ' (' + e.message.split(': ')[1] + ')';
            }
            svgRoot.appendChild(textEl);
        }

        // ]]>
    </script>
</svg>
`;




const MULTI_LINE_CHART_TEMPLATE = `
<svg width="650" height="510" viewBox="25 15 620 485" xmlns="http://www.w3.org/2000/svg" font-family="'Noto Sans JP', sans-serif" id="multi-line-chart-svg">

  <script id="chart-json-data" type="application/json">
    {
      "chartType": "multi-line",
      "data": {
        "title": "データ系列の比較",
        "subtitle": "（年間サンプルデータ）",
        "source": "出典：サンプルデータ",
        "yAxisUnitLabel": "（単位）",
        "xAxisLabels": ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
        "series": [
 

          { "id": "A", "label": "系列A", "values": [10, 12, 15, 20, 24, 27, 29, 28, 25, 20, 16, 12] },
          { "id": "B", "label": "系列B", "values": [7, 8, 11, 15, 18, 20, 21, 20, 16, 13, 10, 8] },
          { "id": "C", "label": "系列C", "values": [4, 5, 6, 8, 10, 11, 12, 10, 8, 7, 5, 4] }
        ],
        
"colors": 
[
          { "id": "A", "start": "#e68a9c", "end": "#d96d8f" },
          { "id": "B", "start": "#b469b8", "end": "#a656ad" },
          { "id": "C", "start": "#7c6ce8", "end": "#6b5ce0" }
        ],
        "layout": {
          "width": 650,
          "height": 510,
         
 
"marginTop": 140,
          "marginBottom": 80,
          "marginLeft": 75,
          "marginRight": 35,
          "horizontalPadding": 20
        },
        "yAxis": {
          "min": 0,
          "max": 30,
          "tickCount": 6
    
 
   },
        "lineOptions": {
          "markerRadius": 5,
          "dataLabelOffsetY": 8
        },

        "animation": 1
      }
    }
  </script>
  <defs>
    <filter id="shadow"><feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2"/></filter>
    
    <filter id="text-halo-white" x="-50%" y="-50%" width="200%" height="200%">
      <feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="white" flood-opacity="0.9" in="SourceAlpha" result="shadow"/>
      <feMerge>
 
       <feMergeNode in="shadow"/>
        <feMergeNode in="SourceGraphic"/>
      </feMerge>
    </filter>
  </defs>

  <style>
    @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500&amp;family=Roboto:wght@400&amp;display=swap');
/* Base styles (Default: Noto Sans JP) */
    .title { font-size: 24px; font-weight: 500; text-anchor: middle;
fill: #5f6368; }
    .subtitle { font-size: 16px; text-anchor: middle; fill: #5f6368;
}
    .source { font-size: 11px; fill: #70757a; text-anchor: end; }
    .legend-item { font-size: 13px;
fill: #202124; dominant-baseline: middle; }
    .grid-line { stroke: #e0e0e0; stroke-dasharray: 2 2; stroke-width: 0.8;
}
    .zero-line { stroke: #bdbdbd; stroke-width: 1; }
    .axis-label { font-size: 13px; fill: #5f6368;
text-anchor: middle; }
    .y-axis-unit-label { font-family: 'Noto Sans JP', sans-serif; font-size: 12px; fill: #757575; text-anchor: start;
}

    /* Styles for numbers (Roboto) */
    .y-axis-value { font-family: 'Roboto', sans-serif; font-size: 12px;
fill: #757575; text-anchor: end; }
    .data-label { font-family: 'Roboto', sans-serif; font-size: 12px; font-weight: normal; text-anchor: middle;
filter: url(#text-halo-white); }

    /* Dynamic styles (line-a, marker-a, data-label-a, etc.) added by JS */
  </style>

  <g id="title-group"></g>
  <g id="legend-group"></g>
  <g id="y-axis-group"></g>
  <g id="x-axis-group"></g>
  <g id="chart-area-group"></g>
  <g id="source-group"></g>

  <script type="text/javascript">
    // <![CDATA[

    // =================================================================
    // Chart rendering logic
    // =================================================================
    const svgNS = "http://www.w3.org/2000/svg";
function createSVGElement(name, attributes, textContent) {
      const el = document.createElementNS(svgNS, name);
for (const key in attributes) el.setAttribute(key, attributes[key]);
      if (textContent !== undefined && textContent !== null) el.textContent = textContent;
      return el;
}

    // =================================================================
    // ▼▼▼【ここから追加】色計算用のヘルパー関数 (IE互換のため var を使用) ▼▼▼
    // =================================================================
    
    /** HEX (例: #FF0000) を RGB (例: {r: 255, g: 0, b: 0}) に変換 */
    function hexToRgb(hex) {
      var shorthandRegex = /^#?([a-f0-9])([a-f0-9])([a-f0-9])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
        return r + r + g + g + b + b;
      });
var result = /^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex);
      return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
      } : null;
}

    /** RGB (0-255) を HSL (H: 0-360, S: 0-100, L: 0-100) に変換 */
    function rgbToHsl(r, g, b) {
      r /= 255;
g /= 255; b /= 255;
      var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
      l = Math.round(l * 100);
// %に変換

      if (max == min) {
        h = s = 0;
// achromatic
      } else {
        var d = max - min;
s = l > 50 ? d / (2 - max - min) : d / (max + min);
s = Math.round(s * 100); // %に変換
        switch (max) {
          case r: h = (g - b) / d + (g < b ? 6 : 0);
break;
          case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
        h /= 6;
}
      h = Math.round(h * 360);
// 角度に変換
      return [h, s, l];
}

    /** HSL (H: 0-360, S: 0-100, L: 0-100) を RGB (0-255) に変換 */
    function hslToRgb(h, s, l) {
      var r, g, b;
h /= 360; // 0-1 に
      s /= 100;
// 0-1 に
      l /= 100;
// 0-1 に

      if (s == 0) {
        r = g = b = l;
// achromatic
      } else {
        function hue2rgb(p, q, t) {
          if (t < 0) t += 1;
if (t > 1) t -= 1;
          if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
          if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
        }
        var q = l < 0.5 ?
l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
      }
      return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}

    /** RGB (0-255) を HEX (例: #FF0000) に変換 */
    function rgbToHex(r, g, b) {
      function componentToHex(c) {
        var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
      return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}
    
    /** 2つのHEXカラー間を線形補間する */
    function linearInterpolateColor(hex1, hex2, t) {
      var rgb1 = hexToRgb(hex1);
var rgb2 = hexToRgb(hex2);
      
      var r = Math.round(rgb1.r + (rgb2.r - rgb1.r) * t);
var g = Math.round(rgb1.g + (rgb2.g - rgb1.g) * t);
      var b = Math.round(rgb1.b + (rgb2.b - rgb1.b) * t);
return rgbToHex(r, g, b);
    }

    /** 複数のカラーストップからグラデーションパレット（HEX配列）を生成する */
    function generateGradientScale(gradientStops, numSteps) {
      var palette = [];
if (numSteps <= 0) return [];
      if (numSteps === 1) return [gradientStops[0]];
// 1色だけなら最初の色
      
      var numSegments = gradientStops.length - 1;
for (var i = 0; i < numSteps; i++) {
        // 全体の進捗 (0.0 ... 1.0)
        var t_global = (numSteps === 1) ?
0 : (i / (numSteps - 1));
        
        // どのセグメントにいるか
        var segmentProgress = t_global * numSegments;
var stopIndex1 = Math.floor(segmentProgress);
        var stopIndex2 = Math.min(stopIndex1 + 1, numSegments);
// そのセグメント内でのローカルな進捗 (0.0 ... 1.0)
        var t_local = segmentProgress - stopIndex1;
var hex1 = gradientStops[stopIndex1];
        var hex2 = gradientStops[stopIndex2];
        
        palette.push(linearInterpolateColor(hex1, hex2, t_local));
      }
      return palette;
}





// (140行目あたりから)
    /**
     * 【単色用】プライマリーカラーとID配列から、自動計算された "colors" 配列を生成する
     * (交互の濃淡パターン)
     */
    function generateMonochromaticScale(primaryHex, colorIds) {
        var numSteps = colorIds.length;
        if (numSteps === 0) return [];

        var rgb = hexToRgb(primaryHex);
        if (!rgb) return [];
        
        var hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
        var h = hsl[0], s = hsl[1], l_mid_original = hsl[2];
        
        var gradientDarken = 7;
        var L_MAX = 80; 
        var L_MIN = 45;

        // ▼▼▼【[A] ここから変更】グラフの数でロジックを分岐 ▼▼▼
        var l_steps_start; // 先に宣言

        if (numSteps === 1) {
            // 【単一グラフの場合】一番濃い L_MIN (45) を使用
            l_steps_start = [L_MIN];

        } else {
            // 【複数グラフの場合】従来のロジック（交互の濃淡）
            var l_step_amount = (L_MAX - L_MIN) / (numSteps - 1);
            
            var l_steps_calc = [];
            for (var i = 0; i < numSteps; i++) {
                var l_val = L_MAX - (i * l_step_amount);
                l_steps_calc.push(l_val);
            }
            
            var l_steps_light = [];
            var l_steps_dark = [];
            var mid_index = Math.ceil(numSteps / 2);
            for (var i = 0; i < mid_index; i++) {
                l_steps_light.push(l_steps_calc[i]);
            }
            for (var i = mid_index; i < numSteps; i++) {
                l_steps_dark.push(l_steps_calc[i]);
            }

            l_steps_start = []; // ここで初期化
            for (var i = 0; i < numSteps; i++) {
                if (i % 2 === 0) {
                    l_steps_start.push(l_steps_light[i / 2]);
                } else {
                    l_steps_start.push(l_steps_dark[(i - 1) / 2]);
                }
            }
        }
        // ▲▲▲【[A] ここまで変更】▲▲▲

        var colors = [];
        for (var i = 0; i < numSteps; i++) {
            var l_start_step = l_steps_start[i];
            var l_start = Math.max(0, Math.min(100, l_start_step));
            var l_end = Math.max(0, Math.min(100, l_start - gradientDarken));
            
            var s_adjusted = s;
            if (l_start > 85) s_adjusted = s * (1 - (l_start - 85) / 15);
            if (l_start < 20) s_adjusted = s * (l_start / 20);
            s_adjusted = Math.max(0, Math.min(100, s_adjusted));
            
            var rgb_start = hslToRgb(h, s_adjusted, l_start);
            var hex_start = rgbToHex(rgb_start[0], rgb_start[1], rgb_start[2]);
            var rgb_end = hslToRgb(h, s_adjusted, l_end);
            var hex_end = rgbToHex(rgb_end[0], rgb_end[1], rgb_end[2]);
            
            colors.push({
                id: colorIds[i],
                start: hex_start,
                end: hex_end
            });
        }
        return colors;
    }
// (217行目あたりまで)



    // =================================S================================
    // ▲▲▲【ここまで追加】▲▲▲
    // =================================================================

    function buildChart(config) {
      const svgRoot = document.getElementById('multi-line-chart-svg');
const defs = svgRoot.querySelector('defs');
      const style = svgRoot.querySelector('style');

      // Clear existing dynamic gradients before adding new ones
      const existingGrads = defs.querySelectorAll('linearGradient');
existingGrads.forEach(function(grad) { grad.remove(); }); // Use function()

      // Clear existing dynamic styles by removing rules containing '-a' (assuming 'a' is always the first series ID)
      let styleContent = style.textContent;
const dynamicRulePattern = /\.((line|marker|area|data-label)-[a-z])\s*{[^}]*}/g;
      styleContent = styleContent.replace(dynamicRulePattern, '');
      style.textContent = styleContent.trim();
// === ▼▼▼【 nl と join を使うように修正 】▼▼▼
      const nl = String.fromCharCode(10);
// 改行コード(LF) を変数化
      let dynamicStyleParts = [];
// 配列でスタイルルールを保持

      config.colors.forEach(function(color) { // Use function()
        const id = color.id;
        
        // === ▼ バッククォート不使用に変更 ▼ ===
        const markerGradId = 'markerGradient' + id;
        const areaGradId = 'areaGradient' + id;
        // === ▲ 変更 ▲ ===
        
        const cssPrefix 
= id.toLowerCase();

     
        // Create gradients
        let markerGrad = createSVGElement('linearGradient', { id: markerGradId, x1: "0", x2: "0", y1: "0", y2: "1" });
        markerGrad.appendChild(createSVGElement('stop', { offset: '0%', 'stop-color': color.start }));
        markerGrad.appendChild(createSVGElement('stop', { offset: '100%', 'stop-color': color.end }));
        defs.appendChild(markerGrad);

        let areaGrad = createSVGElement('linearGradient', { id: areaGradId, x1: "0", x2: "0", y1: "0", y2: "1" });
  
      areaGrad.appendChild(createSVGElement('stop', { offset: '0%', 'stop-color': color.start, 'stop-opacity': '0.2' }));
        areaGrad.appendChild(createSVGElement('stop', { offset: '100%', 'stop-color': color.end, 'stop-opacity': '0' }));
defs.appendChild(areaGrad);

        // 配列にルールを追加 (元のコードにあった余白や不要な改行を削除)
        dynamicStyleParts.push(".line-" + cssPrefix + " { fill: none; stroke: " + color.end + "; stroke-width: 3; stroke-linejoin: round; stroke-linecap: round; filter: url(#shadow); }");
dynamicStyleParts.push(".marker-" + cssPrefix + " { fill: url(#" + markerGradId + "); stroke: white; stroke-width: 2; filter: url(#shadow); }");
dynamicStyleParts.push(".area-" + cssPrefix + " { fill: url(#" + areaGradId + "); }");
dynamicStyleParts.push(".data-label-" + cssPrefix + " { fill: " + color.end + "; }");
      });
// nl (改行) で結合
      const dynamicStyles = dynamicStyleParts.join(nl);
// スタイルシートに追加 (前後に改行を入れるとクリーン)
      style.textContent += nl + dynamicStyles + nl;
// === ▲▲▲【 修正ここまで 】▲▲▲

      const groups = {
        title: document.getElementById('title-group'),
        legend: document.getElementById('legend-group'),
        yAxis: document.getElementById('y-axis-group'),
        xAxis: document.getElementById('x-axis-group'),
        chartArea: document.getElementById('chart-area-group'),
        source: document.getElementById('source-group')
      };
// Clear previous elements
      for (const key in groups) {
        while (groups[key].firstChild) groups[key].removeChild(groups[key].firstChild);
}

      // === ▼ flatMap, ... (spread) 不使用に変更 ▼ ===
      const allValues = config.series.reduce(function(acc, s) { return acc.concat(s.values); }, []);
const dataMin = Math.min.apply(null, allValues.concat([config.yAxis.min]));
      const dataMax = Math.max.apply(null, allValues.concat([config.yAxis.max]));
// === ▲ 変更 ▲ ===

      let effectiveYAxisMin, effectiveYAxisMax;
const tickCount = config.yAxis.tickCount > 0 ? config.yAxis.tickCount : 1;
      const preliminaryRange = Math.max(1, config.yAxis.max - config.yAxis.min);
let tickInterval = preliminaryRange / tickCount;
      
      if (dataMin < 0) {
        const absLimit = Math.max(Math.abs(dataMin), dataMax);
if (tickInterval <= 0 || isNaN(tickInterval)) tickInterval = Math.max(1, absLimit / 5);
        effectiveYAxisMax = Math.ceil(absLimit / tickInterval) * tickInterval;
effectiveYAxisMin = -effectiveYAxisMax;
        if(config.yAxis.min > effectiveYAxisMin) effectiveYAxisMin = config.yAxis.min;
        if(config.yAxis.max < effectiveYAxisMax) effectiveYAxisMax = config.yAxis.max;
} else {
        effectiveYAxisMin = config.yAxis.min;
if (tickInterval <= 0 || isNaN(tickInterval)) tickInterval = Math.max(1, (dataMax - effectiveYAxisMin) / 5) || 1;
effectiveYAxisMax = Math.max(config.yAxis.max, Math.ceil(dataMax / tickInterval) * tickInterval);
      }

      if (effectiveYAxisMax <= effectiveYAxisMin) {
        effectiveYAxisMax = effectiveYAxisMin + tickInterval * tickCount;
if (effectiveYAxisMax <= effectiveYAxisMin) effectiveYAxisMax = effectiveYAxisMin + 1;
      }

      var yAxisRange = effectiveYAxisMax - effectiveYAxisMin;
// Calculate dynamic margin
      const tempYLabel = createSVGElement('text', { class: 'y-axis-value', style: 'visibility: hidden;' }, Math.round(effectiveYAxisMax));
svgRoot.appendChild(tempYLabel);
      const yAxisLabelWidth = tempYLabel.getComputedTextLength();
      svgRoot.removeChild(tempYLabel);
      const dynamicMarginLeft = Math.max(config.layout.marginLeft, yAxisLabelWidth + 15);
const chartAreaWidth = config.layout.width - dynamicMarginLeft - config.layout.marginRight;
      const chartAreaHeight = config.layout.height - config.layout.marginTop - config.layout.marginBottom;
const bottomY = config.layout.marginTop + chartAreaHeight;

      // X-axis scaling with padding
      const horizontalPadding = config.layout.horizontalPadding ||
0;
      const effectivePlotWidth = chartAreaWidth - (horizontalPadding * 2);
      const numPoints = config.xAxisLabels.length;
// === ▼ Arrow function (=>) 不使用に変更 ▼ ===
      const xScale = function(index) { return (dynamicMarginLeft + horizontalPadding) + (numPoints > 1 ? (effectivePlotWidth * (index / (numPoints - 1))) : effectivePlotWidth / 2);
};
      const yScale = function(value) { return bottomY - ((value - effectiveYAxisMin) / yAxisRange) * chartAreaHeight; };
// === ▲ 変更 ▲ ===
      
      const zeroLineY = yScale(0);

      // ▼▼▼【ここから変更】 2種類のアニメーション変数を定義 ▼▼▼
      // 1. グラフの「位置」用 (0.0 -> 1.0)
      var animProgress = (config.animation == null) 
          ? 1.0 
          : Math.max(0, Math.min(1, Number(config.animation)));

      // 2. データラベルの「透明度」用 (0.7 -> 1.0 でフェードイン)
      var animOpacity;
      var fadeStart = 0.7; // 0.7 からフェードイン開始
      if (animProgress <= fadeStart) {
          animOpacity = 0;
      } else {
          // 0.7 (0%) から 1.0 (100%) の間で 0 -> 1 に変化
          animOpacity = (animProgress - fadeStart) / (1.0 - fadeStart);
      }
      // ▲▲▲【ここまで変更】▲▲▲

      // Draw static elements
      groups.title.appendChild(createSVGElement('text', { x: 325, y: 40, class: 'title' }, config.title));
groups.title.appendChild(createSVGElement('text', { x: 325, y: 65, class: 'subtitle' }, config.subtitle));
groups.source.appendChild(createSVGElement('text', { x: config.layout.width - config.layout.marginRight, y: bottomY + 60, class: 'source' }, config.source));







// (236行目あたりから)
// Draw Legend
    const legendY = 95;
    const numLegends = config.series.length;
    // const assumedItemWidth = 130; // ← 削除
    const rectSize = 12;
    const rectTextGap = 6;

    // ▼▼▼【[A] 凡例の「理想幅」を動的に計算 (文字数 x フォントサイズ基準)】▼▼▼
    
    // 1. CSSからフォントサイズを取得 (styleタグの .legend-item { font-size: 13px; })
    const legendFontSize = 13; // px

    // 2. フォントサイズに対する「平均文字幅」の比率 (調整可能)
    // (日本語のような全角文字は 1.0 に近い, 英語のような半角文字は 0.6 に近い)
    // (現在の凡例「東京都」などは全角なので 1.0 を設定)
    const fontSizeCharWidthRatio = 1.0; 

    // 3. 凡例の「最大文字数」を計算
    let maxCharCount = 0;
    config.series.forEach(function(series) {
        const currentLabelLength = (series.label != null) ? series.label.length : 0;
        if (currentLabelLength > maxCharCount) {
            maxCharCount = currentLabelLength;
        }
    });

    // 4. 最大テキスト幅を「文字数 * フォントサイズ * 比率」で推定
    const estimatedMaxTextWidth = maxCharCount * legendFontSize * fontSizeCharWidthRatio;
    
    // 5. 凡例1つあたりの「理想的なスロット幅」を決定
    const legendItemEndPadding = 25; // テキストの右側に確保する余白
    const idealItemSlotWidth = rectSize + rectTextGap + estimatedMaxTextWidth + legendItemEndPadding;
    const totalIdealWidth = idealItemSlotWidth * numLegends;
    
    // ▲▲▲【[A] 変更ここまで】▲▲▲


    // ▼▼▼【[B] 描画幅を決定（はみ出さないように調整）】▼▼▼
    // (このブロックは変更なし、そのまま利用)

    // 3. 凡例を描画できる「全体の有効幅」を定義
    // (Y軸ラベルを考慮した左端 と 右マージンを考慮した右端 の間の幅)
    const availableLegendAreaLeft = dynamicMarginLeft;
    const availableLegendAreaRight = config.layout.width - config.layout.marginRight;
    const availableLegendAreaWidth = availableLegendAreaRight - availableLegendAreaLeft;

    let finalItemSlotWidth = 0;
    let legendBlockStartX = 0;

    // 4. 理想幅が「有効幅」に収まるか判定
    if (totalIdealWidth <= availableLegendAreaWidth) {
        // 【収まる場合】理想幅を使い、中央揃えにする
        finalItemSlotWidth = idealItemSlotWidth;
        const svgCenterX = svgRoot.viewBox.baseVal.x + (svgRoot.viewBox.baseVal.width / 2);
        legendBlockStartX = svgCenterX - (totalIdealWidth / 2);

    } else {
        // 【収まらない場合】有効幅に収まるよう、スロット幅を強制的に縮める（マスクしない）
        const constrainedItemSlotWidth = availableLegendAreaWidth / numLegends;
        finalItemSlotWidth = constrainedItemSlotWidth;
        // 左端から描画を開始する
        legendBlockStartX = availableLegendAreaLeft;
    }
    
    // ▲▲▲【[B] ロジックここまで】▲▲▲

    config.series.forEach(function(series, i) { // Use function() {
    
        // 5. 各凡例の開始X座標を「最終的なスロット幅」で計算
        const columnStartX = legendBlockStartX + (i * finalItemSlotWidth);
        
        // ▼▼▼【[C] マスク（非表示）ロジックを削除】▼▼▼
        // (はみ出すかどうかの if(return;) ブロックを削除)
        // ▲▲▲【[C] 削除】▲▲▲

        const itemGroup = createSVGElement('g');
        
        // === ▼ Arrow function (=>) と Optional Chaining (?.) 不使用に変更 ▼ ===
        var colorEntry = config.colors.find(function(c) { return c.id === series.id; });
        const color = (colorEntry ? colorEntry.end : undefined) || '#000';
        // === ▲ 変更 ▲ ===
        
        itemGroup.appendChild(createSVGElement('rect', { x: columnStartX, y: legendY, width: rectSize, height: rectSize, rx: 2, fill: color, filter: 'url(#shadow)' }));
        
        // ▼▼▼【[D] 省略記号ロジックの参照元を変更】▼▼▼
        let labelContent = series.label;
        const textStartX = columnStartX + rectSize + rectTextGap;
        const availableTextWidth = finalItemSlotWidth - (rectSize + rectTextGap) - (legendItemEndPadding / 2); 
        
        // 'maxTextWidth' -> 'estimatedMaxTextWidth' に変更
        if (estimatedMaxTextWidth > availableTextWidth) {
             // 非表示テキストで再測定（簡易的な省略処理）
            const tempText = createSVGElement('text', { class: 'legend-item', style: 'visibility: hidden;' }, labelContent);
            svgRoot.appendChild(tempText);
            if (tempText.getComputedTextLength() > availableTextWidth) { // ここの実測は残す
                labelContent = labelContent.substring(0, Math.max(1, Math.floor(labelContent.length * availableTextWidth / tempText.getComputedTextLength()))) + '..';
            }
            svgRoot.removeChild(tempText);
        }
        // ▲▲▲【[D] 変更ここまで】▲▲▲

        itemGroup.appendChild(createSVGElement('text', { x: textStartX, y: legendY + rectSize / 2, class: 'legend-item' }, labelContent));
        groups.legend.appendChild(itemGroup);
    });
// (289行目あたりまで)










// Draw Y-Axis unit label
      groups.yAxis.appendChild(createSVGElement('text', {
        x: 30, // Keep fixed X position
        y: config.layout.marginTop - 15,
        class: 'y-axis-unit-label'
      }, config.yAxisUnitLabel));
// Draw Y-Axis ticks and grid lines
      const finalTickStep = yAxisRange / tickCount;
for (let i = 0; i <= tickCount; i++) {
        const tickValue = effectiveYAxisMin + (finalTickStep * i);
const y = yScale(tickValue);
        groups.yAxis.appendChild(createSVGElement('text', { x: dynamicMarginLeft - 10, y, class: 'y-axis-value', 'dominant-baseline': 'middle' }, Math.round(tickValue)));
if (Math.abs(tickValue) > 1e-6 || effectiveYAxisMin >= 0) {
          groups.yAxis.appendChild(createSVGElement('line', { x1: dynamicMarginLeft, y1: y, x2: config.layout.width - config.layout.marginRight, y2: y, class: 'grid-line' }));
}
      }

      // Draw Zero/Bottom Line
      if (effectiveYAxisMin < 0 && effectiveYAxisMax > 0) {
        groups.xAxis.appendChild(createSVGElement('line', { x1: dynamicMarginLeft, y1: zeroLineY, x2: config.layout.width - config.layout.marginRight, y2: zeroLineY, class: 'zero-line' }));
}
      groups.xAxis.appendChild(createSVGElement('line', { x1: dynamicMarginLeft, y1: bottomY, x2: config.layout.width - config.layout.marginRight, y2: bottomY, class: 'zero-line' }));



// Draw X-Axis Labels
    const labelSlotWidth = effectivePlotWidth / (numPoints > 1 ? numPoints - 1 : 1);
    
    // ▼▼▼【ここから追加】回転ロジック ▼▼▼
    
    // 1. ラベルを回転させ始める「幅」のしきい値を定義 (例: 40ピクセル)
    const rotationThreshold = 40; 
    
    // 2. 描画幅がしきい値を下回るか判定 (データが2点以上の場合のみ)
    const needsRotation = (labelSlotWidth < rotationThreshold && numPoints > 1);

    // 3. ラベルのY位置を決定 (前回の修正を反映)
    // 回転する場合は少し高めの位置 (Y=25) にし、回転しない場合は (Y=30) にする
    const labelYOffset = needsRotation ? bottomY + 25 : bottomY + 30;

    // ▲▲▲【ここまで追加】▲▲▲

    config.xAxisLabels.forEach(function(label, i) { // Use function()
      let labelContent = label;
      
      // X座標は共通
      const x = xScale(i); 
      
      // 省略記号 (..) のロジック
      const tempText = createSVGElement('text', { class: 'axis-label', style: 'visibility: hidden;' });
      svgRoot.appendChild(tempText);
      tempText.textContent = label;
      
      // ※回転する場合は省略記号ロジックをスキップする (回転すれば読めるため)
      if (numPoints > 1 && tempText.getComputedTextLength() > labelSlotWidth * 0.9 && !needsRotation) {
        labelContent = label.substring(0, Math.max(1, Math.floor(label.length * labelSlotWidth * 0.8 / tempText.getComputedTextLength()))) + '..';
      }
      svgRoot.removeChild(tempText);
      
      // ▼▼▼【ここから変更】回転を適用 ▼▼▼
      
      // 1. 共通の属性オブジェクトを作成
      var attributes = {
        x: x,
        y: labelYOffset, // 上で計算したY位置
        class: 'axis-label'
        // 'text-anchor' はここで設定しない (CSSの 'middle' が適用される)
      };

      // 2. 回転が必要な場合のみ、属性を上書き・追加
      if (needsRotation) {
        // text-anchor: 'end' に変更し、X座標がラベルの右端に来るようにする
        attributes['text-anchor'] = 'end';
        
        // dominant-baseline: 'middle' を追加し、Y座標基準で垂直中央揃えにする
        attributes['dominant-baseline'] = 'middle';
        
        // transform 属性で回転を適用 (-45度)
        // 'rotate(角度, 中心のX座標, 中心のY座標)'
        attributes.transform = 'rotate(-45, ' + x + ', ' + labelYOffset + ')';
        
      } else {
        // 回転しない場合は、CSSで定義された 'text-anchor: middle' がそのまま使われる
      }

      // 3. 最終的な属性で要素を作成
      groups.xAxis.appendChild(createSVGElement('text', attributes, labelContent));
      
      // ▲▲▲【ここまで変更】▲▲▲
    });








// (299行目あたりから)

// Plot data
    const lastDataPoints = [];
    
    // ▼▼▼【[A] "1つおき"のロジックを series.forEach ループの「内部」に移動】▼▼▼
    
    // 1. 総合計文字数のしきい値を設定 (この値は調整可能です)
    const totalLabelCharThreshold = 75; // 例: 1系列で75文字以上で間引き

    config.series.forEach(function(series, seriesIndex) { // Use function()
        if (!series.values || series.values.length === 0) return;
        const cssPrefix = series.id.toLowerCase();
        
        const areaBaselineY = yScale(Math.max(0, effectiveYAxisMin));
        
        // 1. 各点のX, 最終Y, アニメーションY, 値を計算
        var pointsData = series.values.map(function(v, i) {
            var x = xScale(i);
            var finalY = yScale(v);
            // グラフ位置のアニメーション (0.0 -> 1.0 でせり上がる)
            var animatedY = areaBaselineY + (finalY - areaBaselineY) * animProgress;
            return {
                x: x,
                finalY: finalY,     // ラベル位置決め用
                animatedY: animatedY, // 線/マーカー/エリアの描画用
                value: v
            };
        });

        // 2. 描画用の points 文字列 (animatedY を使用)
        var points = pointsData.map(function(d) { return d.x + ',' + d.animatedY; }).join(' ');
        
        // 3. 描画用の areaPoints 文字列 (animatedY を使用)
        var areaPoints = xScale(0) + ',' + areaBaselineY 
            + ' ' + points + ' ' + xScale(series.values.length - 1) + ',' + areaBaselineY;
        
        groups.chartArea.appendChild(createSVGElement('polygon', { class: 'area-' + cssPrefix, points: areaPoints }));
        groups.chartArea.appendChild(createSVGElement('polyline', { class: 'line-' + cssPrefix, points: points }));

        // 4.【この系列】の総合計文字数を計算
        var totalLabelChars = 0;
        series.values.forEach(function(value) {
            totalLabelChars += (value != null) ? value.toString().length : 0;
        });
        
        // 5.【この系列】がしきい値を超えたかどうかのフラグを立てる
        const skipEveryOtherForThisSeries = (totalLabelChars > totalLabelCharThreshold);

        // 6. マーカー描画とラベル用データ格納
        pointsData.forEach(function(pointData, i) {
            
            // 7. ラベル衝突判定用に {x, y, ...} を格納
            var pointForLabel = { 
                x: pointData.x, 
                y: pointData.finalY, // <--【重要】finalY (最終Y) を使う
                value: pointData.value, 
                seriesIndex: seriesIndex,
                
                // ▼▼▼【[B] "1つおき"の判定結果を格納】▼▼▼
                // (フラグがtrue AND インデックスが奇数) の場合に .skip = true を設定
                skip: (skipEveryOtherForThisSeries && i % 2 === 1)
            };
            lastDataPoints.push(pointForLabel);
            
            // マーカー(circle)は「アニメーションY」で描画
            groups.chartArea.appendChild(createSVGElement('circle', { 
                cx: pointData.x, 
                cy: pointData.animatedY, // <-- 「アニメーションY」で描画
                r: config.lineOptions.markerRadius, 
                class: 'marker-' + cssPrefix 
            }));
        });
    });
    // ▲▲▲【[A] ロジックの移動ここまで】▲▲▲





// (360行目あたりから)
// Add Data Labels
    lastDataPoints.sort(function(a, b) { return a.x - b.x || a.y - b.y; });

    // ▼▼▼【[C] グローバルな文字数カウントを削除 (処理済み)】▼▼▼

    const drawnLabels = [];

    // ▼▼▼【[D] (point, i) の 'i' は不要なため削除】▼▼▼
    lastDataPoints.forEach(function(point) { // Use function()

        // ▼▼▼【[E] "1つおき"スキップロジックの「参照」】▼▼▼
        // point オブジェクトに格納した .skip フラグを参照する
        if (point.skip) {
            return; // この点の処理をスキップ
        }
        // ▲▲▲【[E] 変更ここまで】▲▲▲

        const series = config.series[point.seriesIndex];
        const cssPrefix = series.id.toLowerCase();

        // 衝突判定の感度 (px)
        const verticalThreshold = 18; // ラベル同士のY間隔
        const horizontalThreshold = labelSlotWidth * 0.4;
        const pointThreshold = config.lineOptions.markerRadius + 12; // ラベルと点のY間隔

        // --- Position 1: Above (デフォルト) ---
        const yAbove = point.y - config.lineOptions.dataLabelOffsetY - config.lineOptions.markerRadius;
        // --- Position 2: Below ---
        const yBelow = point.y + config.lineOptions.dataLabelOffsetY + config.lineOptions.markerRadius + 6;

        let finalLabelY = yAbove; // 最終的なY座標。デフォルトは「上」
        let shouldDraw = true;    // 描画するかどうかのフラグ

        // ▼▼▼【[J] ハイブリッド衝突判定ロジック】▼▼▼
        
        // 1.「上」の位置の衝突チェック
        let collidesUpLabel = false;
        let collidesUpPoint = false;
        for (const drawn of drawnLabels) { // Check vs Labels
            if (Math.abs(point.x - drawn.x) < horizontalThreshold && Math.abs(yAbove - drawn.y) < verticalThreshold) {
                collidesUpLabel = true;
                break;
            }
        }
        if (!collidesUpLabel) { // ラベルと重ならなければ、点ともチェック
             for (const otherPoint of lastDataPoints) { 
                if (point.seriesIndex === otherPoint.seriesIndex) continue;
                if (Math.abs(point.x - otherPoint.x) < horizontalThreshold && Math.abs(yAbove - otherPoint.y) < pointThreshold) {
                    collidesUpPoint = true;
                    break;
                }
            }
        }

        // 2.「下」の位置の衝突チェック
        let collidesDownLabel = false;
        // (「上」が何らかの理由で不採用の可能性がある場合のみ、「下」をチェック)
        if (collidesUpLabel || collidesUpPoint) {
            for (const drawn of drawnLabels) { // Check vs Labels
                if (Math.abs(point.x - drawn.x) < horizontalThreshold && Math.abs(yBelow - drawn.y) < verticalThreshold) {
                    collidesDownLabel = true;
                    break;
                }
            }
        }

        // 3. 最終的な位置と描画可否を決定
        
        if (collidesUpLabel) {
            // Case 1: 「上」が「ラベル」と衝突。
            // 「下」を試すしかない。
            if (collidesDownLabel) {
                // Case 1a: 「下」も「ラベル」と衝突。非表示。
                shouldDraw = false;
            } else {
                // Case 1b: 「下」は「ラベル」と衝突しない。
                // (点との衝突は許容する)
                finalLabelY = yBelow;
            }
        } else if (collidesUpPoint) {
            // Case 2: 「上」は「ラベル」とは衝突しないが、「点」と衝突。
            // 「下」を試す。
            if (collidesDownLabel) {
                // Case 2a: 「下」は「ラベル」と衝突。
                // 「上」（点と衝突）に戻す。
                finalLabelY = yAbove;
            } else {
                // Case 2b: 「下」は「ラベル」とも衝突しない。
                // (点と衝突するかは問わず)「下」に移動。
                finalLabelY = yBelow;
            }
        } else {
            // Case 3: 「上」は「ラベル」とも「点」とも衝突しない。
            // (そのまま「上」を採用)
            finalLabelY = yAbove;
        }

        // ▲▲▲【[J] 変更ここまで】▲▲▲

        // 4. 描画フラグが true の場合のみ、描画とリストへの追加を行う
        if (shouldDraw) {
            groups.chartArea.appendChild(createSVGElement('text', { 
                x: point.x, 
                y: finalLabelY, // 決定されたY座標
                class: 'data-label data-label-' + cssPrefix,
                style: 'opacity: ' + animOpacity 
            }, point.value));
            
            // 描画済みリストには、実際に描画した座標を格納する
            drawnLabels.push({x: point.x, y: finalLabelY});
        }
    });

// (390行目あたりまで)






}


    // Execute chart build
    try {
      const configJson = document.getElementById('chart-json-data').textContent;
if (!configJson || configJson.trim() === '') {
        throw new Error("JSON data is empty.");
}
      let fullConfig;
      try {
        fullConfig = JSON.parse(configJson);
} catch (parseError) {
        // === ▼ バッククォート不使用に変更 ▼ ===
        throw new Error('JSON parsing error: ' + parseError.message);
// === ▲ 変更 ▲ ===
      }

      // ▼▼▼【変更点】ネストされた "data" キーから設定を読み込む ▼▼▼
      if (!fullConfig || typeof fullConfig !== 'object' || !fullConfig.data) {
        throw new Error("Invalid JSON structure: Missing 'data' key.");
}
      const config = fullConfig.data;
// config は data オブジェクトを指す
      // ▲▲▲ ここまで ▲▲▲

      // layout の既定値処理（Gemプロンプトは layout を省略してよい仕様。無いと config.layout.width で落ちて
      //   「グラフ描画エラー」になる。指定があれば個々の値だけ上書きする）
      const defaultLayout = { width: 650, height: 510, marginTop: 140, marginBottom: 80, marginLeft: 75, marginRight: 35, horizontalPadding: 20 };
      config.layout = Object.assign({}, defaultLayout, (config.layout && typeof config.layout === 'object') ? config.layout : {});

      // ▼▼▼【ここから変更】 "colors" が文字列だった場合の処理を分岐 ▼▼▼
      if (config.colors && typeof config.colors === 'string') {
        
        // series から ID の配列を生成
        const seriesIds = config.series.map(function(s) { return s.id; });
const numColors = seriesIds.length;
        
        let generatedColors; // ここで宣言
        const colorKeyword = config.colors.toLowerCase();
const gradientDarken = 15; // #gemini に適用される濃淡差

        // ▼▼▼【分岐1】#gemini の場合 ▼▼▼
        if (colorKeyword === '#gemini') {
          // 濃いピンク -> 紫 -> 青
          const GEMINI_STOPS = ['#E68A9C', '#9F63D0', '#616AD8'];
const hexPalette = generateGradientScale(GEMINI_STOPS, numColors);
          generatedColors = [];
          
          for (let i = 0; i < numColors; i++) {
            const hex_start = hexPalette[i];
const rgb_start = hexToRgb(hex_start);
            const hsl = rgbToHsl(rgb_start.r, rgb_start.g, rgb_start.b);
            const h = hsl[0], s = hsl[1], l = hsl[2];
const l_end = Math.max(0, Math.min(100, l - gradientDarken));
            const rgb_end = hslToRgb(h, s, l_end);
            const hex_end = rgbToHex(rgb_end[0], rgb_end[1], rgb_end[2]);
generatedColors.push({
              id: seriesIds[i], // series の ID を使用
              start: hex_start,
              end: hex_end
            });
}
        
        // ▼▼▼【分岐2: ★ロジック変更★】#night の場合 ▼▼▼
        } else if (colorKeyword === '#night') {
          // ユーザー指定の固定カラーパレット (start と end のペア)
          var NIGHT_COLORS = [
            { start: '#8d58d3', end: '#704ca4' }, // 0: 項目 A (紫)
            { start: '#8f83d6', end: 
'#7469a8' }, // 1: 項目 B (青紫)
            { start: '#7891dd', end: '#5c6fbb' }, // 2: 項目 C (青)
            { start: '#50a89d', end: '#258a7f' }, // 3: 項目 D (青緑)
            { start: '#54c5d5', end: '#34a0b0' }  // 4: 項目 E (水色)
          ];
generatedColors = [];
          
          var M_numColors = numColors;
          var N_paletteSize = NIGHT_COLORS.length;
// ▼▼▼ 色の数がパレットサイズ以下(5色以下)の場合、均等に色を抽出する ▼▼▼
          if (M_numColors <= N_paletteSize) {
            // 例: 3色 (M=3) の場合、パレット(N=5)から 0, 2, 4 を選ぶ
            for (var i = 0; i < M_numColors; i++) {
              // i (0, 1, 2) を t (0.0, 0.5, 1.0) に正規化
              var progress = (M_numColors 
=== 1) ? 0 : (i / (M_numColors - 1));
// t (0.0, 0.5, 1.0) を パレットのインデックス (0, 2, 4) に変換
              var colorIndex = Math.round(progress * (N_paletteSize - 1));
var colorPair = NIGHT_COLORS[colorIndex];
              
              generatedColors.push({
                id: seriesIds[i],
                start: colorPair.start,
                end: colorPair.end
              });
}
          } else {
            // ▼▼▼ 色の数がパレットサイズより多い(6色以上)の場合、従来通り順番に割り当て、最後を繰り返す ▼▼▼
            for (var i = 0; i < M_numColors; i++) {
              var colorPair = NIGHT_COLORS[i] ||
NIGHT_COLORS[N_paletteSize - 1];
              
              generatedColors.push({
                id: seriesIds[i],
                start: colorPair.start,
                end: colorPair.end
              });
}
          }
          // ▲▲▲【ロジック変更ここまで】▲▲▲

        } else {
          // ▼▼▼【分岐3】HEXコード（単色・交互濃淡）の場合 ▼▼▼
          const primaryColor = config.colors;
// (generateMonochromaticScale関数内で gradientDarken が7で固定されている)
          generatedColors = generateMonochromaticScale(primaryColor, seriesIds);
// HEXコードが無効だった場合
          if (generatedColors.length === 0) {
             throw new Error("Invalid HEX color string in 'colors': " + primaryColor);
}
        }
        
        // 最終的な色設定を config に上書き
        if (generatedColors && generatedColors.length > 0) {
          config.colors = generatedColors;
} else {
           throw new Error("Failed to generate colors from keyword: " + config.colors);
}
        
      } else if (!config.colors || !Array.isArray(config.colors)) {
         // "colors" が配列でも文字列でもない場合はエラー
         throw new Error("'colors' must be an array of color objects or a single primary color string.");
}

      if (!config.lineOptions) {
        config.lineOptions = {};
// オブジェクト自体がない場合
      }
      // null または undefined の場合にデフォルト値を設定
      if (config.lineOptions.markerRadius == null) {
        config.lineOptions.markerRadius = 5;
}
      if (config.lineOptions.dataLabelOffsetY == null) {
        config.lineOptions.dataLabelOffsetY = 8;
}

      if (!config || typeof config !== 'object' || !config.series || !Array.isArray(config.series)) {
        throw new Error("Invalid JSON structure: Missing required fields like 'series' inside 'data'.");
}
      buildChart(config);
    } catch (e) {
      console.error("Chart Error:", e);
const svgRoot = document.getElementById('multi-line-chart-svg');
      const existingError = svgRoot.querySelector('.error-message');
      if(existingError) existingError.remove();
const textEl = createSVGElement('text', {
        x: svgRoot.viewBox.baseVal.width / 2 + (svgRoot.viewBox.baseVal.x || 0), // (viewBax -> viewBox)
        y: svgRoot.viewBox.baseVal.height / 2 + (svgRoot.viewBox.baseVal.y || 0), // (viewBax -> viewBox)
        'text-anchor': 'middle',
        fill: 'red',
        'font-family': "'Noto Sans JP', sans-serif",
        class: 'error-message'
      });
textEl.textContent = e.message.includes('JSON') ? 'JSONデータにエラーがあります。' : 'グラフ描画エラーが発生しました。';
      if (e.message.includes('JSON parsing error')) {
        // === ▼ バッククォート不使用に変更 ▼ ===
        textEl.textContent += ' (' + e.message.split(': ')[1] + ')';
// === ▲ 変更 ▲ ===
      }
      svgRoot.appendChild(textEl);
}

    // ]]>
  </script>
</svg>
`;




const STACKED_BAR_CHART_TEMPLATE = `
<svg width="600" height="550" viewBox="25 0 575 570" xmlns="http://www.w3.org/2000/svg" font-family="'Noto Sans JP', sans-serif" id="stacked-bar-chart-svg">

  <script id="chart-json-data" type="application/json">
    {
      "chartType": "stacked-bar",
      "data": {
        "title": "サンプルグラフ：カテゴリ別数量",
        "subtitle": "（月別サンプルデータ）",
        "source": "出典：サンプルデータソース",
        "yAxisUnitLabel": "（単位）",
        "colors": [
          { "id": "A", "start": "#e68a9c", "end": "#d96d8f" },
    
      { "id": "B", "start": "#b469b8", "end": "#a656ad" },
          { "id": "C", "start": "#9f63d0", "end": "#8c4fc8" },
          { "id": "D", "start": "#7c6ce8", "end": "#6b5ce0" },
          { "id": "E", "start": "#616ad8", "end": "#5059d1" }
        ],
        "legendLabels": ["項目 A", "項目 B", "項目 C", "項目 D", "項目 E"],
        "barData": [
 
         { "label": "サンプル 1", "values": [60, 50, 40, 25, 20] },
          { "label": "サンプル 2", "values": [90, 60, 50, 40, 25] },
          { "label": "サンプル 3", "values": [60, 80, 70, 50, 30] },
          { "label": "サンプル 4", "values": [40, 50, 60, 70, 80] },
          { "label": "サンプル 5", "values": [70, 30, 55, 45, 65] }
 
       ],
        "layout": {
          "width": 600,
          "height": 550,
          "marginTop": 170,
          "marginBottom": 50,
          "marginLeft": 75,
          "marginRight": 50
        },
        "barOptions": {
 
         "width": 50,
          "cornerRadius": 4,
          "totalLabelOffset": 10
        },
        "yAxis": {
          "max": 300,
          "tickCount": 3
        },

        "animation": 1
      }
    }

  </script>
  <defs>
    <filter id="shadow" x="-10%" 
y="-10%" width="120%" height="120%"><feDropShadow dx="1" dy="2" stdDeviation="1.5" flood-color="#000" flood-opacity="0.3"/></filter>
    <filter id="text-shadow"><feDropShadow dx="1" dy="1" stdDeviation="1" flood-color="#000" flood-opacity="0.7"/></filter>
  </defs>

  <style>
    @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&amp;family=Roboto:wght@400;700&amp;display=swap');
/* 基本スタイル (デフォルト Noto Sans JP) */
    .bar-segment { filter: url(#shadow);
}
    .axis-label { font-size: 13px; fill: #5f6368; text-anchor: middle;
}
    /* ▼▼▼【変更点】 title の fill を #5f6368 に変更 ▼▼▼ */
    .title { font-size: 24px;
font-weight: 500; text-anchor: middle; fill: #5f6368; }
    .subtitle { font-size: 16px; text-anchor: middle; fill: #5f6368;
} /* 変更なし */
    .source { font-size: 11px; fill: #70757a; text-anchor: end;
}
    .grid-line { stroke: #e0e0e0; stroke-dasharray: 2 2; stroke-width: 0.8;
}
    .legend-item { font-size: 13px; fill: #202124; dominant-baseline: middle;
}

    /* 数字用のフォント指定 */
    .segment-label { font-family: 'Roboto', sans-serif; font-size: 13px; font-weight: normal;
fill: white; filter: url(#text-shadow); text-anchor: middle; pointer-events: none; }
    .total-label { font-family: 'Roboto', sans-serif; font-size: 14px;
font-weight: bold; fill: #5f6368; text-anchor: middle; }
    .y-axis-value { font-family: 'Roboto', sans-serif; font-size: 12px; fill: #757575;
text-anchor: end; }
  </style>

  <g id="title-group"></g>
  <g id="legend-group"></g>
  <g id="y-axis-group"></g>
  <g id="chart-area-group"></g>
  <g id="source-group"></g>

  <script type="text/javascript">
    // ▼▼▼ 修正点: <![CDATA[ を削除 ▼▼▼

    // =================================================================
    // この下にあるロジックコードは変更する必要はありません。
    // =================================================================
    var svgNS = "http://www.w3.org/2000/svg";
function createSVGElement(name, attributes, textContent) {
      var el = document.createElementNS(svgNS, name);
for (var key in attributes) el.setAttribute(key, attributes[key]);
      if (textContent !== undefined && textContent !== null) {
        el.textContent = textContent;
}
      return el;
    }

    // =================================================================
    // ▼▼▼【ここから追加】色計算用のヘルパー関数 (IE互換のため var を使用) ▼▼▼
    // =================================================================


    /**
     * フォントサイズ(px)に基づいて簡易的なテキスト幅を計算する
     * (getBBox() を使わない簡易版)
     */
    function getEstimatedTextWidth(text, fontSize) {
        var width = 0;
        // .legend-item のフォントサイズ(13px)を基準にする
        var fullWidthCharSize = fontSize; // 全角文字の幅 (font-size と同じと仮定)
        var halfWidthCharSize = fontSize * 0.6; // 半角英数の幅 (font-size の 60% と仮定)
        
        if (!text) return 0;

        for (var i = 0; i < text.length; i++) {
            var charCode = text.charCodeAt(i);
            // 簡易的な半角判定 (ASCII範囲 + 半角カタカナ)
            if ((charCode >= 0x0020 && charCode <= 0x007E) || (charCode >= 0xFF61 && charCode <= 0xFF9F)) {
                width += halfWidthCharSize;
            } else {
                // それ以外は全角として扱う
                width += fullWidthCharSize;
            }
        }
        return width;
    }

    
    /** HEX (例: #FF0000) を RGB (例: {r: 255, g: 0, b: 0}) に変換 */
    function hexToRgb(hex) {
      var shorthandRegex = /^#?([a-f0-9])([a-f0-9])([a-f0-9])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
        return r + r + g + g + b + b;
      });
var result = /^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex);
      return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
      } : null;
}

    /** RGB (0-255) を HSL (H: 0-360, S: 0-100, L: 0-100) に変換 */
    function rgbToHsl(r, g, b) {
      r /= 255;
g /= 255; b /= 255;
      var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
      l = Math.round(l * 100);
// %に変換

      if (max == min) {
        h = s = 0;
// achromatic
      } else {
        var d = max - min;
s = l > 50 ? d / (2 - max - min) : d / (max + min);
s = Math.round(s * 100); // %に変換
        switch (max) {
          case r: h = (g - b) / d + (g < b ? 6 : 0);
break;
          case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
        h /= 6;
}
      h = Math.round(h * 360);
// 角度に変換
      return [h, s, l];
}

    /** HSL (H: 0-360, S: 0-100, L: 0-100) を RGB (0-255) に変換 */
    function hslToRgb(h, s, l) {
      var r, g, b;
h /= 360; // 0-1 に
      s /= 100;
// 0-1 に
      l /= 100;
// 0-1 に

      if (s == 0) {
        r = g = b = l;
// achromatic
      } else {
        function hue2rgb(p, q, t) {
          if (t < 0) t += 1;
if (t > 1) t -= 1;
          if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
          if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
        }
        var q = l < 0.5 ?
l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
      }
      return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}

    /** RGB (0-255) を HEX (例: #FF0000) に変換 */
    function rgbToHex(r, g, b) {
      function componentToHex(c) {
        var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
      return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}
    
    /** 2つのHEXカラー間を線形補間する */
    function linearInterpolateColor(hex1, hex2, t) {
      var rgb1 = hexToRgb(hex1);
var rgb2 = hexToRgb(hex2);
      
      var r = Math.round(rgb1.r + (rgb2.r - rgb1.r) * t);
var g = Math.round(rgb1.g + (rgb2.g - rgb1.g) * t);
      var b = Math.round(rgb1.b + (rgb2.b - rgb1.b) * t);
return rgbToHex(r, g, b);
    }

    /** 複数のカラーストップからグラデーションパレット（HEX配列）を生成する */
    function generateGradientScale(gradientStops, numSteps) {
      var palette = [];
if (numSteps <= 0) return [];
      if (numSteps === 1) return [gradientStops[0]];
// 1色だけなら最初の色
      
      var numSegments = gradientStops.length - 1;
for (var i = 0; i < numSteps; i++) {
        // 全体の進捗 (0.0 ... 1.0)
        var t_global = (numSteps === 1) ?
0 : (i / (numSteps - 1));
        
        // どのセグメントにいるか
        var segmentProgress = t_global * numSegments;
var stopIndex1 = Math.floor(segmentProgress);
        var stopIndex2 = Math.min(stopIndex1 + 1, numSegments);
// そのセグメント内でのローカルな進捗 (0.0 ... 1.0)
        var t_local = segmentProgress - stopIndex1;
var hex1 = gradientStops[stopIndex1];
        var hex2 = gradientStops[stopIndex2];
        
        palette.push(linearInterpolateColor(hex1, hex2, t_local));
      }
      return palette;
}

    /**
     * 【単色用】プライマリーカラーとID配列から、自動計算された "colors" 配列を生成する
     * (交互の濃淡パターン)
     */
    function generateMonochromaticScale(primaryHex, colorIds) {
      var numSteps = colorIds.length;
if (numSteps === 0) return [];

      var rgb = hexToRgb(primaryHex);
      if (!rgb) return [];
// 無効なHEXの場合は空
      
      var hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
var h = hsl[0], s = hsl[1], l_mid_original = hsl[2];
      
      var gradientDarken = 7;
// (元のSVGのロジックに合わせ 7 を維持)
      var L_MAX = 80; 
      var L_MIN = 45;
var l_step_amount = numSteps > 1 ? (L_MAX - L_MIN) / (numSteps - 1) : 0;
      
      var l_steps_calc = [];
for (var i = 0; i < numSteps; i++) {
        var l_val = L_MAX - (i * l_step_amount);
l_steps_calc.push(l_val);
      }
      
      // ▼▼▼ 100%積み上げグラフから移植したロジック ▼▼▼
      var l_steps_light = [];
var l_steps_dark = [];
      var mid_index = Math.ceil(numSteps / 2);
// 明るいグループ (前から)
      for (var i = 0; i < mid_index; i++) {
        l_steps_light.push(l_steps_calc[i]);
}

      // 暗いグループ (中間から)
      for (var i = mid_index; i < numSteps; i++) {
        l_steps_dark.push(l_steps_calc[i]);
}

      var l_steps_start = [];
for (var i = 0; i < numSteps; i++) {
        if (i % 2 === 0) {
          l_steps_start.push(l_steps_light[i / 2]);
} else {
          l_steps_start.push(l_steps_dark[(i - 1) / 2]);
}
      }
      // ▲▲▲ 移植ロジックここまで ▲▲▲

      var colors = [];
for (var i = 0; i < numSteps; i++) {
        var l_start_step = l_steps_start[i];
var l_start = Math.max(0, Math.min(100, l_start_step));
        var l_end = Math.max(0, Math.min(100, l_start - gradientDarken));
        
        var s_adjusted = s;
if (l_start > 85) s_adjusted = s * (1 - (l_start - 85) / 15);
if (l_start < 20) s_adjusted = s * (l_start / 20);
        s_adjusted = Math.max(0, Math.min(100, s_adjusted));
var rgb_start = hslToRgb(h, s_adjusted, l_start);
        var hex_start = rgbToHex(rgb_start[0], rgb_start[1], rgb_start[2]);
        var rgb_end = hslToRgb(h, s_adjusted, l_end);
var hex_end = rgbToHex(rgb_end[0], rgb_end[1], rgb_end[2]);
        
        colors.push({
          id: colorIds[i],
          start: hex_start,
          end: hex_end
        });
}
      return colors;
    }
    // =================================================================
    // ▲▲▲【ここまで追加】▲▲▲
    // =================================================================


    function buildChart(config) {
      var chartConfig = config;
var colorConfig = config.colors; // (色分岐処理後に確定した配列が渡される)

      var svgRoot = document.getElementById('stacked-bar-chart-svg');
      var defs = svgRoot.querySelector('defs');
// Clear existing dynamic gradients
      var existingGrads = defs.querySelectorAll('linearGradient');
      existingGrads.forEach(function(grad) { grad.remove(); });
// Use function()
      
      // Clear existing dynamic styles by removing rules containing '-a' (assuming 'a' is always the first series ID)
      var styleContent = document.querySelector('style').textContent;
// Get current style content
      var dynamicRulePattern = /\.bar-[a-z]\s*{[^}]*}/g;
// Pattern to find dynamic rules like .bar-a, .bar-b
      styleContent = styleContent.replace(dynamicRulePattern, '');
// Remove existing dynamic rules
      document.querySelector('style').textContent = styleContent.trim();
// Update style element


      var dynamicStyles = [];
// [修正済み] '' から [] に変更
      colorConfig.forEach(function(color) { // Use function()
        // === ▼ バッククォート不使用に変更 ▼ ===
        var gradId = 'grad' + color.id;
        var cssClass = 'bar-' + color.id.toLowerCase();
        // === ▲ 変更 ▲ ===
        
        var gradient = createSVGElement('linearGradient', { id: gradId, x1:"0", x2:"0", y1:"0", y2:"1" });
      

        gradient.appendChild(createSVGElement('stop', { offset: '0%', 'stop-color': color.start }));
   
        gradient.appendChild(createSVGElement('stop', { offset: '100%', 'stop-color': color.end }));
        
        defs.appendChild(gradient);
        
        dynamicStyles.push('.' + cssClass + ' { fill: url(#' + gradId + '); }');
        // === ▲ 変更 ▲ ===
      });
// [修正済み] String.fromCharCode(10) を使って join し、スタイルに追加
      document.querySelector('style').textContent += dynamicStyles.join(String.fromCharCode(10));


      var titleGroup = document.getElementById('title-group');
var legendGroup = document.getElementById('legend-group');
      var yAxisGroup = document.getElementById('y-axis-group');
      var chartAreaGroup = document.getElementById('chart-area-group');
      var sourceGroup = document.getElementById('source-group');
// Clear previous chart elements
      var groups = [titleGroup, legendGroup, yAxisGroup, chartAreaGroup, sourceGroup];
for(var i = 0; i < groups.length; i++) {
        var group = groups[i];
while (group.firstChild) group.removeChild(group.firstChild);
      }

      var chartAreaWidth = chartConfig.layout.width - chartConfig.layout.marginLeft - chartConfig.layout.marginRight;
var chartAreaHeight = chartConfig.layout.height - chartConfig.layout.marginTop - chartConfig.layout.marginBottom;
      var bottomY = chartConfig.layout.marginTop + chartAreaHeight;
// === ▼ ... (spread) 不使用に変更 ▼ ===
      var totals = chartConfig.barData.map(function(d) { 
        return d.values.reduce(function(sum, v) { return sum + v; }, 0); 
      });
var maxTotal = Math.max.apply(null, totals.concat([config.yAxis.max]));
      // === ▲ 変更 ▲ ===

      // Include axis max
      var tickCount = chartConfig.yAxis.tickCount > 0 ?
chartConfig.yAxis.tickCount : 1;
      var preliminaryTickInterval = (config.yAxis.max > 0 && tickCount > 0) ? config.yAxis.max / tickCount : 1;
// Base interval on initial max
      var effectiveYAxisMax = Math.max(config.yAxis.max, Math.ceil(maxTotal / preliminaryTickInterval) * preliminaryTickInterval);
// Ensure it covers data, rounded up


      if (effectiveYAxisMax <= 0) effectiveYAxisMax = preliminaryTickInterval * tickCount;
// Handle zero/negative max case
      if (effectiveYAxisMax === 0) effectiveYAxisMax = 1;
// Absolute fallback

      var yAxisRange = effectiveYAxisMax;
// Since min is 0

      // === ▼ Arrow function (=>) 不使用に変更 ▼ ===
      var xScale = function(index) { return chartConfig.layout.marginLeft + (chartAreaWidth / chartConfig.barData.length) * (index + 0.5);
};
      var yScale = function(value) { return bottomY - (value / effectiveYAxisMax) * chartAreaHeight; };
// === ▲ 変更 ▲ ===

      // ▼▼▼【ここから変更】 2種類のアニメーション変数を定義 ▼▼▼
      // 1. グラフの「位置」用 (0.0 -> 1.0)
      var animProgress = (config.animation == null) 
          ? 1.0 
          : Math.max(0, Math.min(1, Number(config.animation)));

      // 2. データラベルの「透明度」用 (0.7 -> 1.0 でフェードイン)
      var animOpacity;
      var fadeStart = 0.7; // 0.7 からフェードイン開始
      if (animProgress <= fadeStart) {
          animOpacity = 0;
      } else {
          // 0.7 (0%) から 1.0 (100%) の間で 0 -> 1 に変化
          animOpacity = (animProgress - fadeStart) / (1.0 - fadeStart);
      }
      // ▲▲▲【ここまで変更】▲▲▲

      var svgWidth = svgRoot.viewBox.baseVal.width;
titleGroup.appendChild(createSVGElement('text', { x: svgWidth/2, y: 50, class: 'title' }, chartConfig.title));
      titleGroup.appendChild(createSVGElement('text', { x: svgWidth/2, y: 75, class: 'subtitle' }, chartConfig.subtitle));
sourceGroup.appendChild(createSVGElement('text', { x: 600, y: 555, class: 'source' }, chartConfig.source));




//
      // ▼▼▼ ここから差し替え後のコード ▼▼▼
      var legendY = 110;
      var numLegends = chartConfig.legendLabels.length;
      var legendStartX = chartConfig.layout.marginLeft;
      var rectSize = 12;
      var rectTextGap = 6;
      var legendFontSize = 13; // .legend-item の font-size
      var legendPadding = 15; // 項目間の最小パディング

      // 1. 各凡例の「必要な幅」を計算する
      // (getEstimatedTextWidth は別で定義)
      var requiredWidths = chartConfig.legendLabels.map(function(label) {
          var textWidth = getEstimatedTextWidth(label, legendFontSize);
          // 幅 = 四角 + 隙間 + テキスト幅 + 項目間パディング
          return rectSize + rectTextGap + textWidth + legendPadding;
      });

      // 2. 必要な合計幅を計算する (最後の項目のパディングは不要なので引く)
      var totalRequiredWidth = requiredWidths.reduce(function(sum, w) { return sum + w; }, 0) - legendPadding;
      if (numLegends === 0) totalRequiredWidth = 0;

      var useEqualWidths = false; // レイアウトモードフラグ
      var legendColumnWidth = 0; // 均等割りモードの場合に使う

      // 3. 描画エリア幅と比較
      if (totalRequiredWidth > chartAreaWidth || numLegends === 0) {
          // B) はみ出す場合 (または凡例ゼロ): 均等割りモード
          useEqualWidths = true;
          legendColumnWidth = (numLegends > 0) ? (chartAreaWidth / numLegends) : 0;
          // legendStartX はデフォルト (marginLeft) のまま
      } else {
          // A) 収まる場合: 可変幅モード
          useEqualWidths = false;
          // 全体を中央揃えにするための開始X座標を計算
          legendStartX = chartConfig.layout.marginLeft + (chartAreaWidth - totalRequiredWidth) / 2;
      }

      var currentX = legendStartX; // Aの場合のX座標トラッカー
      // (defs変数は buildChart 関数の冒頭で定義されています)

      // 4. 凡例を描画
      chartConfig.legendLabels.forEach(function(label, i) { // Use function()
        var cssClass = 'bar-' + colorConfig[i].id.toLowerCase();
        
        var columnStartX;
        var currentColumnWidth; // このカラムに割り当てられた幅

        if (useEqualWidths) {
            // B) 均等割りモード
            columnStartX = legendStartX + (i * legendColumnWidth);
            currentColumnWidth = legendColumnWidth;
        } else {
            // A) 可変幅モード
            columnStartX = currentX;
            currentColumnWidth = requiredWidths[i];
            currentX += currentColumnWidth; // 次の開始位置を更新
        }

        var itemGroup = createSVGElement('g');
        
        itemGroup.appendChild(createSVGElement('rect', {
          x: columnStartX, y: legendY, width: rectSize, height: rectSize, rx: 2, class: 'bar-segment ' + cssClass
        }));
        
        var textX = columnStartX + rectSize + rectTextGap;
        
        // --- ▼ クリッピングパスのロジック (はみ出し防止) ▼ ---
        
        var clipId = 'legend-clip-' + i;
        
        // テキストを表示する「枠」の幅を計算
        // (使える幅から四角と隙間、パディングの半分を引く)
        var clipMargin = (legendPadding / 2);
        if (useEqualWidths) clipMargin = 5; // 均等割りの時は 5px 固定

        var clipWidth = (currentColumnWidth - rectSize - rectTextGap - clipMargin); 
        clipWidth = Math.max(0, clipWidth); 

        // <clipPath> 要素（切り抜き枠）を作成
        var clipPath = createSVGElement('clipPath', { id: clipId });
        clipPath.appendChild(createSVGElement('rect', {
            x: textX, // テキスト開始X座標
            y: legendY - (rectSize / 2), 
            width: clipWidth, // 計算した幅
            height: rectSize * 2 
        }));
        
        // <defs> タグに <clipPath> を追加
        defs.appendChild(clipPath);

        // テキスト要素に 'clip-path' 属性を追加
        itemGroup.appendChild(createSVGElement('text', { 
            x: textX, 
            y: legendY + rectSize / 2, 
            class: 'legend-item',
            'clip-path': 'url(#' + clipId + ')' 
        }, label));
        
        // --- ▲ クリッピングパスここまで ▲ ---
       
         legendGroup.appendChild(itemGroup);
      });
// ▲▲▲ ここまで差し替え後のコード ▲▲▲
//




yAxisGroup.appendChild(createSVGElement('text', { x: 65, y: chartConfig.layout.marginTop - 30, class: 'y-axis-value' }, chartConfig.yAxisUnitLabel));
      var finalTickStep = yAxisRange / tickCount;
// Use final range
      for (var i = 0; i <= tickCount; i++) {
        var value = Math.round(finalTickStep * i);
// Calculate value based on step
        var y = yScale(value);
yAxisGroup.appendChild(createSVGElement('line', { x1: chartConfig.layout.marginLeft, y1: y, x2: chartConfig.layout.width - chartConfig.layout.marginRight, y2: y, class: 'grid-line' }));
yAxisGroup.appendChild(createSVGElement('text', { x: 65, y: y, class: 'y-axis-value', 'dominant-baseline': 'middle' }, value));
}

      chartConfig.barData.forEach(function(bar, index) { // Use function()
        var barGroup = createSVGElement('g', { class: 'bar-group' });
        var centerX = xScale(index);
        var total = bar.values.reduce(function(sum, v) { return sum + v; }, 0); // Use function()

        // ▼▼▼【ここから変更】 合計値とアニメーション後の合計値を計算 ▼▼▼
        var finalTotal = total;
        var animatedTotal = finalTotal * animProgress;
        // ▲▲▲【ここまで変更】▲▲▲

        // Only draw total label if total is positive
        if (finalTotal > 0) {
          // ▼▼▼【ここから変更】 Y座標を animatedTotal に、透明度を animOpacity に ▼▼▼
          barGroup.appendChild(createSVGElement('text', { 
            x: centerX, 
            y: yScale(animatedTotal) - chartConfig.barOptions.totalLabelOffset, // アニメーション後のY座標
            class: 'total-label',
            style: 'opacity: ' + animOpacity // 0.7-1.0でフェードイン
          }, finalTotal)); // 表示する数字は最終的な合計値
          // ▲▲▲【ここまで変更】▲▲▲
        }
        barGroup.appendChild(createSVGElement('text', { x: centerX, y: bottomY + 20, class: 'axis-label' }, bar.label));

        var yOffset = 0;
        

        bar.values.slice().reverse().forEach(function(value, i) { // Use function()
          if (value <= 0) return; // Skip zero or negative values for segments

          // ▼▼▼【ここから変更】 アニメーション適用後のY座標と高さを計算 ▼▼▼
          var finalValue = value;
          var finalYOffset = yOffset;
          
          var animatedYOffset = finalYOffset * animProgress;
          var animatedValue = finalValue * animProgress;
          
          var segmentIndex = bar.values.length - 1 - i;
          var cssClass = 'bar-' + colorConfig[segmentIndex].id.toLowerCase();

          var yTop = yScale(animatedYOffset + animatedValue); // アニメーション後のTop
          var yBottom = yScale(animatedYOffset); // アニメーション後のBottom
          var height = yBottom - yTop; // アニメーション後の高さ
          
          if (height <= 0.5) return; // 0.5px以下は描画スキップ
          // ▲▲▲【ここまで変更】▲▲▲
          
          var r = chartConfig.barOptions.cornerRadius;
var x = centerX - chartConfig.barOptions.width / 2;
          var w = chartConfig.barOptions.width;

          var segment;
          // ▼▼▼【ここから変更】 判定を finalYOffset と finalTotal に変更 ▼▼▼
          var isBottomSegment = (finalYOffset === 0);
          var isTopSegment = Math.abs((finalYOffset + finalValue) - finalTotal) < 0.01;
          // ▲▲▲【ここまで変更】▲▲▲

          if (isBottomSegment && isTopSegment) { // Single segment bar
            segment = createSVGElement('rect', { x: x, y: yTop, width: w, height: height, rx: r, ry: r, class: 'bar-segment ' + cssClass });
// クラス結合を修正
          } else if (isBottomSegment) {
            // === ▼▼▼ 修正点: 文字列リテラル内の改行を削除 ▼▼▼ ===
            // ▼▼▼【ここから変更】 yTop/yBottom/height はアニメーション後の値を使用 ▼▼▼
            var d = 'M ' + x + ' ' + yTop + ' H ' + (x+w) + ' V ' + (yBottom-r) + ' A ' + r + ' ' + r + ' 0 0 1 ' + (x+w-r) + ' ' + yBottom + ' H ' + (x+r) + 
' A ' + r + ' ' + r + ' 0 0 1 ' + x + ' ' + (yBottom-r) + ' Z';
// === ▲▲▲ 変更 ▲▲▲ ===
            segment = createSVGElement('path', { d: d, class: 'bar-segment ' + cssClass });
// クラス結合を修正
          } else if (isTopSegment) {
            // === ▼▼▼ 修正点: 文字列リテラル内の改行を削除 ▼▼▼ ===
            // ▼▼▼【ここから変更】 yTop/yBottom/height はアニメーション後の値を使用 ▼▼▼
            var d = 'M ' + x + ' ' + yBottom + ' H ' + (x+w) + ' V ' + (yTop+r) + ' A ' + r + ' ' + r + ' 0 0 0 ' + (x+w-r) + ' ' + yTop + ' H ' + (x+r) + 
' A ' + r + ' ' + r + ' 0 0 0 ' + x + ' ' + (yTop+r) + ' Z';
// === ▲▲▲ 変更 ▲▲▲ ===
            segment = createSVGElement('path', { d: d, class: 'bar-segment ' + cssClass });
// クラス結合を修正
          } else {
            segment = createSVGElement('rect', { x: x, y: yTop, width: w, height: height, class: 'bar-segment ' + cssClass });
// クラス結合を修正
          }

          barGroup.appendChild(segment);

          // ▼▼▼【ここから変更】 アニメーション後の高さ(height)で判定し、透明度(animOpacity)を適用 ▼▼▼
          if(height > 15){ // Threshold for displaying label
            barGroup.appendChild(createSVGElement('text', { 
                x: centerX, 
                y: yTop + height / 2, // アニメーション後のY座標
                'dominant-baseline': 'middle', 
                class: 'segment-label',
                style: 'opacity: ' + animOpacity // 0.7-1.0でフェードイン
            }, finalValue)); // 表示する数字は最終的な値
}
          // ▲▲▲【ここまで変更】▲▲▲
          yOffset += value;
        });
        
        chartAreaGroup.appendChild(barGroup);
      });
}


    // グラフ描画を実行
    try {
      var configJson = document.getElementById('chart-json-data').textContent;
if (!configJson || configJson.trim() === '') {
        throw new Error("JSON data is empty.");
}
      var fullConfig;
      try {
        fullConfig = JSON.parse(configJson);
} catch (parseError) {
        // === ▼ バッククォート不使用に変更 ▼ ===
        throw new Error('JSON parsing error: ' + parseError.message);
// === ▲ 変更 ▲ ===
      }

      // ▼▼▼【変更点】ネストされた "data" キーから設定を読み込む ▼▼▼
      if (!fullConfig || typeof fullConfig !== 'object' || !fullConfig.data) {
        throw new Error("Invalid JSON structure: Missing 'data' key.");
}
      var config = fullConfig.data;
// config は data オブジェクトを指す
      // ▲▲▲ ここまで ▲▲▲
      
      // ▼▼▼【ここから変更】 "colors" が文字列だった場合の処理を分岐 (IE互換のため var を使用) ▼▼▼
      if (config.colors && typeof config.colors === 'string') {
        
        var numColors = config.legendLabels ?
config.legendLabels.length : 5;
        var generatedIds = [];
        for (var i = 0; i < numColors; i++) {
          generatedIds.push(String.fromCharCode(65 + i));
// 'A', 'B', 'C'...
        }
        
        var generatedColors;
// ここで宣言
        var colorKeyword = config.colors.toLowerCase();
        
        var gradientDarken = 15;
// #gemini に適用される濃淡差

        // ▼▼▼【分岐1】#gemini の場合 ▼▼▼
        if (colorKeyword === '#gemini') {
          // 濃いピンク -> 紫 -> 青
          var GEMINI_STOPS = ['#E68A9C', '#9F63D0', '#616AD8'];
var hexPalette = generateGradientScale(GEMINI_STOPS, numColors);
          generatedColors = [];
          
          for (var i = 0; i < numColors; i++) {
            var hex_start = hexPalette[i];
var rgb_start = hexToRgb(hex_start);
            var hsl = rgbToHsl(rgb_start.r, rgb_start.g, rgb_start.b);
            var h = hsl[0], s = hsl[1], l = hsl[2];
var l_end = Math.max(0, Math.min(100, l - gradientDarken));
            var rgb_end = hslToRgb(h, s, l_end);
            var hex_end = rgbToHex(rgb_end[0], rgb_end[1], rgb_end[2]);
generatedColors.push({
              id: generatedIds[i],
              start: hex_start,
              end: hex_end
            });
}
        
        // ▼▼▼【分岐2: 変更】#night の場合 ▼▼▼
        } else if (colorKeyword === '#night') {
          // ユーザー指定の固定カラーパレット (start と end のペア)
          var NIGHT_COLORS = [
            { start: '#8d58d3', end: '#704ca4' }, // 項目 A (紫)
            { start: '#8f83d6', end: '#7469a8' 
}, // 項目 B (青紫)
            { start: '#7891dd', end: '#5c6fbb' }, // 項目 C (青)
            { start: '#50a89d', end: '#258a7f' }, // 項目 D (青緑)
            { start: '#54c5d5', end: '#34a0b0' }  // 項目 E (水色)
          ];
generatedColors = [];
          
          // generatedIds ('A', 'B', 'C'...) と NIGHT_COLORS をマッピング
          for (var i = 0; i < numColors; i++) {
            // 該当する色を取得 (もし定義数を超えたら最後のを繰り返す)
            var colorPair = NIGHT_COLORS[i] ||
NIGHT_COLORS[NIGHT_COLORS.length - 1];
            
            generatedColors.push({
              id: generatedIds[i], // 'A', 'B', 'C'...
              start: colorPair.start,
              end: colorPair.end
            });
}

        } else {
          // ▼▼▼【分岐3】HEXコード（単色・交互濃淡）の場合 ▼▼▼
          var primaryColor = config.colors;
// (generateMonochromaticScale関数内で gradientDarken が7で固定されている)
          generatedColors = generateMonochromaticScale(primaryColor, generatedIds);
// HEXコードが無効だった場合
          if (generatedColors.length === 0) {
             throw new Error("Invalid HEX color string in 'colors': " + primaryColor);
}
        }
        
        // 最終的な色設定を config に上書き
        if (generatedColors && generatedColors.length > 0) {
          config.colors = generatedColors;
} else {
           throw new Error("Failed to generate colors from keyword: " + config.colors);
}
        
      } else if (!config.colors || !Array.isArray(config.colors)) {
         // "colors" が配列でも文字列でもない場合はエラー
         throw new Error("'colors' must be an array of color objects or a single primary color string.");
}


      if (!config.layout) {
        // 'config.layout' が JSON にない場合、デフォルト値を設定
        config.layout = {
          width: 600,
          height: 550,
          marginTop: 170,
          marginBottom: 50,
          marginLeft: 75,
          marginRight: 50
        };
      }
      
      // 2. barOptions のデフォルト値を設定
      if (!config.barOptions) {
        // 'config.barOptions' が JSON にない場合、デフォルト値を設定
        config.barOptions = {
          width: 50,
          cornerRadius: 4,
          totalLabelOffset: 10
        };
      }


      
      if (!config || typeof config !== 'object' || !config.barData || !Array.isArray(config.barData)) {
        throw new Error("Invalid JSON structure: Missing required fields like 'barData' inside 'data'.");
}
      buildChart(config);
    } catch(e) {
      console.error("Chart Error:", e);
var svgRoot = document.getElementById('stacked-bar-chart-svg');
      var existingError = svgRoot.querySelector('.error-message');
      if(existingError) existingError.remove();
var textEl = createSVGElement('text', {
        x: svgRoot.viewBox.baseVal.width / 2 + svgRoot.viewBox.baseVal.x,
        y: svgRoot.viewBox.baseVal.height / 2 + svgRoot.viewBox.baseVal.y,
        'text-anchor': 'middle', fill: 'red',
        'font-family': "'Noto Sans JP', sans-serif",
        class: 'error-message'
      });
textEl.textContent = e.message.includes('JSON') ? 'JSONデータにエラーがあります。' : 'グラフ描画エラーが発生しました。';
      if (e.message.includes('JSON parsing error')) {
        // === ▼ バッククォート不使用に変更 ▼ ===
        textEl.textContent += ' (' + e.message.split(': ')[1] + ')';
// === ▲ 変更 ▲ ===
      }
      svgRoot.appendChild(textEl);
}

    // ▼▼▼ 修正点: // ]]> を削除 ▼▼▼
  </script>
</svg>
`;




const BAR_CHART_TEMPLATE = `
<svg width="600" height="450" viewBox="25 25 570 435" xmlns="http://www.w3.org/2000/svg" font-family="'Noto Sans JP', sans-serif" id="bar-chart-svg">

    <script id="chart-json-data" type="application/json">
{
        "chartType": "bar",
        "data": {
            "title": "サンプル棒グラフ（動的レイアウト）",
            "subtitle": "項目数に応じて幅が均等に調整されます",
            "source": "出典：サンプルデータソース",
            "items": [
        
                { "label": "項目A", "value": 85 },
                { "label": "項目B", "value": 72 },
                { "label": "項目C", "value": 93 },
                { "label": "項目D", "value": 65 },
                { "label": "項目E", "value": 48 },
   
                { "label": "項目F", "value": 78 },
                { "label": "項目G", "value": 55 },
                { "label": "項目H", "value": 30 }
             ],
            "colors": {
              
                "start": "#e68a9c",
                "end": "#9f63d0"
            },
            "layout": {
                "width": 600,
                "height": 450,
                "marginTop": 100,
    
                "marginBottom": 65,
                "marginLeft": 70,
                "marginRight": 40
            },
            "barOptions": {
                "barToSlotRatio": 0.6
          
            },
            "yAxis": {
                "max": 100,
                "min": 0,
                "tickCount": 4,
                "unit": "%"
            },
            
            "animation": 1
     
        }
    }
    </script>
    <defs>
        <filter id="shadow">
            <feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2"/>
        </filter>
         <filter id="text-shadow">
            <feDropShadow dx="0" dy="0" stdDeviation="2.5" flood-color="#000000" flood-opacity="0.9"/>
        </filter>
    </defs>
    <style>
        
        @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500&amp;family=Roboto:wght@400&amp;display=swap');
        /* 基本スタイル (デフォルト Noto Sans JP) */
        .bar { filter: url(#shadow); }
        .axis-label { font-size: 13px; fill: #5f6368; text-anchor: middle; }
        .title { font-size: 21px; font-weight: 500; text-anchor: middle; fill: #5f6368; }
        .subtitle { font-size: 16px; text-anchor: middle; fill: #5f6368; }
        .source { font-size: 11px; fill: #70757a; text-anchor: end; }
        .grid-line { stroke: #e0e0e0; stroke-dasharray: 2 2; stroke-width: 0.8; }
        .y-axis-unit { font-size: 12px; fill: #5f6368; text-anchor: end; } /* Noto Sans JP */

        /* 数字用のフォント指定 */
        .data-label { font-family: 'Roboto', sans-serif; font-size: 14px; font-weight: normal; text-anchor: middle; }
        .y-axis-value { font-family: 'Roboto', sans-serif; font-size: 12px; fill: #757575; text-anchor: end; }

        /* 動的スタイル */
        /* .bar fill は JS で設定 */
    </style>

    <g id="title-group"></g>
    <g id="y-axis-group"></g>
    <g id="chart-area-group"></g>
    <g id="x-axis-group"></g>
    <g id="source-group"></g>

    <script type="text/javascript">
    // <![CDATA[

    // =================================================================
    // ロジックコード
    // =================================================================
    const svgNS = "http://www.w3.org/2000/svg";
    function createSVGElement(name, attributes, textContent) {
        const el = document.createElementNS(svgNS, name);
        for (const key in attributes) el.setAttribute(key, attributes[key]);
        if (textContent !== undefined && textContent !== null) {
            el.textContent = textContent;
        }
        return el;
    }

    // =================================================================
    // ▼▼▼【ここから追加】色計算用のヘルパー関数 ▼▼▼
    // =================================================================
    
    /** HEX (例: #FF0000) を RGB (例: {r: 255, g: 0, b: 0}) に変換 */
    function hexToRgb(hex) {
      let shorthandRegex = /^#?([a-f0-9])([a-f0-9])([a-f0-9])$/i;
      hex = hex.replace(shorthandRegex, function(m, r, g, b) {
        return r + r + g + g + b + b;
      });
      let result = /^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex);
      return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
      } : null;
    }

    /** RGB (0-255) を HSL (H: 0-360, S: 0-100, L: 0-100) に変換 */
    function rgbToHsl(r, g, b) {
      r /= 255; g /= 255; b /= 255;
      let max = Math.max(r, g, b), min = Math.min(r, g, b);
      let h, s, l = (max + min) / 2;
      l = Math.round(l * 100); // %に変換

      if (max == min) {
        h = s = 0; // achromatic
      } else {
        let d = max - min;
        s = l > 50 ? d / (2 - max - min) : d / (max + min);
        s = Math.round(s * 100); // %に変換
        switch (max) {
          case r: h = (g - b) / d + (g < b ? 6 : 0); break;
          case g: h = (b - r) / d + 2; break;
          case b: h = (r - g) / d + 4; break;
        }
        h /= 6;
      }
      h = Math.round(h * 360); // 角度に変換
      return [h, s, l];
    }

    /** HSL (H: 0-360, S: 0-100, L: 0-100) を RGB (0-255) に変換 */
    function hslToRgb(h, s, l) {
      let r, g, b;
      h /= 360; // 0-1 に
      s /= 100; // 0-1 に
      l /= 100; // 0-1 に

      if (s == 0) {
        r = g = b = l; // achromatic
      } else {
        function hue2rgb(p, q, t) {
          if (t < 0) t += 1;
          if (t > 1) t -= 1;
          if (t < 1/6) return p + (q - p) * 6 * t;
          if (t < 1/2) return q;
          if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
          return p;
        }
        let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        let p = 2 * l - q;
        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3);
      }
      return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
    }

    /** RGB (0-255) を HEX (例: #FF0000) に変換 */
    function rgbToHex(r, g, b) {
      function componentToHex(c) {
        let hex = c.toString(16);
        return hex.length == 1 ? "0" + hex : hex;
      }
      return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
    }

    /** HEXカラーの輝度(L)を指定範囲内に丸める */
    function clampLuminance(hex, minL, maxL) {
      var rgb = hexToRgb(hex);
      if (!rgb) return hex; // 無効なHEXはそのまま返す
      
      var hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
      var h = hsl[0], s = hsl[1], l = hsl[2];
      
      // 輝度(l)を minL と maxL の間に丸める
      var clamped_l = Math.max(minL, Math.min(maxL, l));
      
      if (clamped_l === l) {
        return hex; // 輝度が範囲内なら変更不要
      }
      
      var clamped_rgb = hslToRgb(h, s, clamped_l);
      return rgbToHex(clamped_rgb[0], clamped_rgb[1], clamped_rgb[2]);
    }


    // =================================================================
    // ▲▲▲【ここまで追加】▲▲▲
    // =================================================================
    
    /**
 * 指定された maxWidth に収まるようにテキストを省略します。
 * フォントサイズと平均文字幅の近似値に基づいて文字数を計算する簡易ロジックです。
 * * @param {string} text 元のテキスト
 * @param {number} maxWidth 最大幅 (px)
 * @returns {string} 省略されたテキストまたは元のテキスト
 */
function truncateText(text, maxWidth) {
    // X軸ラベルのフォントサイズは 13px ですが、日本語(全角)を考慮し、
    // 1文字あたりの平均ピクセル幅を 14px (約1.07倍) と仮定します。
    // この値は、簡易計算のための近似値であり、正確な幅は保証されません。
    const AVERAGE_CHAR_WIDTH = 10; 
    
    // '...' (三点リーダー) の幅を3文字分として概算
    const ELLIPSIS_WIDTH = 3 * AVERAGE_CHAR_WIDTH; 

    // 1. 現在のテキストの概算幅を計算し、maxWidth 以下であれば、そのまま返す
    if (text.length * AVERAGE_CHAR_WIDTH <= maxWidth) {
        return text;
    }

    // 2. '...' の幅を引いた残りのスペースに収まる最大文字数を計算
    // これが切り詰めずに残す文字数になります。
    const maxVisibleChars = Math.floor((maxWidth - ELLIPSIS_WIDTH) / AVERAGE_CHAR_WIDTH);

    // 3. 保持すべき文字数が 0 以下の場合は、'...' のみ返す
    if (maxVisibleChars <= 0) {
        return '...';
    }

    // 4. 文字列を切り詰めて '...' を追加
    return text.slice(0, maxVisibleChars) + '...';
}


    


    function buildChart(config) {
        const svgRoot = document.getElementById('bar-chart-svg');
        const defs = svgRoot.querySelector('defs');

        // =================================================================
        // ▼▼▼【ここから追加】アニメーション比率の計算 ▼▼▼
        // =================================================================
        // config.animation が null や undefined の場合は 1 を設定
        let animRatio = (config.animation == null) ? 1.0 : parseFloat(config.animation);
        
        // 0.0 未満は 0.0、 1.0 より大きい場合は 1.0 に丸める
        config.animationRatio = Math.max(0.0, Math.min(1.0, isNaN(animRatio) ? 1.0 : animRatio));
        // =================================================================
        // ▲▲▲【ここまで追加】▲▲▲
        // =================================================================

        // Clear existing dynamic elements
        const existingGrad = document.getElementById('barGradient');
        if (existingGrad) existingGrad.remove();
        const styleElement = svgRoot.querySelector('style');
        if (styleElement) {
            styleElement.textContent = styleElement.textContent.replace(/[.]bar {[^}]*}/g, '');
        }


        const gradId = 'barGradient';
        let gradient = createSVGElement('linearGradient', { id: gradId, x1:"0", x2:"0", y1:"0", y2:"1" });
        gradient.appendChild(createSVGElement('stop', { offset: '0%', 'stop-color': config.color.start }));
        gradient.appendChild(createSVGElement('stop', { offset: '100%', 'stop-color': config.color.end }));
        defs.appendChild(gradient);

        const barStyleRule = '.bar { fill: url(#' + gradId + '); filter: url(#shadow); }';
        if(styleElement && !styleElement.textContent.includes(barStyleRule)) {
             styleElement.textContent += barStyleRule;
        }

        const groups = {
            titleGroup: document.getElementById('title-group'),
            yAxisGroup: document.getElementById('y-axis-group'),
            chartAreaGroup: document.getElementById('chart-area-group'),
            xAxisGroup: document.getElementById('x-axis-group'),
            sourceGroup: document.getElementById('source-group')
        };
        for (let key in groups) {
            while (groups[key].firstChild) groups[key].removeChild(groups[key].firstChild);
        }

        const chartAreaHeight = config.layout.height - config.layout.marginTop - config.layout.marginBottom;
        const dataMax = Math.max(...config.items.map(item => item.value), config.yAxis.max); // Include axis max
        const dataMin = Math.min(...config.items.map(item => item.value), config.yAxis.min);
        // Include axis min

        let effectiveYAxisMax, effectiveYAxisMin;
        const tickCount = config.yAxis.tickCount > 0 ? config.yAxis.tickCount : 1;
        const preliminaryRange = Math.max(1, config.yAxis.max - config.yAxis.min);
        let tickInterval = preliminaryRange / tickCount;

        if (dataMin < config.yAxis.min) { // Handles negative values or data below specified min
            const limit = Math.max(Math.abs(dataMin), dataMax);
            if (tickInterval <= 0 || isNaN(tickInterval)) tickInterval = Math.max(1, limit / 5);
             effectiveYAxisMax = Math.ceil(limit / tickInterval) * tickInterval;
             effectiveYAxisMin = -effectiveYAxisMax;
             if (config.yAxis.min > effectiveYAxisMin) effectiveYAxisMin = config.yAxis.min;
             if (config.yAxis.max < effectiveYAxisMax) effectiveYAxisMax = config.yAxis.max;
        } else {
             effectiveYAxisMin = config.yAxis.min;
             if (tickInterval <= 0 || isNaN(tickInterval)) tickInterval = Math.max(1, (dataMax - effectiveYAxisMin) / 5) || 1;
             effectiveYAxisMax = Math.max(config.yAxis.max, Math.ceil(dataMax / tickInterval) * tickInterval);
        }

        if (effectiveYAxisMax <= effectiveYAxisMin) {
            effectiveYAxisMax = effectiveYAxisMin + tickInterval * tickCount;
            if (effectiveYAxisMax <= effectiveYAxisMin) effectiveYAxisMax = effectiveYAxisMin + 1; // Absolute fallback
        }
        const yRange = effectiveYAxisMax - effectiveYAxisMin;
        const yScale = (value) => (yRange === 0) ? config.layout.marginTop + chartAreaHeight / 2 : config.layout.marginTop + chartAreaHeight * (1 - (value - effectiveYAxisMin) / yRange);
        groups.titleGroup.appendChild(createSVGElement('text', { x: 300, y: 50, class: 'title' }, config.title));
        groups.titleGroup.appendChild(createSVGElement('text', { x: 300, y: 75, class: 'subtitle' }, config.subtitle));
        groups.yAxisGroup.appendChild(createSVGElement('text', { x: config.layout.marginLeft, y: config.layout.marginTop - 25, class: 'y-axis-unit' }, '(' + config.yAxis.unit + ')'));
        
        const finalTickStep = yRange / tickCount;
        for (let i = 0; i <= tickCount; i++) {
            const tickValue = effectiveYAxisMin + (finalTickStep * i);
            const y = yScale(tickValue);
            groups.yAxisGroup.appendChild(createSVGElement('text', { x: config.layout.marginLeft - 10, y: y, class: 'y-axis-value', 'dominant-baseline': 'middle' }, Math.round(tickValue)));
            if (i > 0 && i <= tickCount && Math.abs(tickValue) > 1e-6) {
                groups.yAxisGroup.appendChild(createSVGElement('line', { x1: config.layout.marginLeft, y1: y, x2: config.layout.width - config.layout.marginRight, y2: y, class: 'grid-line' }));
            }
        }

        const bottomY = config.layout.marginTop + chartAreaHeight;
        groups.xAxisGroup.appendChild(createSVGElement('line', { x1: config.layout.marginLeft, y1: bottomY, x2: config.layout.width - config.layout.marginRight, y2: bottomY, stroke: '#bdbdbd' }));
        if (effectiveYAxisMin < 0 && effectiveYAxisMax > 0) {
            const yZero = yScale(0);
            groups.yAxisGroup.appendChild(createSVGElement('line', { x1: config.layout.marginLeft, y1: yZero, x2: config.layout.width - config.layout.marginRight, y2: yZero, stroke: '#bdbdbd', 'stroke-width': 1 }));
        }

        const plotAreaWidth = config.layout.width - config.layout.marginLeft - config.layout.marginRight;
        const slotWidth = plotAreaWidth / config.items.length;
        const barWidth = slotWidth * config.barOptions.barToSlotRatio;
        const yZero = yScale(Math.max(0, effectiveYAxisMin));


        
        // 1. ラベルを回転させ始める「幅」のしきい値を定義 (調整可能)
        const rotationThreshold = 50; 
        
        // 2. 描画幅がしきい値を下回るか判定
        const numItems = config.items.length;
        const needsRotation = (slotWidth < rotationThreshold && numItems > 1);

        // 3. ラベルのY位置を決定
        // (回転する場合はY=25, 回転しない場合はY=30 にする)
        const labelYOffset = needsRotation ? bottomY + 25 : bottomY + 30;
        
        
        // アニメーション比率を取得
        const animationRatio = config.animationRatio;


        // ▼▼▼【ここから追加】▼▼▼
        // データラベルのフェードイン用 Opacity (0.7 -> 1.0)
        let animationOpacity;
        const fadeStart = 0.7; // 0.7 からフェードイン開始
        if (animationRatio <= fadeStart) {
            animationOpacity = 0;
        } else {
            // 0.7 (0%) から 1.0 (100%) の間で 0 -> 1 に変化
            animationOpacity = (animationRatio - fadeStart) / (1.0 - fadeStart);
        }
        // ▲▲▲【ここまで追加】▲▲▲




        config.items.forEach((item, index) => {
            
            // ▼▼▼【[K] ここで '...' かどうかを判定】▼▼▼
            const isEllipsisItem = (item.label === '...');

            // アニメーション適用後の値を計算
            const animatedValue = item.value * animationRatio;
            
            const centerX = config.layout.marginLeft + (index * slotWidth) + (slotWidth / 2);
            const barX = centerX - barWidth / 2;
            let barY, barHeight;

// ( ... 棒グラフの高さ計算 ... )
            if (item.value >= 0) {
                barY = yScale(animatedValue); 
                barHeight = Math.max(0, yZero - barY); 
            } else {
                barY = yZero;
                barHeight = Math.max(0, yScale(animatedValue) - yZero); 
            }
            
            // 棒の高さが1ピクセル以上ある場合のみ描画 (ただし '...' の場合は棒も描画しない)
            if (barHeight >= 1 && !isEllipsisItem) {
                 groups.chartAreaGroup.appendChild(createSVGElement('rect', { x: barX, y: barY, width: barWidth, height: barHeight, rx: 4, ry: 4, class: 'bar' }));
            }
// ( ... 棒グラフの高さ計算ここまで ... )
            
            // ラベルの位置判定はアニメーション後の棒の高さ(barHeight)で行う
            const isOutside = barHeight < 30; // Threshold to place label outside


            // ▼▼▼【[L] '...' ではない場合のみデータラベルを描画】▼▼▼
            if (!isEllipsisItem) {
                // ▼▼▼ style 属性に animationOpacity を追加 ▼▼▼
                const dataLabel = createSVGElement('text', { 
                    x: centerX, 
                    class: 'data-label',
                    style: 'opacity: ' + animationOpacity // 0.7-1.0でフェードイン
                }, item.value);
                // ▲▲▲ 変更ここまで ▲▲▲
                
                if (isOutside) {
                    dataLabel.setAttribute('fill', '#424242'); // Darker text for outside
                    dataLabel.setAttribute('filter', 'none'); // No shadow for outside
                    dataLabel.setAttribute('y', item.value >= 0 ? barY - 8 : barY + barHeight + 18);
                } else {
                    dataLabel.setAttribute('fill', 'white'); // White text for inside
                    dataLabel.setAttribute('filter', 'url(#text-shadow)'); // Add shadow for contrast
                    dataLabel.setAttribute('y', item.value >= 0 ? barY + 20 : barY + barHeight - 10);
                }
                groups.chartAreaGroup.appendChild(dataLabel);
            }
            // ▲▲▲【[L] 変更ここまで】▲▲▲

            
            // ▼▼▼【[M] X軸ラベル描画ロジック ('...' の回転をスキップ)】▼▼▼
            let labelContent;
            
            if (needsRotation && !isEllipsisItem) {
                labelContent = item.label; // 回転する (省略しない)
            } else {
                // 回転しない、または '...' の場合
                if (isEllipsisItem) {
                    labelContent = item.label; // '...' は省略処理(truncateText)しない
                } else {
                    labelContent = truncateText(item.label, slotWidth * 0.9); // 回転しないものは省略
                }
            }

            // 1. 共通の属性オブジェクトを作成
            var attributes = {
                x: centerX, // 棒グラフのX中央座標
                y: labelYOffset, // [A]で計算したY位置
                class: 'axis-label'
            };

            // 2. 回転が必要 かつ '...' でない場合のみ、属性を上書き
            if (needsRotation && !isEllipsisItem) {
                attributes['text-anchor'] = 'end';
                attributes['dominant-baseline'] = 'middle';
                attributes.transform = 'rotate(-45, ' + centerX + ', ' + labelYOffset + ')';
            } else {
                // '...' または回転しない場合は、CSSの 'text-anchor: middle' が使われる
            }

            // 3. 最終的な属性で要素を作成
            groups.xAxisGroup.appendChild(createSVGElement('text', attributes, labelContent));
            // ▲▲▲【[M] 変更ここまで】▲▲▲

        });




        groups.sourceGroup.appendChild(createSVGElement('text', { x: 595, y: config.layout.height - 10, class: 'source' }, config.source));
    }


    // グラフ描画を実行
    try {
        const configJson = document.getElementById('chart-json-data').textContent;
        if (!configJson || configJson.trim() === '') {
            throw new Error("JSON data is empty.");
        }
        let fullConfig;
        try {
            fullConfig = JSON.parse(configJson);
        } catch (parseError) {
             throw new Error('JSON parsing error: ' + parseError.message);
        }
        
        if (!fullConfig || typeof fullConfig !== 'object' || !fullConfig.data) {
             throw new Error("Invalid JSON structure: Missing 'data' key.");
        }
        const config = fullConfig.data;

        // layout の既定値処理（Gemプロンプトは layout を省略してよい仕様。無いと config.layout.width で落ちて
        //   「グラフ描画エラー」になる。指定があれば個々の値だけ上書きする）
        const defaultLayout = { width: 600, height: 450, marginTop: 100, marginBottom: 65, marginLeft: 70, marginRight: 40 };
        config.layout = Object.assign({}, defaultLayout, (config.layout && typeof config.layout === 'object') ? config.layout : {});

        if (config.colors) {
            config.color = config.colors;
        }

        // ▼▼▼【ここから変更】 "color" が文字列だった場合の処理を分岐 ▼▼▼
        if (config.color && typeof config.color === 'string') {
            
            const colorKeyword = config.color.toLowerCase();
            let generatedColorObject;

            // ▼▼▼【分岐1】#gemini の場合 ▼▼▼
            if (colorKeyword === '#gemini') {
                generatedColorObject = {
                    start: "#E68A9C", // Pink
                    end:   "#9F63D0"  // Purple
                };
            
            // ▼▼▼【分岐2: ★変更★】#night の場合 ▼▼▼
            } else if (colorKeyword === '#night') {
                // 折れ線グラフの定義から色を拝借
                const NIGHT_COLORS_SUBSET = [
                     { start: '#8d58d3', end: '#704ca4' }, // 0: Purple
                     { start: '#8f83d6', end: '#7469a8' }, // 1: Blue-Purple
                     { start: '#7891dd', end: '#5c6fbb' }  // 2: Blue
                ];
                generatedColorObject = {
                    start: NIGHT_COLORS_SUBSET[0].start, // Light Purple ('#8d58d3')
                    end:   NIGHT_COLORS_SUBSET[2].end    // ★変更★ Dark Blue ('#5c6fbb')
                };

            // ▼▼▼【分岐3】HEXコード（単色・明るさ違い）の場合 ▼▼▼
            } else {
                const primaryColor = config.color;
                const rgb = hexToRgb(primaryColor);
                if (!rgb) {
                    throw new Error("Invalid HEX color string in 'color': " + primaryColor);
                }
                
                const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
                const h = hsl[0], s = hsl[1], l = hsl[2];
                
                // 明るい色 (start)
                const l_start = Math.min(100, l + 20);
                const s_start = (l > 80) ? s * 0.7 : s;
                const rgb_start = hslToRgb(h, s_start, l_start);
                const hex_start = rgbToHex(rgb_start[0], rgb_start[1], rgb_start[2]);

                generatedColorObject = {
                    start: hex_start,
                    end:   primaryColor
                };
            }
            
            // 最終的な色設定を config に上書き
            config.color = generatedColorObject;
            
        } else if (!config.color || typeof config.color !== 'object') {
             // "color" がオブジェクトでも文字列でもない場合はエラー
             throw new Error("'color' must be a color object or a single color string.");
        }
        // ▲▲▲【ここまで変更】▲▲▲


        // ▼▼▼【[K] ここから追加】棒グラフの色の輝度(L)を制限する ▼▼▼
        // (白すぎたり黒すぎたりするのを防ぐ)
        
        const L_MIN_CLAMP = 30; // 輝度の下限 (0=黒, 30=濃い色)
        const L_MAX_CLAMP = 70; // 輝度の上限 (100=白, 70=明るい色)
        
        if (config.color && typeof config.color === 'object') {
            if (config.color.start) {
                config.color.start = clampLuminance(config.color.start, L_MIN_CLAMP, L_MAX_CLAMP);
            }
            if (config.color.end) {
                config.color.end = clampLuminance(config.color.end, L_MIN_CLAMP, L_MAX_CLAMP);
            }
        }
        // ▲▲▲【[K] ここまで追加】▲▲▲


        if (!config.barOptions) {
          config.barOptions = {}; // オブジェクト自体がない場合
        }
        // null または undefined の場合にデフォルト値を設定
        if (config.barOptions.barToSlotRatio == null) {
          config.barOptions.barToSlotRatio = 0.7; // 棒グラフの幅の比率 (70%)
        }


         if (!config || typeof config !== 'object' || !config.items || !Array.isArray(config.items)) {
             throw new Error("Invalid JSON structure: Missing required fields like 'items' inside 'data'.");
         }
        buildChart(config);
    } catch(e) {
        console.error("Chart Error:", e);
        const svgRoot = document.getElementById('bar-chart-svg');
        const existingError = svgRoot.querySelector('.error-message');
        if(existingError) existingError.remove();
        const textEl = createSVGElement('text', {
            x: svgRoot.viewBox.baseVal.width / 2 + svgRoot.viewBox.baseVal.x,
            y: svgRoot.viewBox.baseVal.height / 2 + svgRoot.viewBox.baseVal.y,
            'text-anchor': 'middle', fill: 'red',
            'font-family': "'Noto Sans JP', sans-serif",
            class: 'error-message'
            });
        textEl.textContent = e.message.includes('JSON') ? 'JSONデータにエラーがあります。' : 'グラフ描画エラーが発生しました。';
        if (e.message.includes('JSON parsing error')) {
           textEl.textContent += ' (' + e.message.split(': ')[1] + ')';
        }
        svgRoot.appendChild(textEl);
    }
    
    // ]]>
    </script>
</svg>
`;



const PERCENT_STACKED_BAR_CHART_TEMPLATE = `
<svg width="600" height="510" viewBox="15 15 580 505" xmlns="http://www.w3.org/2000/svg" font-family="'Noto Sans JP', sans-serif" id="100-stacked-bar-chart-svg">

  <script id="chart-json-data" type="application/json">
    {
      "chartType": "100-stacked-bar",
      "data": {
        "title": "サンプルグラフ：カテゴリ別割合",
        "subtitle": "（月別サンプルデータ）",
        "source": "出典：サンプルデータソース",
        "colors": [
          { "id": "A", "start": "#e68a9c", "end": "#d96d8f" },
          { "id": "B", 
"start": "#b469b8", "end": "#a656ad" },
          { "id": "C", "start": "#9f63d0", "end": "#8c4fc8" },
          { "id": "D", "start": "#7c6ce8", "end": "#6b5ce0" },
          { "id": "E", "start": "#616ad8", "end": "#5059d1" }
        ],
        "legendLabels": ["項目 A", "項目 B", "項目 C", "項目 D", "項目 E"],
        "barData": [
          
{ "label": "サンプル 1", "values": [40, 60, 30, 50, 20] },
          { "label": "サンプル 2", "values": [70, 50, 40, 20, 20] },
          { "label": "サンプル 3", "values": [30, 40, 60, 40, 30] },
          { "label": "サンプル 4", "values": [25, 35, 55, 65, 45] },
          { "label": "サンプル 5", "values": [50, 40, 30, 20, 60] }
        ],
  
      "layout": {
          "width": 600,
          "height": 510,
          "marginTop": 150,
          "marginBottom": 80,
          "marginLeft": 70,
          "marginRight": 25
        },
        "barOptions": {
          
"width": 50,
          "cornerRadius": 4
        },
        "yAxis": {
          "tickCount": 4
        },
        "animation": 1
      }
    }

  </script>
  <defs>
    <filter id="shadow"><feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.4"/></filter>
    <filter id="text-shadow"><feDropShadow dx="1" dy="1" stdDeviation="1" flood-color="#000" flood-opacity="0.7"/></filter>
  </defs>

  <style>
    @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500&amp;family=Roboto:wght@400&amp;display=swap');
    /* 基本スタイル (デフォルト Noto Sans JP) */
    .bar-segment { filter: url(#shadow); }
    .axis-label { font-size: 13px; fill: #5f6368; text-anchor: middle; }
    /* ▼▼▼【変更点】 title の fill を #5f6368 に変更 ▼▼▼ */
    .title { font-size: 24px; font-weight: 500; text-anchor: middle; fill: #5f6368; }
    .subtitle { font-size: 16px; text-anchor: middle; fill: #5f6368; } /* 変更なし */
    .source { font-size: 11px; fill: #70757a; text-anchor: end; }
    .grid-line { stroke: #e0e0e0; stroke-dasharray: 2 2; stroke-width: 0.8; }
    .legend-item { font-size: 13px; fill: #202124; dominant-baseline: middle; }

    /* 数字用のフォント指定 */
    .segment-label { font-family: 'Roboto', sans-serif; font-size: 14px; font-weight: normal; fill: white; filter: url(#text-shadow); text-anchor: middle; pointer-events: none; }
    .y-axis-value { font-family: 'Roboto', sans-serif; font-size: 12px; fill: #757575; text-anchor: end; }
  </style>

  <g id="title-group"></g>
  <g id="legend-group"></g>
  <g id="y-axis-group"></g>
  <g id="chart-area-group"></g>
  <g id="source-group"></g>

  <script type="text/javascript">
    // <![CDATA[

    // =================================================================
    // この下にあるロジックコードは変更されています。
    // =================================================================
    const svgNS = "http://www.w3.org/2000/svg";
    function createSVGElement(name, attributes, textContent) {
      const el = document.createElementNS(svgNS, name);
      for (const key in attributes) el.setAttribute(key, attributes[key]);
      if (textContent !== undefined && textContent !== null) {
        el.textContent = textContent;
      }
      return el;
    }

    // =================================================================
    // ▼▼▼【ここから追加】色計算用のヘルパー関数 ▼▼▼
    // =================================================================
    

    /**
     * フォントサイズ(px)に基づいて簡易的なテキスト幅を計算する
     */
    function getEstimatedTextWidth(text, fontSize) {
        let width = 0;
        // .legend-item のフォントサイズ(13px)を基準にする
        const fullWidthCharSize = fontSize; // 全角文字の幅
        const halfWidthCharSize = fontSize * 0.6; // 半角英数の幅
        
        if (!text) return 0;

        for (let i = 0; i < text.length; i++) {
            const charCode = text.charCodeAt(i);
            // 簡易的な半角判定 (ASCII範囲 + 半角カタカナ)
            if ((charCode >= 0x0020 && charCode <= 0x007E) || (charCode >= 0xFF61 && charCode <= 0xFF9F)) {
                width += halfWidthCharSize;
            } else {
                // それ以外は全角として扱う
                width += fullWidthCharSize;
            }
        }
        return width;
    }

    /** HEX (例: #FF0000) を RGB (例: {r: 255, g: 0, b: 0}) に変換 */
    function hexToRgb(hex) {
      let shorthandRegex = /^#?([a-f0-9])([a-f0-9])([a-f0-9])$/i;
      hex = hex.replace(shorthandRegex, function(m, r, g, b) {
        return r + r + g + g + b + b;
      });
      let result = /^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex);
      return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
      } : null;
    }

    /** RGB (0-255) を HSL (H: 0-360, S: 0-100, L: 0-100) に変換 */
    function rgbToHsl(r, g, b) {
      r /= 255; g /= 255; b /= 255;
      let max = Math.max(r, g, b), min = Math.min(r, g, b);
      let h, s, l = (max + min) / 2;
      l = Math.round(l * 100); // %に変換

      if (max == min) {
        h = s = 0; // achromatic
      } else {
        let d = max - min;
        s = l > 50 ? d / (2 - max - min) : d / (max + min);
        s = Math.round(s * 100); // %に変換
        switch (max) {
          case r: h = (g - b) / d + (g < b ? 6 : 0); break;
          case g: h = (b - r) / d + 2; break;
          case b: h = (r - g) / d + 4; break;
        }
        h /= 6;
      }
      h = Math.round(h * 360); // 角度に変換
      return [h, s, l];
    }

    /** HSL (H: 0-360, S: 0-100, L: 0-100) を RGB (0-255) に変換 */
    function hslToRgb(h, s, l) {
      let r, g, b;
      h /= 360; // 0-1 に
      s /= 100; // 0-1 に
      l /= 100; // 0-1 に

      if (s == 0) {
        r = g = b = l; // achromatic
      } else {
        function hue2rgb(p, q, t) {
          if (t < 0) t += 1;
          if (t > 1) t -= 1;
          if (t < 1/6) return p + (q - p) * 6 * t;
          if (t < 1/2) return q;
          if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
          return p;
        }
        let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        let p = 2 * l - q;
        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3);
      }
      return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
    }

    /** RGB (0-255) を HEX (例: #FF0000) に変換 */
    function rgbToHex(r, g, b) {
      function componentToHex(c) {
        let hex = c.toString(16);
        return hex.length == 1 ? "0" + hex : hex;
      }
      return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
    }
    
    /** 2つのHEXカラー間を線形補間する */
    function linearInterpolateColor(hex1, hex2, t) {
      const rgb1 = hexToRgb(hex1);
      const rgb2 = hexToRgb(hex2);
      
      const r = Math.round(rgb1.r + (rgb2.r - rgb1.r) * t);
      const g = Math.round(rgb1.g + (rgb2.g - rgb1.g) * t);
      const b = Math.round(rgb1.b + (rgb2.b - rgb1.b) * t);
      
      return rgbToHex(r, g, b);
    }

    /** 複数のカラーストップからグラデーションパレット（HEX配列）を生成する */
    function generateGradientScale(gradientStops, numSteps) {
      const palette = [];
      if (numSteps <= 0) return [];
      if (numSteps === 1) return [gradientStops[0]]; // 1色だけなら最初の色
      
      const numSegments = gradientStops.length - 1;

      for (let i = 0; i < numSteps; i++) {
        // 全体の進捗 (0.0 ... 1.0)
        const t_global = (numSteps === 1) ? 0 : (i / (numSteps - 1));
        
        // どのセグメントにいるか
        const segmentProgress = t_global * numSegments;
        const stopIndex1 = Math.floor(segmentProgress);
        const stopIndex2 = Math.min(stopIndex1 + 1, numSegments); 
        
        // そのセグメント内でのローカルな進捗 (0.0 ... 1.0)
        const t_local = segmentProgress - stopIndex1;
        
        const hex1 = gradientStops[stopIndex1];
        const hex2 = gradientStops[stopIndex2];
        
        palette.push(linearInterpolateColor(hex1, hex2, t_local));
      }
      return palette;
    }


    /**
     * 【単色用】プライマリーカラーとID配列から、自動計算された "colors" 配列を生成する
     * (交互の濃淡パターン)
     */
    function generateMonochromaticScale(primaryHex, colorIds) {
      const numSteps = colorIds.length;
      if (numSteps === 0) return [];

      const rgb = hexToRgb(primaryHex);
      if (!rgb) return []; // 無効なHEXの場合は空
      
      const [h, s, l_mid_original] = rgbToHsl(rgb.r, rgb.g, rgb.b);
      const gradientDarken = 7; 
      const L_MAX = 80; 
      const L_MIN = 45; 
      const l_step_amount = numSteps > 1 ? (L_MAX - L_MIN) / (numSteps - 1) : 0;
      
      const l_steps_calc = [];
      for (let i = 0; i < numSteps; i++) {
        const l_val = L_MAX - (i * l_step_amount);
        l_steps_calc.push(l_val);
      }
      

// ▼▼▼【ここから修正】元の から を置き換えます ▼▼▼
      
      const l_steps_light = [];
      const l_steps_dark = [];
      // l_steps_calc (例: [80, 66.25, 52.5, 38.75, 25]) を
      // 明るいグループと暗いグループに振り分ける
      const mid_index = Math.ceil(numSteps / 2);

      // 明るいグループ (前から)
      for (let i = 0; i < mid_index; i++) {
        l_steps_light.push(l_steps_calc[i]);
      }
      // 例 (5色の場合): [80, 66.25, 52.5]

      // 暗いグループ (中間から)
      // ※暗いグループも「明るい順」で保持
      for (let i = mid_index; i < numSteps; i++) {
        l_steps_dark.push(l_steps_calc[i]);
      }
      // 例 (5色の場合): [38.75, 25]

      const l_steps_start = [];
      for (let i = 0; i < numSteps; i++) {
        if (i % 2 === 0) {
          // 偶数番目 (A, C, E...) は 明るいグループから
          l_steps_start.push(l_steps_light[i / 2]);
        } else {
          // 奇数番目 (B, D...) は 暗いグループから
          l_steps_start.push(l_steps_dark[(i - 1) / 2]);
        }
      }
      // 例 (5色の場合): [80, 38.75, 66.25, 25, 52.5]

      // ▲▲▲【ここまで修正】▲▲▲


      const colors = [];
      for (let i = 0; i < numSteps; i++) {
        const l_start_step = l_steps_start[i];
        let l_start = Math.max(0, Math.min(100, l_start_step));
        let l_end = Math.max(0, Math.min(100, l_start - gradientDarken));
        
        let s_adjusted = s;
        if (l_start > 85) s_adjusted = s * (1 - (l_start - 85) / 15);
        if (l_start < 20) s_adjusted = s * (l_start / 20);
        s_adjusted = Math.max(0, Math.min(100, s_adjusted));

        const rgb_start = hslToRgb(h, s_adjusted, l_start);
        const hex_start = rgbToHex(rgb_start[0], rgb_start[1], rgb_start[2]);
        const rgb_end = hslToRgb(h, s_adjusted, l_end);
        const hex_end = rgbToHex(rgb_end[0], rgb_end[1], rgb_end[2]);
        
        colors.push({
          id: colorIds[i],
          start: hex_start,
          end: hex_end
        });
      }
      return colors;
    }
    // =================================================================
    // ▲▲▲【ここまで追加】▲▲▲
    // =================================================================


    function buildChart(config) {
      // === ▼ 安定性の高い記述に変更 ▼ ===
      const nl = String.fromCharCode(10); // 改行コード(LF) を変数化
      // === ▲ 変更 ▲ ===

      // ▼▼▼【ここから追加】▼▼▼
  // 1. グラフの「位置」用 (0.0 -> 1.0)
  const animProgress = (config.animation == null) 
      ? 1.0 
      : Math.max(0, Math.min(1, Number(config.animation)));

  // 2. データラベルの「透明度」用 (0.7 -> 1.0 でフェードイン)
  let animOpacity;
  const fadeStart = 0.7; // 0.7 からフェードイン開始
  if (animProgress <= fadeStart) {
      animOpacity = 0;
  } else {
      // 0.7 (0%) から 1.0 (100%) の間で 0 -> 1 に変化
      animOpacity = (animProgress - fadeStart) / (1.0 - fadeStart);
  }
  // ▲▲▲【ここまで追加】▲▲▲


         const defaultLayout = {
          width: 600,
          height: 510,
          marginTop: 150,
          marginBottom: 80,
          marginLeft: 70,
          marginRight: 25
      };

      if (config.layout && typeof config.layout === 'object') {
          // layout が存在する場合、個々のキーをチェックしてデフォルト値で埋める
          config.layout = {
              width: (config.layout.width !== undefined) ? config.layout.width : defaultLayout.width,
              height: (config.layout.height !== undefined) ? config.layout.height : defaultLayout.height,
              marginTop: (config.layout.marginTop !== undefined) ? config.layout.marginTop : defaultLayout.marginTop,
              marginBottom: (config.layout.marginBottom !== undefined) ? config.layout.marginBottom : defaultLayout.marginBottom,
              marginLeft: (config.layout.marginLeft !== undefined) ? config.layout.marginLeft : defaultLayout.marginLeft,
              marginRight: (config.layout.marginRight !== undefined) ? config.layout.marginRight : defaultLayout.marginRight
          };
      } else {
          // layout が存在しないか、オブジェクトでない場合は、丸ごとデフォルトを設定
          config.layout = defaultLayout;
      }


         const defaultBarOptions = {
               width: 50,
               cornerRadius: 4
         };

         if (config.barOptions && typeof config.barOptions === 'object') {
               // barOptions が存在する場合、個々のキーをチェックしてデフォルト値で埋める
               config.barOptions = {
                     width: (config.barOptions.width !== undefined && typeof config.barOptions.width === 'number') ? config.barOptions.width : defaultBarOptions.width,
                     cornerRadius: (config.barOptions.cornerRadius !== undefined && typeof config.barOptions.cornerRadius === 'number') ? config.barOptions.cornerRadius : defaultBarOptions.cornerRadius
               };
         } else {
               // barOptions が存在しないか、オブジェクトでない場合は、丸ごとデフォルトを設定
               config.barOptions = defaultBarOptions;
         }
         // ▲▲▲【ここまで追加】▲▲▲


      const chartConfig = config;
      const colorConfig = config.colors;

      const svgRoot = document.getElementById('100-stacked-bar-chart-svg');
      const defs = svgRoot.querySelector('defs');
      const style = svgRoot.querySelector('style');
      // Clear existing dynamic gradients before adding new ones
      const existingGrads = defs.querySelectorAll('linearGradient');
      existingGrads.forEach(grad => grad.remove());
      // Clear existing clipPaths
      const existingClips = defs.querySelectorAll('clipPath');
      existingClips.forEach(clip => clip.remove());
      // === ▼ 安定性の高い記述に変更 (配列 + join) ▼ ===
      let dynamicStyleParts = []; // 配列でスタイルルールを保持
      // === ▲ 変更 ▲ ===
      
      colorConfig.forEach(color => {
        // === ▼ バッククォート不使用に変更 ▼ ===
        const gradId = 'grad' + color.id;
        const cssClass = 'bar-' + color.id.toLowerCase();
        // === ▲ 変更 ▲ ===

        const gradient = createSVGElement('linearGradient', { id: gradId, x1:"0", x2:"0", y1:"0", y2:"1" });
   
        gradient.appendChild(createSVGElement('stop', { offset: '0%', 'stop-color': color.start }));
        gradient.appendChild(createSVGElement('stop', { offset: '100%', 'stop-color': color.end }));
        
        defs.appendChild(gradient);
        
        // === ▼ 安定性の高い記述に変更 (配列 + join) ▼ ===
        dynamicStyleParts.push('.' + cssClass + ' { fill: url(#' + gradId + '); }');
        // === ▲ 変更 ▲ ===
  
      });
      
      // === ▼ 安定性の高い記述に変更 (配列 + join) ▼ ===
      const dynamicStyles = dynamicStyleParts.join(nl); // nl (改行) で結合
      // === ▲ 変更 ▲ ===
      
      const styleElement = svgRoot.querySelector('style');
      // Append dynamic styles only if they are not already present
      
      // === ▼ バッククォート不使用に変更 ▼ ===
      const firstRuleSelector = '.bar-' + (config.colors[0] ? config.colors[0].id.toLowerCase() : ''); // Optional chaining も考慮
      // === ▲ 変更 ▲ ===
      
      if (styleElement && firstRuleSelector && !styleElement.textContent.includes(firstRuleSelector)) {
        // === ▼ 安定性の高い記述に変更 (nl を使用) ▼ ===
        styleElement.textContent += nl + dynamicStyles + nl; // 前後にも改行を追加
        // === ▲ 変更 ▲ ===
      }

      const titleGroup = document.getElementById('title-group');
      const legendGroup = document.getElementById('legend-group');
      const yAxisGroup = document.getElementById('y-axis-group');
      const chartAreaGroup = document.getElementById('chart-area-group');
      const sourceGroup = document.getElementById('source-group');
      // Clear previous chart elements
      for(let group of [titleGroup, legendGroup, yAxisGroup, chartAreaGroup, sourceGroup]) {
        while (group.firstChild) {
          group.removeChild(group.firstChild);
        }
      }

      const chartAreaWidth = chartConfig.layout.width - chartConfig.layout.marginLeft - chartConfig.layout.marginRight;
      const chartAreaHeight = chartConfig.layout.height - chartConfig.layout.marginTop - chartConfig.layout.marginBottom;
      const bottomY = chartConfig.layout.marginTop + chartAreaHeight;
      const xScale = (index) => chartConfig.layout.marginLeft + (chartAreaWidth / chartConfig.barData.length) * (index + 0.5);
      const yScale = (percentage) => bottomY - (percentage / 100) * chartAreaHeight;
      titleGroup.appendChild(createSVGElement('text', { x: 300, y: 50, class: 'title' }, chartConfig.title));
      titleGroup.appendChild(createSVGElement('text', { x: 300, y: 75, class: 'subtitle' }, chartConfig.subtitle));
      sourceGroup.appendChild(createSVGElement('text', { x: 595, y: 485, class: 'source' }, chartConfig.source));


//
      const legendY = 105;
      const numLegends = chartConfig.legendLabels.length;
      let legendStartX = chartConfig.layout.marginLeft; // let に変更
      const rectSize = 12;
      const rectTextGap = 6;
      const legendFontSize = 13; // .legend-item の font-size
      const legendPadding = 15; // 項目間の最小パディング
      
      // (defs変数は buildChart 関数の冒頭で定義されています)

      // 1. 凡例ラベルから最大の「テキスト幅」を見つける
      // (getEstimatedTextWidth は別で定義)
      let maxTextWidth = 0;
      chartConfig.legendLabels.forEach(function(label) {
          const textWidth = getEstimatedTextWidth(label, legendFontSize);
          if (textWidth > maxTextWidth) {
              maxTextWidth = textWidth;
          }
      });

      // 2. 最長のテキスト幅を基準にした「標準の項目幅」を計算
      // 幅 = 四角 + 隙間 + 最大テキスト幅 + 項目間パディング
      const standardItemWidth = rectSize + rectTextGap + maxTextWidth + legendPadding;

      // 3. 必要な合計幅を計算
      const totalRequiredWidth = standardItemWidth * numLegends;

      let useEqualWidths = false; // レイアウトモードフラグ
      let layoutItemWidth = 0; // 各項目に割り当てるレイアウト幅

      // 4. 描画エリア幅と比較
      if (totalRequiredWidth > chartAreaWidth || numLegends === 0) {
          // B) はみ出す場合 (または凡例ゼロ): 均等割りモード
          useEqualWidths = true;
          layoutItemWidth = (numLegends > 0) ? (chartAreaWidth / numLegends) : 0;
          // legendStartX はデフォルト (marginLeft) のまま
      } else {
          // A) 収まる場合: 標準化モード
          useEqualWidths = false;
          layoutItemWidth = standardItemWidth;
          // 全体を中央揃えにするための開始X座標を計算
          legendStartX = chartConfig.layout.marginLeft + (chartAreaWidth - totalRequiredWidth) / 2;
      }

      // 5. 凡例を描画
      chartConfig.legendLabels.forEach(function(label, i) {
        const cssClass = 'bar-' + colorConfig[i].id.toLowerCase();
        
        // この項目の開始X座標
        const columnStartX = legendStartX + (i * layoutItemWidth);
        // この項目が使える幅
        const currentColumnWidth = layoutItemWidth;

        const itemGroup = createSVGElement('g');
        
        // 四角を描画
        itemGroup.appendChild(createSVGElement('rect', {
          x: columnStartX, y: legendY, width: rectSize, height: rectSize, rx: 2, class: 'bar-segment ' + cssClass
        }));
        
        // テキストのX座標
        const textX = columnStartX + rectSize + rectTextGap;
        
        // --- ▼ クリッピングパスのロジック (はみ出し防止) ▼ ---
        
        const clipId = 'legend-clip-' + i;
        
        // テキストを表示する「枠」の幅を計算
        // (使える幅から四角と隙間、パディングの半分を引く)
        let clipMargin = (legendPadding / 2); // 標準モード時
        if (useEqualWidths) clipMargin = 5; // 均等割りの時は 5px 固定
        
        let clipWidth = (currentColumnWidth - rectSize - rectTextGap - clipMargin); 
        clipWidth = Math.max(0, clipWidth); // 0未満にならないように

        // <clipPath> 要素（切り抜き枠）を作成
        const clipPath = createSVGElement('clipPath', { id: clipId });
        clipPath.appendChild(createSVGElement('rect', {
            x: textX, // テキスト開始X座標
            y: legendY - (rectSize / 2), // テキスト上部
            width: clipWidth, // 計算した幅
            height: rectSize * 2 // テキストの高さ
        }));
        
        // <defs> タグに <clipPath> を追加
        defs.appendChild(clipPath);

        // テキスト要素に 'clip-path' 属性を追加
        const textElement = createSVGElement('text', { 
            x: textX, 
            y: legendY + rectSize / 2, 
            class: 'legend-item',
            'clip-path': 'url(#' + clipId + ')' 
        }, label);
        
        itemGroup.appendChild(textElement);
        // --- ▲ クリッピングパスここまで ▲ ---
        
         legendGroup.appendChild(itemGroup);
      });

//



      const tickCount = chartConfig.yAxis.tickCount > 0 ? chartConfig.yAxis.tickCount : 1;
      // Ensure tickCount > 0
      for (let i = 0; i <= tickCount; i++) {
        const tickPercentage = (100 / tickCount) * i;
        const y = yScale(tickPercentage);
        
        // ▼▼▼【バグ修正】短縮記法 { y } を { y: y } に明示的に修正 ▼▼▼
        yAxisGroup.appendChild(createSVGElement('text', { x: 60, y: y, class: 'y-axis-value', 'dominant-baseline': 'middle' }, Math.round(tickPercentage) + '%'));
        // ▲▲▲ 修正 ▲▲▲
        
        if (i > 0 && i < tickCount) { // Avoid grid on top/bottom axis
          yAxisGroup.appendChild(createSVGElement('line', { x1: chartConfig.layout.marginLeft, y1: y, x2: chartConfig.layout.width - chartConfig.layout.marginRight, y2: y, class: 'grid-line' }));
        }
      }
      // Draw top and bottom lines explicitly if needed for styling consistency
      yAxisGroup.appendChild(createSVGElement('line', { x1: chartConfig.layout.marginLeft, y1: bottomY, x2: chartConfig.layout.width - chartConfig.layout.marginRight, y2: bottomY, stroke: '#bdbdbd' })); // Bottom Line
      yAxisGroup.appendChild(createSVGElement('line', { x1: chartConfig.layout.marginLeft, y1: config.layout.marginTop, x2: chartConfig.layout.width - chartConfig.layout.marginRight, y2: config.layout.marginTop, stroke: '#bdbdbd' })); // Top Line (optional)

      
      // 1. スロット幅（1項目あたりの幅）を計算
      const slotWidth = chartAreaWidth / chartConfig.barData.length;
      
      // 2. 最小の隙間（ピクセル）を定義 (この値を調整してください)
      const minGap = 5;
      
      // 3. JSONから最大幅を取得
      const maxBarWidth = chartConfig.barOptions.width; // (現在のJSONでは 50px)
      
      // 4. 隙間を確保した場合の幅を計算
      const calculatedWidth = slotWidth - minGap;
      
      // 5. 最終的な棒の幅を決定 (最大幅を超えないようにし、0未満にならないようにする)
      const finalBarWidth = Math.max(1, Math.min(maxBarWidth, calculatedWidth));


      chartConfig.barData.forEach((bar, index) => {
        const barGroup = createSVGElement('g', { class: 'bar-group' });
        const centerX = xScale(index);
        const total = bar.values.reduce((sum, v) => sum + v, 0);
        barGroup.appendChild(createSVGElement('text', { x: centerX, y: bottomY + 20, class: 'axis-label' }, bar.label));
        
        let yOffsetPercentage = 0;

        // ▼▼▼【ここから追加】 2種類の割合リストを事前計算 ▼▼▼
    const numItems = bar.values.length;
    const equalPercentage = 100 / numItems; // 均等割の%

    // 1. 最終的な割合リスト (例: [20, 30, 50])
    const finalPercentages = bar.values.map(value => (total > 0 ? (value / total) * 100 : 0));

    // 2. 均等割の割合リスト (例: [33.3, 33.3, 33.3])
    const equalPercentages = bar.values.map(() => equalPercentage);
    // ▲▲▲【ここまで追加】▲▲▲



      
        let animatedYOffsetPercentage = 0; // アニメーション適用後のYオフセット



        bar.values.slice().reverse().forEach((originalValue, i) => {
            const segmentIndex = bar.values.length - 1 - i;

            // 1. 均等割の% と 最終的な% を取得
            const eqPerc = equalPercentages[segmentIndex];
            const finPerc = finalPercentages[segmentIndex];

            // 2. アニメーション適用後の% (高さ) を計算
            const animatedPercentage = eqPerc + (finPerc - eqPerc) * animProgress;

            if (animatedPercentage <= 0.01) return; // 0%のセグメントはスキップ

            // 3. アニメーション適用後の Y座標 と 高さを計算
            const yTop = yScale(animatedYOffsetPercentage + animatedPercentage);
            const yBottom = yScale(animatedYOffsetPercentage);
            const height = yBottom - yTop;

            if (height <= 0.5) return; // 0.5px以下は描画スキップ

            const cssClass = 'bar-' + colorConfig[segmentIndex].id.toLowerCase();
            const r = chartConfig.barOptions.cornerRadius;

            const w = finalBarWidth;
            const x = centerX - w / 2;

            let segment;

            // 4. どのセグメントがTop/Bottomかの判定 (アニメーション前の割合で判定)
            let currentFinalOffset = 0;
            for(let j = bar.values.length - 1; j > segmentIndex; j--) {
                currentFinalOffset += finalPercentages[j];
            }
            const isBottomSegment = (Math.abs(currentFinalOffset) < 0.01);
            const isTopSegment = (Math.abs((currentFinalOffset + finPerc) - 100) < 0.01);

            // 5. パスを描画 (yTop, height はアニメーション後の値を使用)
            if (isBottomSegment && isTopSegment) { // Single segment bar
                segment = createSVGElement('rect', { x: x, y: yTop, width: w, height: height, rx: r, ry: r, class: 'bar-segment ' + cssClass });
            } else if (isBottomSegment) {
                const d = [
                    'M ' + x + ' ' + yTop, 'H ' + (x+w), 'V ' + (yBottom-r),
                    'A ' + r + ' ' + r + ' 0 0 1 ' + (x+w-r) + ' ' + yBottom, 'H ' + (x+r),
                    'A ' + r + ' ' + r + ' 0 0 1 ' + x + ' ' + (yBottom-r), 'Z'
                ].join(' ');
                segment = createSVGElement('path', { d: d, class: 'bar-segment ' + cssClass });
            } else if (isTopSegment) {
                const d = [
                    'M ' + x + ' ' + yBottom, 'H ' + (x+w), 'V ' + (yTop+r),
                    'A ' + r + ' ' + r + ' 0 0 0 ' + (x+w-r) + ' ' + yTop, 'H ' + (x+r),
                    'A ' + r + ' ' + r + ' 0 0 0 ' + x + ' ' + (yTop+r), 'Z'
                ].join(' ');
                segment = createSVGElement('path', { d: d, class: 'bar-segment ' + cssClass });
            } else {
                segment = createSVGElement('rect', { x: x, y: yTop, width: w, height: height, class: 'bar-segment ' + cssClass });
            }
            barGroup.appendChild(segment);

            // 6. ラベルを描画 (アニメーション後の高さ/Y座標 と 透明度を適用)
            const finalPercentageValue = finPerc; // 表示する%は最終的な値
            
            // ▼▼▼【[K] ここを変更】▼▼▼
            // 判定を animatedPercentage から finalPercentageValue に変更
            // (最終的に 5% 以下の項目はアニメーション中も非表示にする)
            if (finalPercentageValue > 5) {
            // ▲▲▲【[K] 変更ここまで】▲▲▲

                barGroup.appendChild(createSVGElement('text', { 
                    x: centerX, 
                    y: yTop + height / 2, // アニメーション後のY
                    'dominant-baseline': 'middle', 
                    class: 'segment-label',
                    style: 'opacity: ' + animOpacity // 0.7-1.0でフェードイン
                }, Math.round(finalPercentageValue) + '%'));
            }

            // 7. 次のセグメントのためにオフセットを増やす
            animatedYOffsetPercentage += animatedPercentage;
        });





        chartAreaGroup.appendChild(barGroup);
      });
    }


    // グラフ描画を実行
    try {
      const configJson = document.getElementById('chart-json-data').textContent;
      if (!configJson || configJson.trim() === '') {
        throw new Error("JSON data is empty.");
      }
      let fullConfig;
      try {
        fullConfig = JSON.parse(configJson);
      } catch (parseError) {
        // === ▼ バッククォート不使用に変更 ▼ ===
        throw new Error('JSON parsing error: ' + parseError.message);
        // === ▲ 変更 ▲ ===
      }
      
      // ▼▼▼【変更点】ネストされた "data" キーから設定を読み込む ▼▼▼
      if (!fullConfig || typeof fullConfig !== 'object' || !fullConfig.data) {
        throw new Error("Invalid JSON structure: Missing 'data' key.");
      }
      const config = fullConfig.data;
      // ▲▲▲ ここまで ▲▼▼

      // ▼▼▼【ここから変更】 "colors" が文字列だった場合の処理を分岐 ▼▼▼
      if (config.colors && typeof config.colors === 'string') {
        
        const numColors = config.legendLabels ? config.legendLabels.length : 5;
        const generatedIds = [];
        for (let i = 0; i < numColors; i++) {
          generatedIds.push(String.fromCharCode(65 + i)); // 'A', 'B', 'C'...
        }
        
        let generatedColors; // ここで宣言
        const colorKeyword = config.colors.toLowerCase();
        
        const gradientDarken = 15; // すべてのグラデーションに適用される濃淡差

        // ▼▼▼【分岐1】#gemini の場合 ▼▼▼
        if (colorKeyword === '#gemini') {
          // 濃いピンク -> 紫 -> 青
          const GEMINI_STOPS = ['#E68A9C', '#9F63D0', '#616AD8'];
          const hexPalette = generateGradientScale(GEMINI_STOPS, numColors);
          generatedColors = [];
          
          for (let i = 0; i < numColors; i++) {
            const hex_start = hexPalette[i];
            const rgb_start = hexToRgb(hex_start);
            const [h, s, l] = rgbToHsl(rgb_start.r, rgb_start.g, rgb_start.b);
            const l_end = Math.max(0, Math.min(100, l - gradientDarken));
            const rgb_end = hslToRgb(h, s, l_end);
            const hex_end = rgbToHex(rgb_end[0], rgb_end[1], rgb_end[2]);
            
            generatedColors.push({
              id: generatedIds[i],
              start: hex_start,
              end: hex_end
            });
          }
        
        // ▼▼▼【分岐2: 変更】#night の場合 ▼▼▼
        } else if (colorKeyword === '#night') {
          // ユーザー指定の固定カラーパレット (start と end のペア)
          // 項目 A, B, C, D, E にこの順で適用されます
const NIGHT_COLORS = [
          { start: '#8d58d3', end: '#704ca4' }, // 項目 A (紫)
          { start: '#8f83d6', end: '#7469a8' }, // 項目 B (青紫)
          { start: '#7891dd', end: '#5c6fbb' }, // 項目 C (青)
          { start: '#50a89d', end: '#258a7f' }, // 項目 D (青緑)
          { start: '#54c5d5', end: '#34a0b0' }  // 項目 E (水色)
        ];
          generatedColors = [];
          
          // generatedIds ('A', 'B', 'C'...) と NIGHT_COLORS をマッピング
          for (let i = 0; i < numColors; i++) {
            // 該当する色を取得 (もし定義数を超えたら最後のを繰り返す)
            const colorPair = NIGHT_COLORS[i] || NIGHT_COLORS[NIGHT_COLORS.length - 1];
            
            generatedColors.push({
              id: generatedIds[i], // 'A', 'B', 'C'...
              start: colorPair.start,
              end: colorPair.end
            });
          }

        } else {
          // ▼▼▼【分岐3】HEXコード（単色・交互濃淡）の場合 ▼▼▼
          const primaryColor = config.colors;
          // generateMonochromaticScale関数内で gradientDarken が7で固定されているため、
          // もし単色指定の濃淡差も15に統一したい場合は、この関数を修正するか、
          // こちらでstart/endを再計算する必要があります。
          // 今回の要望は #night の調整がメインのため、ここは現状維持とします。
          generatedColors = generateMonochromaticScale(primaryColor, generatedIds);
          // HEXコードが無効だった場合
          if (generatedColors.length === 0) {
             throw new Error("Invalid HEX color string in 'colors': " + primaryColor);
          }
        }
        
        // 最終的な色設定を config に上書き
        if (generatedColors && generatedColors.length > 0) {
          config.colors = generatedColors;
        } else {
          // このルートは通常通らないはずだが、念のため
           throw new Error("Failed to generate colors from keyword: " + config.colors);
        }
        
      } else if (!config.colors || !Array.isArray(config.colors)) {
         // "colors" が配列でも文字列でもない場合はエラー
         throw new Error("'colors' must be an array of color objects or a single primary color string.");
      }
      // ▲▲▲【ここまで変更】▲▲▲
      
      if (!config || typeof config !== 'object' || !config.barData || !Array.isArray(config.barData)) {
        throw new Error("Invalid JSON structure: Missing required fields like 'barData' inside 'data'.");
      }
      buildChart(config);
    } catch(e) {
      console.error("Chart Error:", e);
      const svgRoot = document.getElementById('100-stacked-bar-chart-svg');
      const existingError = svgRoot.querySelector('.error-message');
      if(existingError) existingError.remove();
      // Remove old error message if present
      const textEl = createSVGElement('text', {
        x: svgRoot.viewBox.baseVal.width / 2 + svgRoot.viewBox.baseVal.x, // Center in viewBox
        y: svgRoot.viewBox.baseVal.height / 2 + svgRoot.viewBox.baseVal.y,
        'text-anchor': 'middle', fill: 'red',
        'font-family': "'Noto Sans JP', sans-serif", // Ensure correct font
        class: 'error-message' // Add class for potential removal
      });
      textEl.textContent = e.message.includes('JSON') ? 'JSONデータにエラーがあります。' : 'グラフ描画エラーが発生しました。';
      if (e.message.includes('JSON parsing error')) {
        // === ▼ バッククォート不使用に変更 ▼ ===
        textEl.textContent += ' (' + e.message.split(': ')[1] + ')';
        // === ▲ 変更 ▲ ===
      }
      svgRoot.appendChild(textEl);
    }

    // ]]>
  </script>
</svg>
`;




const LINE_CHART_TEMPLATE = `
<svg width="600" height="465" viewBox="25 25 575 430" xmlns="http://www.w3.org/2000/svg" font-family="'Noto Sans JP', sans-serif" id="line-chart-svg">

  <script id="chart-json-data" type="application/json">
    {
      "chartType": "line",
      "data": {
        "title": "年間データ推移",
        "subtitle": "（サンプルデータ）",
        "source": "出典：サンプルデータ",
        "yAxisUnitLabel": "（単位）",
        "items": [
          { "label": "1月", "value": 8 },
          { "label": "2月", "value": 9 },
          { "label": "3月", "value": 12 },
          { "label": "4月", "value": 16 },
          { "label": "5月", "value": 20 },
          { "label": "6月", "value": 24 },
          { "label": "7月", "value": 28 },
          { "label": "8月", "value": 27 },
          { "label": "9月", "value": 35 }
        ],
        "colors": {
          "start": "#e68a9c",
          "end": "#b469b8",
          "line": "#b469b8",
          "label": "#8c4fc8"
        },
        "layout": {
          "width": 600,
          "height": 465,
          "marginTop": 110,
          "marginBottom": 75,
          "marginLeft": 75,
          "marginRight": 25
        },
        "yAxis": {
          "max": 40,
          "min": 0,
          "tickCount": 4
        },
        "lineOptions": {
          "markerRadius": 5,
          "dataLabelOffsetY": 15,
          "horizontalPadding": 30
        },
        "animation": 1
      }
    }
</script>
  <defs>
    <filter id="shadow">
      <feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2"/>
    </filter>
    <filter id="text-halo">
      <feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="white" flood-opacity="0.9"/>
    </filter>
  </defs>

  <style>
    @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&amp;family=Roboto:wght@400;700&amp;display=swap');
    /* 基本スタイル (デフォルト Noto Sans JP) */
    .title { font-size: 24px; font-weight: 500; text-anchor: middle; fill: #5f6368; }
    .subtitle { font-size: 16px; text-anchor: middle; fill: #5f6368; }
    .grid-line { stroke: #e0e0e0; stroke-dasharray: 2 2; stroke-width: 0.8; }
    .axis-label { font-size: 13px; fill: #5f6368; text-anchor: middle; }
    .source { font-size: 11px; fill: #70757a; text-anchor: end; }
    /* 数字用のフォント指定 */
    .y-axis-value { font-family: 'Roboto', sans-serif; font-size: 12px; fill: #757575; text-anchor: end; }
    /* 動的スタイル (line, marker, area, data-label) はJSで追加 */
  </style>

  <g id="title-group"></g>
  <g id="y-axis-group"></g>
  <g id="chart-area-group"></g>
  <g id="x-axis-group"></g>
  <g id="source-group"></g>

  <script type="text/javascript">
    // <![CDATA[

    // =================================================================
    // ロジックコード
    // =================================================================
    const svgNS = "http://www.w3.org/2000/svg";
    function createSVGElement(name, attributes, textContent) {
      const el = document.createElementNS(svgNS, name);
      for (const key in attributes) el.setAttribute(key, attributes[key]);
      if (textContent !== undefined && textContent !== null) el.textContent = textContent;
      return el;
    }
    
    // =================================================================
    // 色計算用のヘルパー関数
    // =================================================================
    function hexToRgb(hex) {
      let shorthandRegex = /^#?([a-f0-9])([a-f0-9])([a-f0-9])$/i;
      hex = hex.replace(shorthandRegex, function(m, r, g, b) {
        return r + r + g + g + b + b;
      });
      let result = /^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex);
      return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
      } : null;
    }
    function rgbToHsl(r, g, b) {
      r /= 255; g /= 255; b /= 255;
      let max = Math.max(r, g, b), min = Math.min(r, g, b);
      let h, s, l = (max + min) / 2;
      l = Math.round(l * 100);
      if (max == min) { h = s = 0; } else {
        let d = max - min;
        s = l > 50 ? d / (2 - max - min) : d / (max + min);
        s = Math.round(s * 100);
        switch (max) {
          case r: h = (g - b) / d + (g < b ? 6 : 0); break;
          case g: h = (b - r) / d + 2; break;
          case b: h = (r - g) / d + 4; break;
        }
        h /= 6;
      }
      h = Math.round(h * 360);
      return [h, s, l];
    }
    function hslToRgb(h, s, l) {
      let r, g, b;
      h /= 360; s /= 100; l /= 100;
      if (s == 0) { r = g = b = l; } else {
        function hue2rgb(p, q, t) {
          if (t < 0) t += 1;
          if (t > 1) t -= 1;
          if (t < 1/6) return p + (q - p) * 6 * t;
          if (t < 1/2) return q;
          if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
          return p;
        }
        let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        let p = 2 * l - q;
        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3);
      }
      return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
    }
    function rgbToHex(r, g, b) {
      function componentToHex(c) {
        let hex = c.toString(16);
        return hex.length == 1 ? "0" + hex : hex;
      }
      return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
    }
    // =================================================================
    
    function truncateText(text, maxWidth, className) {
      const svgRoot = document.getElementById('line-chart-svg');
      const tempText = createSVGElement('text', { class: className, style: 'visibility: hidden;' });
      svgRoot.appendChild(tempText);
      tempText.textContent = text;
      if (tempText.getComputedTextLength() <= maxWidth) {
        svgRoot.removeChild(tempText);
        return text;
      }
      let truncatedText = text;
      while (truncatedText.length > 0) {
        truncatedText = truncatedText.slice(0, -1);
        tempText.textContent = truncatedText + '...';
        if (tempText.getComputedTextLength() <= maxWidth) {
          svgRoot.removeChild(tempText);
          return truncatedText + '...';
        }
      }
      svgRoot.removeChild(tempText);
      return '...';
    }

    function buildChart(config) {
      const svgRoot = document.getElementById('line-chart-svg');
      const defs = svgRoot.querySelector('defs');
      const style = svgRoot.querySelector('style');

      // Clear existing dynamic gradients and styles
      const existingGrads = defs.querySelectorAll('linearGradient');
      existingGrads.forEach(grad => grad.remove());
      let styleContent = style.textContent;
      styleContent = styleContent.replace(/[.]line {[^}]*}/g, '');
      styleContent = styleContent.replace(/[.]marker {[^}]*}/g, '');
      styleContent = styleContent.replace(/[.]area {[^}]*}/g, '');
      styleContent = styleContent.replace(/[.]data-label {[^}]*}/g, '');
      style.textContent = styleContent.trim();
      
      // Generate dynamic styles and definitions
      const markerGradId = 'markerGradient';
      let markerGrad = createSVGElement('linearGradient', { id: markerGradId, x1:"0", x2:"0", y1:"0", y2:"1" });
      markerGrad.appendChild(createSVGElement('stop', { offset: '0%', 'stop-color': config.color.start }));
      markerGrad.appendChild(createSVGElement('stop', { offset: '100%', 'stop-color': config.color.end }));
      defs.appendChild(markerGrad);

      const areaGradId = 'areaGradient';
      let areaGrad = createSVGElement('linearGradient', { id: areaGradId, x1:"0", x2:"0", y1:"0", y2:"1" });
      areaGrad.appendChild(createSVGElement('stop', { offset: '0%', 'stop-color': config.color.start, 'stop-opacity': '0.2' }));
      areaGrad.appendChild(createSVGElement('stop', { offset: '100%', 'stop-color': config.color.end, 'stop-opacity': '0' }));
      defs.appendChild(areaGrad);
      
      const nl = String.fromCharCode(10);
      let dynamicStyles = [
          "",
          "            .line { fill: none; stroke: " + config.color.line + "; stroke-width: 3; stroke-linejoin: round; stroke-linecap: round; filter: url(#shadow);",
          "            }",
          "            .marker { fill: url(#" + markerGradId + "); stroke: white; stroke-width: 2; filter: url(#shadow); }",
          "            .area { fill: url(#" + areaGradId + ");",
          "            }",
          "            .data-label { font-family: 'Roboto', sans-serif; font-size: 14px; font-weight: bold; fill: " + config.color.label + "; text-anchor: middle; filter: url(#text-halo); }",
          "        "
      ].join(nl);
      
      style.textContent += dynamicStyles;

      const groups = {
        title: document.getElementById('title-group'),
        yAxis: document.getElementById('y-axis-group'),
        chartArea: document.getElementById('chart-area-group'),
        xAxis: document.getElementById('x-axis-group'),
        source: document.getElementById('source-group')
      };
      for (const key in groups) {
        while (groups[key].firstChild) groups[key].removeChild(groups[key].firstChild);
      }

      const chartAreaWidth = config.layout.width - config.layout.marginLeft - config.layout.marginRight;
      const chartAreaHeight = config.layout.height - config.layout.marginTop - config.layout.marginBottom;
      const effectivePlotWidth = chartAreaWidth - (config.lineOptions.horizontalPadding * 2);
      const dataMax = Math.max.apply(null, config.items.map(function(item) { return item.value; }).concat([config.yAxis.max]));
      const dataMin = Math.min.apply(null, config.items.map(function(item) { return item.value; }).concat([config.yAxis.min]));
      
      const hasDataDecimals = config.items.some(function(item) {
        return typeof item.value === 'number' && item.value % 1 !== 0;
      });
      const hasAxisDecimals = (typeof config.yAxis.min === 'number' && config.yAxis.min % 1 !== 0) ||
        (typeof config.yAxis.max === 'number' && config.yAxis.max % 1 !== 0);
      const showDecimals = hasDataDecimals || hasAxisDecimals;

      let effectiveYAxisMax, effectiveYAxisMin;
      const tickCount = config.yAxis.tickCount > 0 ? config.yAxis.tickCount : 1;
      const preliminaryRange = Math.max(0.1, config.yAxis.max - config.yAxis.min);
      let tickInterval = preliminaryRange / tickCount;
      
      if (dataMin < config.yAxis.min) {
        const limit = Math.max(Math.abs(dataMin), dataMax);
        if (tickInterval <= 0 || isNaN(tickInterval)) tickInterval = Math.max(1, limit / 5);
        effectiveYAxisMax = Math.ceil(limit / tickInterval) * tickInterval;
        effectiveYAxisMin = -effectiveYAxisMax;
        if (config.yAxis.min > effectiveYAxisMin) effectiveYAxisMin = config.yAxis.min;
        if (config.yAxis.max < effectiveYAxisMax) effectiveYAxisMax = config.yAxis.max;
      } else {
        effectiveYAxisMin = config.yAxis.min;
        if (tickInterval <= 0 || isNaN(tickInterval)) tickInterval = Math.max(1, (dataMax - effectiveYAxisMin) / 5) || 1;
        effectiveYAxisMax = Math.max(config.yAxis.max, Math.ceil(dataMax / tickInterval) * tickInterval);
      }

      if (effectiveYAxisMax <= effectiveYAxisMin) {
        effectiveYAxisMax = effectiveYAxisMin + tickInterval * tickCount;
        if (effectiveYAxisMax <= effectiveYAxisMin) effectiveYAxisMax = effectiveYAxisMin + 1;
      }
      
      const yRange = effectiveYAxisMax - effectiveYAxisMin;
      const xScale = function(index) { return (config.layout.marginLeft + config.lineOptions.horizontalPadding) + (config.items.length > 1 ? (index * (effectivePlotWidth / (config.items.length - 1))) : effectivePlotWidth / 2 ); };
      const yScale = function(value) { return (yRange === 0) ? config.layout.marginTop + chartAreaHeight / 2 : config.layout.marginTop + chartAreaHeight * (1 - (value - effectiveYAxisMin) / yRange); };
      const bottomY = config.layout.marginTop + chartAreaHeight;

      // ▼▼▼【ここから変更】 2種類のアニメーション変数を定義 ▼▼▼
      
      // 1. グラフの「位置」用 (0.0 -> 1.0)
      const animationProgress = (config.animation == null) 
          ? 1.0 
          : Math.max(0, Math.min(1, Number(config.animation)));

      // 2. データラベルの「透明度」用 (0.7 -> 1.0 でフェードイン)
      let animationOpacity;
      const fadeStart = 0.7; // 0.7 からフェードイン開始
      if (animationProgress <= fadeStart) {
          animationOpacity = 0;
      } else {
          // 0.7 (0%) から 1.0 (100%) の間で 0 -> 1 に変化
          animationOpacity = (animationProgress - fadeStart) / (1.0 - fadeStart);
      }
      // ▲▲▲【ここまで変更】▲▲▲

      groups.title.appendChild(createSVGElement('text', { x: 312, y: 50, class: 'title' }, config.title));
      groups.title.appendChild(createSVGElement('text', { x: 312, y: 75, class: 'subtitle' }, config.subtitle));
      groups.source.appendChild(createSVGElement('text', { x: 600, y: 450, class: 'source' }, config.source));
      groups.yAxis.appendChild(createSVGElement('text', { x: config.layout.marginLeft, y: config.layout.marginTop - 15, class: 'y-axis-value', 'text-anchor': 'end' }, config.yAxisUnitLabel));

      const finalTickStep = yRange / tickCount;
      for (let i = 0; i <= tickCount; i++) {
        const tickValue = effectiveYAxisMin + (finalTickStep * i);
        const y = yScale(tickValue);
        let tickLabel;
        if (showDecimals) {
          tickLabel = parseFloat(tickValue.toFixed(5));
        } else {
          tickLabel = Math.round(tickValue);
        }
        groups.yAxis.appendChild(createSVGElement('text', { x: config.layout.marginLeft - 10, y, class: 'y-axis-value', 'dominant-baseline': 'middle' }, tickLabel));
        if (i > 0 && (Math.abs(tickValue) > 1e-6 || effectiveYAxisMin >= 0)) {
          groups.yAxis.appendChild(createSVGElement('line', { x1: config.layout.marginLeft, y1: y, x2: config.layout.width - config.layout.marginRight, y2: y, class: 'grid-line' }));
        }
      }
      if (effectiveYAxisMin < 0 && effectiveYAxisMax > 0) {
        groups.yAxis.appendChild(createSVGElement('line', { x1: config.layout.marginLeft, y1: yScale(0), x2: config.layout.width - config.layout.marginRight, y2: yScale(0), stroke: '#bdbdbd', 'stroke-width': 1 }));
      }

      groups.xAxis.appendChild(createSVGElement('line', { x1: config.layout.marginLeft, y1: bottomY, x2: config.layout.width - config.layout.marginRight, y2: bottomY, stroke: '#bdbdbd', 'stroke-width': 1 }));
      const xSlotWidth = config.items.length > 1 ? effectivePlotWidth / (config.items.length - 1) : effectivePlotWidth;
      config.items.forEach(function(item, index) {
        const x = xScale(index);
        const labelContent = truncateText(item.label, xSlotWidth * 0.9, 'axis-label');
        groups.xAxis.appendChild(createSVGElement('text', { x: x, y: bottomY + 20, class: 'axis-label' }, labelContent));
      });
      
      if (config.items.length > 0) {
        
        // ▼▼▼【ここから変更】 animatedY の計算を animationProgress に基づいて行う ▼▼▼
        const areaBottomY = yScale(Math.max(0, effectiveYAxisMin)); // Baseline at 0 or axis min
        
        const pointsData = config.items.map(function(item, index) {
            const x = xScale(index);
            const finalY = yScale(item.value); // 最終的なY座標（ラベル位置決め用）
            
            // グラフ位置のアニメーション (0.0 -> 1.0 でせり上がる)
            const animatedY = areaBottomY + (finalY - areaBottomY) * animationProgress;

            return {
                x: x,
                finalY: finalY,     // ラベル位置決め用
                animatedY: animatedY, // 線/マーカー/エリアの描画用
                value: item.value
            };
        });

        // 描画は animatedY を使う
        const points = pointsData.map(function(d) { return d.x + ',' + d.animatedY; });
        const areaPoints = xScale(0) + ',' + areaBottomY + ' ' + points.join(' ') + ' ' + xScale(config.items.length - 1) + ',' + areaBottomY;
        // ▲▲▲【ここまで変更】▲▲▲

        groups.chartArea.appendChild(createSVGElement('polygon', { class: 'area', points: areaPoints }));
        groups.chartArea.appendChild(createSVGElement('polyline', { class: 'line', points: points.join(' ') }));
        
        const dataPointsGroup = createSVGElement('g', { class: 'data-points' });
        
        // ▼▼▼【ここから変更】 グラフ位置(animatedY) と ラベル透明度(animationOpacity) を別々に適用 ▼▼▼
        pointsData.forEach(function(data, index) {
          const x = data.x;
          
          // マーカーのY座標は 'animatedY' (せり上がる)
          const y = data.animatedY;
          // ラベルの位置計算は 'finalY' (固定)
          const finalY = data.finalY; 
          
          // 1. マーカー描画 (animatedY を使用)
          dataPointsGroup.appendChild(createSVGElement('circle', { 
              cx: x, 
              cy: y, // アニメーション適用後のY座標
              r: config.lineOptions.markerRadius, 
              class: 'marker' 
          }));

          let labelY;
          // ▼▼▼【ここから変更】 判定基準を 'finalY' から 'y' (animatedY) に変更 ▼▼▼
          // 2. ラベルの「位置」計算 (y を使用)
  if (y < config.layout.marginTop + 20) { 
            labelY = y + config.lineOptions.dataLabelOffsetY + 5; 
          } else {
            labelY = y - config.lineOptions.dataLabelOffsetY;

  }
          // ▲▲▲【ここまで変更】▲▲▲

          let dataLabelValue = data.value;
          if (!showDecimals) {
            dataLabelValue = Math.round(dataLabelValue);
          }

          // 3. ラベル描画 (labelY と animationOpacity を使用)
          dataPointsGroup.appendChild(createSVGElement('text', { 
              x: x, 
              y: labelY, // 最終的なY座標 (固定)
              class: 'data-label',
              style: 'opacity: ' + animationOpacity // 0.7-1.0でフェードイン
          }, dataLabelValue));
        });
        // ▲▲▲【ここまで変更】▲▲▲
        groups.chartArea.appendChild(dataPointsGroup);
      }
    }

    // =================================================================
    // グラフ描画を実行
    // =================================================================
    try {
      const configJson = document.getElementById('chart-json-data').textContent;
      if (!configJson || configJson.trim() === '') {
        throw new Error("JSON data is empty.");
      }
      let fullConfig;
      try {
        fullConfig = JSON.parse(configJson);
      } catch (parseError) {
        throw new Error('JSON parsing error: ' + parseError.message);
      }
      
      if (!fullConfig || typeof fullConfig !== 'object' || !fullConfig.data) {
        throw new Error("Invalid JSON structure: Missing 'data' key.");
      }
      const config = fullConfig.data;

      // layout の既定値処理（Gemプロンプトは layout を省略してよい仕様。無いと config.layout.width で落ちて
      //   「グラフ描画エラー」になる。指定があれば個々の値だけ上書きする）
      const defaultLayout = { width: 600, height: 465, marginTop: 110, marginBottom: 75, marginLeft: 75, marginRight: 25 };
      config.layout = Object.assign({}, defaultLayout, (config.layout && typeof config.layout === 'object') ? config.layout : {});

      // lineOptions のデフォルト値処理
      const defaultLineOptions = {
          markerRadius: 5,
          dataLabelOffsetY: 15,
          horizontalPadding: 30
      };
      if (config.lineOptions && typeof config.lineOptions === 'object') {
          config.lineOptions = {
              markerRadius: (config.lineOptions.markerRadius !== undefined && typeof config.lineOptions.markerRadius === 'number') ?
                config.lineOptions.markerRadius : defaultLineOptions.markerRadius,
              dataLabelOffsetY: (config.lineOptions.dataLabelOffsetY !== undefined && typeof config.lineOptions.dataLabelOffsetY === 'number') ?
                config.lineOptions.dataLabelOffsetY : defaultLineOptions.dataLabelOffsetY,
              horizontalPadding: (config.lineOptions.horizontalPadding !== undefined && typeof config.lineOptions.horizontalPadding === 'number') ?
                config.lineOptions.horizontalPadding : defaultLineOptions.horizontalPadding
          };
      } else {
          config.lineOptions = defaultLineOptions;
      }
      
      if (config.colors) {
          config.color = config.colors;
      }

      // "color" が文字列だった場合の処理を分岐
      if (config.color && typeof config.color === 'string') {
        const colorKeyword = config.color.toLowerCase();
        let generatedColorObject;
        if (colorKeyword === '#gemini') {
          const GEMINI_STOPS = ['#E68A9C', '#9F63D0', '#616AD8'];
          generatedColorObject = {
            start: GEMINI_STOPS[0], line:  GEMINI_STOPS[1],
            end:   GEMINI_STOPS[1], label: GEMINI_STOPS[2]
          };
        } else if (colorKeyword === '#night') {
          const NIGHT_COLORS = [
            { start: '#8d58d3', end: '#704ca4' },
            { start: '#8f83d6', end: '#7469a8' },
            { start: '#7891dd', end: '#5c6fbb' }
          ];
          generatedColorObject = {
            start: NIGHT_COLORS[0].start, end:   NIGHT_COLORS[1].start,
            line:  NIGHT_COLORS[1].end,   label: NIGHT_COLORS[2].end
          };
        } else {
          const primaryColor = config.color;
          const rgb = hexToRgb(primaryColor);
          if (!rgb) { throw new Error("Invalid HEX color string in 'color': " + primaryColor); }
          const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
          const h = hsl[0], s = hsl[1], l = hsl[2];
          const l_start = Math.min(100, l + 20);
          const s_start = (l > 80) ? s * 0.7 : s;
          const rgb_start = hslToRgb(h, s_start, l_start);
          const hex_start = rgbToHex(rgb_start[0], rgb_start[1], rgb_start[2]);
          const l_label = Math.max(0, l - 15);
          const s_label = (l < 20) ? s * 0.7 : s;
          const rgb_label = hslToRgb(h, s_label, l_label);
          const hex_label = rgbToHex(rgb_label[0], rgb_label[1], rgb_label[2]);
          generatedColorObject = {
            start: hex_start, end:   primaryColor,
            line:  primaryColor, label: hex_label
          };
        }
        config.color = generatedColorObject;
      } else if (!config.color || typeof config.color !== 'object') {
         throw new Error("'color' must be a color object or a single color string.");
      }

      if (!config || typeof config !== 'object' || !config.items || !Array.isArray(config.items)) {
        throw new Error("Invalid JSON structure: Missing required fields like 'items' inside 'data'.");
      }
      
      // 描画を実行
      buildChart(config);
      
    } catch (e) {
      console.error("Chart Error:", e);
      const svgRoot = document.getElementById('line-chart-svg');
      const existingError = svgRoot.querySelector('.error-message');
      if(existingError) existingError.remove();
      const textEl = createSVGElement('text', {
        x: svgRoot.viewBox.baseVal.width / 2 + svgRoot.viewBox.baseVal.x,
        y: svgRoot.viewBox.baseVal.height / 2 + svgRoot.viewBox.baseVal.y,
        'text-anchor': 'middle', fill: 'red',
        'font-family': "'Noto Sans JP', sans-serif",
        class: 'error-message'
      });
      textEl.textContent = e.message.includes('JSON') ? 'JSONデータにエラーがあります。' : 'グラフ描画エラーが発生しました。';
      if (e.message.includes('JSON parsing error')) {
        textEl.textContent += ' (' + e.message.split(': ')[1] + ')';
      }
      svgRoot.appendChild(textEl);
    }

    // ]]>
  </script>
</svg>
`;



const DONUT_CHART_TEMPLATE = `
<svg width="600" height="460" viewBox="0 25 600 425" xmlns="http://www.w3.org/2000/svg" font-family="'Noto Sans JP', sans-serif" id="donut-chart-svg">

  <script id="chart-json-data" type="application/json">
    {
      "chartType": "donut",
      "data": {
        "title": "グラフタイトル",
        "subtitle": "グラフサブタイトル",
        "source": "出典: データソース",
        "centerLabel": "合計ラベル",
        "colors": [
          { "id": "A", "start": "#e68a9c", "end": "#d96d8f" },
          { "id": "B", "start": "#b469b8", "end": "#a656ad" },
          { "id": "C", "start": "#9f63d0", "end": "#8c4fc8" },
          { "id": "D", "start": "#7c6ce8", "end": "#6b5ce0" },
          { "id": "E", "start": "#616ad8", "end": "#5059d1" }
        ],
        "items": [
          { "label": "項目 A", "value": 40, "id": "A" },
          { "label": "項目 B", "value": 25, "id": "B" },
          { "label": "項目 C", "value": 15, "id": "C" },
          { "label": "項目 D", "value": 10, "id": "D" },
          { "label": "項目 E", "value": 10, "id": "E" }
        ],
        "animation" : 1
      }
    }
  </script>
    
  <defs>
    <filter id="shadow"><feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.4"/></filter>
  </defs>

  <style>
    @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&amp;family=Roboto:wght@400;700&amp;display=swap');
    .title { font-size: 24px; font-weight: 500; text-anchor: middle; fill: #5f6368; }
    .subtitle { font-size: 16px; text-anchor: middle; fill: #5f6368; }
    .source { font-size: 11px; fill: #70757a; text-anchor: end; }
    .center-total-label { font-size: 14px; fill: #5f6368; text-anchor: middle; dominant-baseline: central; }
    .legend-item { font-size: 13px; fill: #5f6368; dominant-baseline: middle; }
    .donut-segment { filter: url(#shadow); }
    .center-total-value { font-family: 'Roboto', sans-serif; font-size: 28px; font-weight: bold; fill: #5f6368; text-anchor: middle; dominant-baseline: central; }
    .legend-percentage { font-family: 'Roboto', sans-serif; font-weight: 400; }
  </style>

  <g id="title-group"></g>
  <g id="donut-group"></g>
  <g id="legend-group"></g>
  <g id="source-group"></g>

  <script type="text/javascript">
    // <![CDATA[

    // =================================================================
    // 既存のヘルパー関数
    // =================================================================
    const svgNS = "http://www.w3.org/2000/svg";
    function createSVGElement(name, attributes, textContent) {
      const el = document.createElementNS(svgNS, name);
      for (const key in attributes) el.setAttribute(key, attributes[key]);
      if (textContent !== undefined && textContent !== null) el.textContent = textContent;
      return el;
    }

    function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
      const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
      return {
        x: centerX + (radius * Math.cos(angleInRadians)),
        y: centerY + (radius * Math.sin(angleInRadians))
      };
    }
    
    function createDonutSegmentPath(x, y, radius, startAngle, endAngle) {
      if (endAngle - startAngle >= 360) endAngle = 359.99;
      const start = polarToCartesian(x, y, radius, endAngle);
      const end = polarToCartesian(x, y, radius, startAngle);
      const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
      return 'M ' + x + ' ' + y + ' L ' + start.x + ' ' + start.y + ' A ' + radius + ' ' + radius + ' 0 ' + largeArcFlag + ' 0 ' + end.x + ' ' + end.y + ' Z';
    }

    // =================================================================
    // ▼▼▼【ここから追加】色計算用のヘルパー関数 (棒グラフから移植) ▼▼▼
    // =================================================================
    
    /** HEX (例: #FF0000) を RGB (例: {r: 255, g: 0, b: 0}) に変換 */
    function hexToRgb(hex) {
      let shorthandRegex = /^#?([a-f0-9])([a-f0-9])([a-f0-9])$/i;
      hex = hex.replace(shorthandRegex, function(m, r, g, b) {
        return r + r + g + g + b + b;
      });
      let result = /^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex);
      return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
      } : null;
    }

    /** RGB (0-255) を HSL (H: 0-360, S: 0-100, L: 0-100) に変換 */
    function rgbToHsl(r, g, b) {
      r /= 255; g /= 255; b /= 255;
      let max = Math.max(r, g, b), min = Math.min(r, g, b);
      let h, s, l = (max + min) / 2;
      l = Math.round(l * 100); // %に変換

      if (max == min) {
        h = s = 0; // achromatic
      } else {
        let d = max - min;
        s = l > 50 ? d / (2 - max - min) : d / (max + min);
        s = Math.round(s * 100); // %に変換
        switch (max) {
          case r: h = (g - b) / d + (g < b ? 6 : 0); break;
          case g: h = (b - r) / d + 2; break;
          case b: h = (r - g) / d + 4; break;
        }
        h /= 6;
      }
      h = Math.round(h * 360); // 角度に変換
      return [h, s, l];
    }

    /** HSL (H: 0-360, S: 0-100, L: 0-100) を RGB (0-255) に変換 */
    function hslToRgb(h, s, l) {
      let r, g, b;
      h /= 360; // 0-1 に
      s /= 100; // 0-1 に
      l /= 100; // 0-1 に

      if (s == 0) {
        r = g = b = l; // achromatic
      } else {
        function hue2rgb(p, q, t) {
          if (t < 0) t += 1;
          if (t > 1) t -= 1;
          if (t < 1/6) return p + (q - p) * 6 * t;
          if (t < 1/2) return q;
          if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
          return p;
        }
        let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        let p = 2 * l - q;
        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3);
      }
      return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
    }

    /** RGB (0-255) を HEX (例: #FF0000) に変換 */
    function rgbToHex(r, g, b) {
      function componentToHex(c) {
        let hex = c.toString(16);
        return hex.length == 1 ? "0" + hex : hex;
      }
      return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
    }
    
    /** 2つのHEXカラー間を線形補間する */
    function linearInterpolateColor(hex1, hex2, t) {
      const rgb1 = hexToRgb(hex1);
      const rgb2 = hexToRgb(hex2);
      
      const r = Math.round(rgb1.r + (rgb2.r - rgb1.r) * t);
      const g = Math.round(rgb1.g + (rgb2.g - rgb1.g) * t);
      const b = Math.round(rgb1.b + (rgb2.b - rgb1.b) * t);
      
      return rgbToHex(r, g, b);
    }

    /** 複数のカラーストップからグラデーションパレット（HEX配列）を生成する */
    function generateGradientScale(gradientStops, numSteps) {
      const palette = [];
      if (numSteps <= 0) return [];
      if (numSteps === 1) return [gradientStops[0]]; // 1色だけなら最初の色
      
      const numSegments = gradientStops.length - 1;

      for (let i = 0; i < numSteps; i++) {
        // 全体の進捗 (0.0 ... 1.0)
        const t_global = (numSteps === 1) ? 0 : (i / (numSteps - 1));
        
        // どのセグメントにいるか
        const segmentProgress = t_global * numSegments;
        const stopIndex1 = Math.floor(segmentProgress);
        const stopIndex2 = Math.min(stopIndex1 + 1, numSegments); 
        
        // そのセグメント内でのローカルな進捗 (0.0 ... 1.0)
        const t_local = segmentProgress - stopIndex1;
        
        const hex1 = gradientStops[stopIndex1];
        const hex2 = gradientStops[stopIndex2];
        
        palette.push(linearInterpolateColor(hex1, hex2, t_local));
      }
      return palette;
    }


    /**
      * 【単色用】プライマリーカラーとID配列から、自動計算された "colors" 配列を生成する
      * (交互の濃淡パターン)
      */
    function generateMonochromaticScale(primaryHex, colorIds) {
      const numSteps = colorIds.length;
      if (numSteps === 0) return [];

      const rgb = hexToRgb(primaryHex);
      if (!rgb) return []; // 無効なHEXの場合は空
      
      const [h, s, l_mid_original] = rgbToHsl(rgb.r, rgb.g, rgb.b);
      
      // ▼▼▼【変更点】ドーナツグラフ用に濃淡差を調整 ▼▼▼
      const gradientDarken = 7; // 棒グラフ(15)より差を小さく
      // ▲▲▲
      
      const L_MAX = 80; 
      const L_MIN = 45; 
      const l_step_amount = numSteps > 1 ? (L_MAX - L_MIN) / (numSteps - 1) : 0;
      
      const l_steps_calc = [];
      for (let i = 0; i < numSteps; i++) {
        const l_val = L_MAX - (i * l_step_amount);
        l_steps_calc.push(l_val);
      }
      
      // 交互パターン生成ロジック (棒グラフから流用)
      const l_steps_light = [];
      const l_steps_dark = [];
      const mid_index = Math.ceil(numSteps / 2);
      for (let i = 0; i < mid_index; i++) {
        l_steps_light.push(l_steps_calc[i]);
      }
      for (let i = mid_index; i < numSteps; i++) {
        l_steps_dark.push(l_steps_calc[i]);
      }
      const l_steps_start = [];
      for (let i = 0; i < numSteps; i++) {
        if (i % 2 === 0) {
          l_steps_start.push(l_steps_light[i / 2]);
        } else {
          l_steps_start.push(l_steps_dark[(i - 1) / 2]);
        }
      }

      const colors = [];
      for (let i = 0; i < numSteps; i++) {
        const l_start_step = l_steps_start[i];
        let l_start = Math.max(0, Math.min(100, l_start_step));
        let l_end = Math.max(0, Math.min(100, l_start - gradientDarken));
        
        let s_adjusted = s;
        if (l_start > 85) s_adjusted = s * (1 - (l_start - 85) / 15);
        if (l_start < 20) s_adjusted = s * (l_start / 20);
        s_adjusted = Math.max(0, Math.min(100, s_adjusted));

        const rgb_start = hslToRgb(h, s_adjusted, l_start);
        const hex_start = rgbToHex(rgb_start[0], rgb_start[1], rgb_start[2]);
        const rgb_end = hslToRgb(h, s_adjusted, l_end);
        const hex_end = rgbToHex(rgb_end[0], rgb_end[1], rgb_end[2]);
        
        colors.push({
          id: colorIds[i],
          start: hex_start,
          end: hex_end
        });
      }
      return colors;
    }
    // =================================================================
    // ▲▲▲【ここまで追加】▲▲▲
    // =================================================================


    function buildChart(config) {

      const animProgress = (config.animation == null) 
        ? 1.0 
        : Math.max(0, Math.min(1, Number(config.animation)));

      const total = config.items.reduce((sum, item) => sum + item.value, 0);
      if (total === 0) return;

      const layout = {
        centerY: 260,
        outerRadius: 150,
        innerRadius: 75,
        legendItemHeight: 30,
        legendColorBoxSize: 12,
        legendTextOffsetX: 18
      };
      const svgElement = document.getElementById('donut-chart-svg');
      const defs = svgElement.querySelector('defs');
      
      // 既存のグラデーションをクリア (動的生成のため)
      const existingGrads = defs.querySelectorAll('linearGradient');
      existingGrads.forEach(grad => grad.remove());

      config.colors.forEach(color => {
        const gradId = 'grad' + color.id;
        
        let gradient = document.getElementById(gradId);
        
        if(gradient) gradient.parentNode.removeChild(gradient);
        gradient = createSVGElement('linearGradient', { id: gradId, x1:"0", x2:"0", y1:"0", y2:"1" });
        gradient.appendChild(createSVGElement('stop', { offset: '0%', 'stop-color': color.start }));
        gradient.appendChild(createSVGElement('stop', { offset: '100%', 'stop-color': color.end }));
        defs.appendChild(gradient);
      });

      const groups = {
        title: document.getElementById('title-group'),
        donut: document.getElementById('donut-group'),
        legend: document.getElementById('legend-group'),
        source: document.getElementById('source-group')
      };
      for (const key in groups) {
        while (groups[key].firstChild) groups[key].removeChild(groups[key].firstChild);
      }
      
      const viewBoxWidth = svgElement.viewBox.baseVal.width;
      const centerX = viewBoxWidth / 4;
      const legendX = viewBoxWidth / 2 + 30;

      config.items.forEach((item, index) => {
        const percentage = ((item.value / total) * 100).toFixed(0);
        const legendItemGroup = createSVGElement('g', {
          class: 'legend-item-group',
          transform: 'translate(0, ' + (index * layout.legendItemHeight) + ')'
        });

        // config.colors から該当する ID の色を探す
        const colorData = config.colors.find(c => c.id === item.id);
        if (!colorData) {
            console.error("Color not found for ID:", item.id);
            return; 
        }
        const color = colorData.end; // 凡例には 'end' カラーを使用

        legendItemGroup.appendChild(createSVGElement('rect', { 
          x: 0, y: 0, 
          width: layout.legendColorBoxSize, height: layout.legendColorBoxSize, 
          rx: 2, fill: color, filter: 'url(#shadow)' 
        }));
        
        const textEl = createSVGElement('text', { 
          x: layout.legendTextOffsetX, 
          y: layout.legendColorBoxSize / 2, 
          class: 'legend-item' 
        });
        
        textEl.textContent = item.label + ' (';
        const percentageSpan = createSVGElement('tspan', { class: 'legend-percentage' }, percentage + '%');
        textEl.appendChild(percentageSpan);
        textEl.appendChild(document.createTextNode(')'));
        legendItemGroup.appendChild(textEl);
        groups.legend.appendChild(legendItemGroup);
      });

      const legendHeight = config.items.length * layout.legendItemHeight;
      const legendY = layout.centerY - (legendHeight / 2);
      groups.legend.setAttribute('transform', 'translate(' + legendX + ', ' + legendY + ')');
      
      const titleX = viewBoxWidth / 2;
      groups.title.appendChild(createSVGElement('text', { x: titleX, y: 50, class: 'title' }, config.title));
      groups.title.appendChild(createSVGElement('text', { x: titleX, y: 75, class: 'subtitle' }, config.subtitle));
      groups.source.appendChild(createSVGElement('text', { x: viewBoxWidth - 10, y: 440, class: 'source' }, config.source));


      // 1. 最終的な角度 (valueに基づく)
    let finalStartAngle = 0;
    const finalAngles = config.items.map(item => {
        const sweep = (item.value / total) * 360;
        const start = finalStartAngle;
        finalStartAngle += sweep;
        return { start: start, end: finalStartAngle };
    });

    // 2. 均等割の角度 (項目数に基づく)
    const numItems = config.items.length;
    const equalSweep = 360 / numItems;
    const equalAngles = config.items.map((item, i) => {
        const start = i * equalSweep;
        const end = (i + 1) * equalSweep;
        return { start: start, end: end };
    });


      let startAngle = 0;


      config.items.forEach((item, index) => {
    // 1. 均等割の角度を取得
    const eqStart = equalAngles[index].start;
    const eqEnd = equalAngles[index].end;

    // 2. 最終的な角度を取得
    const finStart = finalAngles[index].start;
    const finEnd = finalAngles[index].end;

    // 3. アニメーション進捗率に基づいて現在の角度を補間
    // (開始角度)
    const animatedStartAngle = eqStart + (finStart - eqStart) * animProgress;
    // (終了角度)
    const animatedEndAngle = eqEnd + (finEnd - eqEnd) * animProgress;

    // 4. パスを描画
const pathData = createDonutSegmentPath(centerX, layout.centerY, layout.outerRadius, animatedStartAngle, animatedEndAngle);
    groups.donut.appendChild(createSVGElement('path', {
        d: pathData,
        fill: 'url(#grad' + item.id + ')',
class: 'donut-segment'
    }));
});


      groups.donut.appendChild(createSVGElement('circle', { cx: centerX, cy: layout.centerY, r: layout.innerRadius, fill: 'white' }));
      groups.donut.appendChild(createSVGElement('text', { x: centerX, y: layout.centerY - 12, class: 'center-total-value' }, total));
      groups.donut.appendChild(createSVGElement('text', { x: centerX, y: layout.centerY + 15, class: 'center-total-label' }, config.centerLabel));
    }

    // =================================================================
    // ▼▼▼【変更点】グラフ描画実行 (棒グラフのロジックを移植・適応) ▼▼▼
    // =================================================================
    try {
      const configJson = document.getElementById('chart-json-data').textContent;
      if (!configJson || configJson.trim() === '') {
        throw new Error("JSON data is empty.");
      }
      
      let fullConfig;
      try {
        fullConfig = JSON.parse(configJson);
      } catch (parseError) {
        throw new Error('JSON parsing error: ' + parseError.message);
      }
      
      if (!fullConfig || typeof fullConfig !== 'object' || !fullConfig.data) {
        throw new Error("Invalid JSON structure: Missing 'data' key.");
      }
      const config = fullConfig.data;
      
      // ▼▼▼ "colors" が文字列だった場合の処理を分岐 ▼▼▼
      if (config.colors && typeof config.colors === 'string') {
        
        // ★ ドーナツグラフ用に変更: config.items から ID リストと色数を取得
        if (!config.items || !Array.isArray(config.items)) {
            throw new Error("Invalid JSON: 'items' array is required to generate colors.");
        }
        const colorIds = config.items.map(item => item.id);
        const numColors = colorIds.length;

        if (numColors === 0) {
             throw new Error("Invalid JSON: 'items' array is empty.");
        }

        let generatedColors; // ここで宣言
        const colorKeyword = config.colors.toLowerCase();
        
        // ★ 棒グラフのロジックから流用
        const geminiGradientDarken = 15; // Gemini用の濃淡差

        // ▼▼▼【分岐1】#gemini の場合 ▼▼▼
        if (colorKeyword === '#gemini') {
          // 濃いピンク -> 紫 -> 青
          const GEMINI_STOPS = ['#E68A9C', '#9F63D0', '#616AD8'];
          const hexPalette = generateGradientScale(GEMINI_STOPS, numColors);
          generatedColors = [];
          
          for (let i = 0; i < numColors; i++) {
            const hex_start = hexPalette[i];
            const rgb_start = hexToRgb(hex_start);
            const [h, s, l] = rgbToHsl(rgb_start.r, rgb_start.g, rgb_start.b);
            const l_end = Math.max(0, Math.min(100, l - geminiGradientDarken));
            const rgb_end = hslToRgb(h, s, l_end);
            const hex_end = rgbToHex(rgb_end[0], rgb_end[1], rgb_end[2]);
            
            generatedColors.push({
              id: colorIds[i], // ★ ドーナGグラフのID ('A', 'B'...) を使用
              start: hex_start,
              end: hex_end
            });
          }
        
        // ▼▼▼【分岐2】#night の場合 ▼▼▼
        } else if (colorKeyword === '#night') {
          // ユーザー指定の固定カラーパレット
          const NIGHT_COLORS = [
            { start: '#8d58d3', end: '#704ca4' }, // 項目 A (紫)
            { start: '#8f83d6', end: '#7469a8' }, // 項目 B (青紫)
            { start: '#7891dd', end: '#5c6fbb' }, // 項目 C (青)
            { start: '#50a89d', end: '#258a7f' }, // 項目 D (青緑)
            { start: '#54c5d5', end: '#34a0b0' }  // 項目 E (水色)
          ];
          generatedColors = [];
          
          for (let i = 0; i < numColors; i++) {
            // 該当する色を取得 (もし定義数を超えたら最後のを繰り返す)
            const colorPair = NIGHT_COLORS[i] || NIGHT_COLORS[NIGHT_COLORS.length - 1];
            
            generatedColors.push({
              id: colorIds[i], // ★ ドーナツグラフのID ('A', 'B'...) を使用
              start: colorPair.start,
              end: colorPair.end
            });
          }

        } else {
          // ▼▼▼【分岐3】HEXコード（単色・交互濃淡）の場合 ▼▼▼
          const primaryColor = config.colors;
          // ★ ドーナツグラフのIDリストを渡す
          generatedColors = generateMonochromaticScale(primaryColor, colorIds);
          
          if (generatedColors.length === 0) {
              throw new Error("Invalid HEX color string in 'colors': " + primaryColor);
          }
        }
        
        // 最終的な色設定を config に上書き
        if (generatedColors && generatedColors.length > 0) {
          config.colors = generatedColors;
        } else {
          // このルートは通常通らないはずだが、念のため
            throw new Error("Failed to generate colors from keyword: " + config.colors);
        }
        
      } else if (!config.colors || !Array.isArray(config.colors)) {
          // "colors" が配列でも文字列でもない場合はエラー
          throw new Error("'colors' must be an array of color objects or a single primary color string.");
      }
      // ▲▲▲ 色指定の分岐ここまで ▲▲▲
      
      if (!config || typeof config !== 'object' || !config.items || !Array.isArray(config.items)) {
        throw new Error("Invalid JSON structure: Missing required fields like 'items' inside 'data'.");
      }
      buildChart(config);

    } catch (e) {
      console.error("Chart Error:", e);
      const svgRoot = document.getElementById('donut-chart-svg');
      const textEl = createSVGElement('text', { x: 300, y: 230, 'text-anchor': 'middle', fill: 'red' });
      textEl.textContent = e.message.includes('JSON') ? 'JSONデータにエラーがあります。' : 'グラフ描画エラーが発生しました。';
      if (e.message.includes('JSON parsing error')) {
        textEl.textContent += ' (' + e.message.split(': ')[1] + ')';
      }
      svgRoot.appendChild(textEl);
    }
    // =================================================================
    // ▲▲▲【変更ここまで】▲▲▲
    // =================================================================
    
    // ]]>
  </script>
</svg>
  `;











function mt_getHistoryList() {
  const props = PropertiesService.getUserProperties();
  const json = props.getProperty('MT_HISTORY_LOG'); 
  if (json) {
    return JSON.parse(json);
  }
  return [];
}


function mt_createSlideFromImages(imagesData, fileName, pdfWidth, pdfHeight, isBackgroundMode) {
  
  const ID_16_9 = '18bU3NhEPujxFv2yLDGW5yk_ygCy25KxKxa6PK0ONGMM';
  const ID_A4_LANDSCAPE = '1I-74QmjDrDpCG29JXOX9II7OWHGJDQ60h4Ro15aBFLU';
  const ID_A4_PORTRAIT = '1aAQvzeaj6vOvcXpQ3Q6GGsBwZj_wMTWy9Afi_co_d2c';

  try {
    const ratio = pdfWidth / pdfHeight;
    let selectedTemplateId = "";
    let isStretchMode = false;

    
    if (pdfWidth > pdfHeight) {
      if (Math.abs(ratio - 1.778) < 0.15) {
        selectedTemplateId = ID_16_9;
        isStretchMode = true; 
      } else {
        selectedTemplateId = ID_A4_LANDSCAPE;
        isStretchMode = false;
      }
    } else {
      selectedTemplateId = ID_A4_PORTRAIT;
      isStretchMode = false;
    }

    
    const templateFile = DriveApp.getFileById(selectedTemplateId);
    const newFile = templateFile.makeCopy(fileName || "Converted Presentation", DriveApp.getRootFolder());
    
    const deck = SlidesApp.openById(newFile.getId());
    const originalSlides = deck.getSlides();
    const layouts = deck.getLayouts();
    
    
    const START_LAYOUT_INDEX = 11; 

    
    imagesData.forEach((base64Str, index) => {
      const contentType = base64Str.match(/data:(.*);base64/)[1];
      const data = base64Str.replace(/^data:image\/.*;base64,/, "");
      const decodedBlob = Utilities.newBlob(Utilities.base64Decode(data), contentType, "image" + index + ".jpg");

      let targetPage;

      if (isBackgroundMode) {
        
        const layoutIndex = (START_LAYOUT_INDEX + index) % layouts.length;
        const targetLayout = layouts[layoutIndex];
        const slide = deck.appendSlide(targetLayout);
        slide.getPageElements().forEach(e => e.remove());
        targetPage = targetLayout; 

      } else {
        
        const slide = deck.appendSlide(SlidesApp.PredefinedLayout.BLANK);
        slide.getPageElements().forEach(e => e.remove());
        targetPage = slide;
      }

      
      const image = targetPage.insertImage(decodedBlob);
      
      if (isBackgroundMode) {
        image.sendToBack();
      }

      
      if (isStretchMode) {
        image.setLeft(0);
        image.setTop(0);
        image.setWidth(deck.getPageWidth());
        image.setHeight(deck.getPageHeight());
      } else {
        mt_alignImageToSlide(deck, image);
      }
    });

    
    originalSlides.forEach(slide => slide.remove());

    const fileUrl = deck.getUrl();
    
    
    mt_updateHistory(newFile.getName(), fileUrl);

    return fileUrl;

  } catch (e) {
    throw new Error("スライド作成エラー: " + e.message);
  }
}


function mt_updateHistory(name, url) {
  const props = PropertiesService.getUserProperties();
  let history = [];
  const json = props.getProperty('MT_HISTORY_LOG'); 
  
  if (json) {
    try {
      history = JSON.parse(json);
    } catch (e) {
      history = [];
    }
  }

  const newItem = {
    name: name,
    url: url,
    timestamp: new Date().toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' })
  };
  history.unshift(newItem);

  if (history.length > 10) {
    history = history.slice(0, 10);
  }

  props.setProperty('MT_HISTORY_LOG', JSON.stringify(history));
}

function mt_alignImageToSlide(presentation, image) {
  const slideWidth = presentation.getPageWidth();
  const slideHeight = presentation.getPageHeight();
  const imgWidth = image.getWidth();
  const imgHeight = image.getHeight();
  const ratio = Math.min(slideWidth / imgWidth, slideHeight / imgHeight);
  image.setWidth(imgWidth * ratio);
  image.setHeight(imgHeight * ratio);
  image.setLeft((slideWidth - (imgWidth * ratio)) / 2);
  image.setTop((slideHeight - (imgHeight * ratio)) / 2);
}

/* ===== Ooxml.gs ===== */


var XML_DECL = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
var NS_A = 'http://schemas.openxmlformats.org/drawingml/2006/main';
var NS_R = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
var NS_P = 'http://schemas.openxmlformats.org/presentationml/2006/main';
var REL = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';

function ooxmlContentTypes() {
  return XML_DECL +
    '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
    '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
    '<Default Extension="xml" ContentType="application/xml"/>' +
    '<Default Extension="png" ContentType="image/png"/>' +
    '<Default Extension="jpeg" ContentType="image/jpeg"/>' +
    '<Default Extension="jpg" ContentType="image/jpeg"/>' +
    '<Default Extension="gif" ContentType="image/gif"/>' +
    '<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>' +
    '<Override PartName="/ppt/presProps.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"/>' +
    '<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>' +
    '<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>' +
    '<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>' +
    '<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>' +
    '</Types>';
}

function ooxmlRootRels() {
  return XML_DECL +
    '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
    '<Relationship Id="rId1" Type="' + REL + '/officeDocument" Target="ppt/presentation.xml"/>' +
    '</Relationships>';
}

function ooxmlPresentation(slideW, slideH) {
  return XML_DECL +
    '<p:presentation xmlns:a="' + NS_A + '" xmlns:r="' + NS_R + '" xmlns:p="' + NS_P + '" saveSubsetFonts="1">' +
    '<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>' +
    '<p:sldIdLst><p:sldId id="256" r:id="rId2"/></p:sldIdLst>' +
    '<p:sldSz cx="' + slideW + '" cy="' + slideH + '" type="screen16x9"/>' +
    '<p:notesSz cx="6858000" cy="9144000"/>' +
    '</p:presentation>';
}

function ooxmlPresentationRels() {
  return XML_DECL +
    '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
    '<Relationship Id="rId1" Type="' + REL + '/slideMaster" Target="slideMasters/slideMaster1.xml"/>' +
    '<Relationship Id="rId2" Type="' + REL + '/slide" Target="slides/slide1.xml"/>' +
    '<Relationship Id="rId3" Type="' + REL + '/presProps" Target="presProps.xml"/>' +
    '<Relationship Id="rId4" Type="' + REL + '/theme" Target="theme/theme1.xml"/>' +
    '</Relationships>';
}

function ooxmlPresProps() {
  return XML_DECL +
    '<p:presentationPr xmlns:a="' + NS_A + '" xmlns:r="' + NS_R + '" xmlns:p="' + NS_P + '"/>';
}

function ooxmlSlideMaster() {
  return XML_DECL +
    '<p:sldMaster xmlns:a="' + NS_A + '" xmlns:r="' + NS_R + '" xmlns:p="' + NS_P + '">' +
    '<p:cSld>' +
    '<p:bg><p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef></p:bg>' +
    '<p:spTree>' +
    '<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
    '<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>' +
    '</p:spTree>' +
    '</p:cSld>' +
    '<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>' +
    '<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>' +
    '</p:sldMaster>';
}

function ooxmlSlideMasterRels() {
  return XML_DECL +
    '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
    '<Relationship Id="rId1" Type="' + REL + '/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>' +
    '<Relationship Id="rId2" Type="' + REL + '/theme" Target="../theme/theme1.xml"/>' +
    '</Relationships>';
}

function ooxmlSlideLayout() {
  return XML_DECL +
    '<p:sldLayout xmlns:a="' + NS_A + '" xmlns:r="' + NS_R + '" xmlns:p="' + NS_P + '" type="blank" preserve="1">' +
    '<p:cSld name="Blank">' +
    '<p:spTree>' +
    '<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
    '<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>' +
    '</p:spTree>' +
    '</p:cSld>' +
    '<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>' +
    '</p:sldLayout>';
}

function ooxmlSlideLayoutRels() {
  return XML_DECL +
    '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
    '<Relationship Id="rId1" Type="' + REL + '/slideMaster" Target="../slideMasters/slideMaster1.xml"/>' +
    '</Relationships>';
}

function ooxmlSlideRels() {
  return XML_DECL +
    '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
    '<Relationship Id="rId1" Type="' + REL + '/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>' +
    '</Relationships>';
}

function ooxmlTheme() {
  return XML_DECL +
    '<a:theme xmlns:a="' + NS_A + '" name="Office Theme">' +
    '<a:themeElements>' +
    '<a:clrScheme name="Office">' +
    '<a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>' +
    '<a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>' +
    '<a:dk2><a:srgbClr val="44546A"/></a:dk2>' +
    '<a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>' +
    '<a:accent1><a:srgbClr val="4472C4"/></a:accent1>' +
    '<a:accent2><a:srgbClr val="ED7D31"/></a:accent2>' +
    '<a:accent3><a:srgbClr val="A5A5A5"/></a:accent3>' +
    '<a:accent4><a:srgbClr val="FFC000"/></a:accent4>' +
    '<a:accent5><a:srgbClr val="5B9BD5"/></a:accent5>' +
    '<a:accent6><a:srgbClr val="70AD47"/></a:accent6>' +
    '<a:hlink><a:srgbClr val="0563C1"/></a:hlink>' +
    '<a:folHlink><a:srgbClr val="954F72"/></a:folHlink>' +
    '</a:clrScheme>' +
    '<a:fontScheme name="Office">' +
    '<a:majorFont><a:latin typeface="Calibri Light"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>' +
    '<a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>' +
    '</a:fontScheme>' +
    '<a:fmtScheme name="Office">' +
    '<a:fillStyleLst>' +
    '<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>' +
    '<a:gradFill rotWithShape="1"><a:gsLst>' +
    '<a:gs pos="0"><a:schemeClr val="phClr"><a:lumMod val="110000"/><a:satMod val="105000"/><a:tint val="67000"/></a:schemeClr></a:gs>' +
    '<a:gs pos="50000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="103000"/><a:tint val="73000"/></a:schemeClr></a:gs>' +
    '<a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="109000"/><a:tint val="81000"/></a:schemeClr></a:gs>' +
    '</a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill>' +
    '<a:gradFill rotWithShape="1"><a:gsLst>' +
    '<a:gs pos="0"><a:schemeClr val="phClr"><a:satMod val="103000"/><a:lumMod val="102000"/><a:tint val="94000"/></a:schemeClr></a:gs>' +
    '<a:gs pos="50000"><a:schemeClr val="phClr"><a:satMod val="110000"/><a:lumMod val="100000"/><a:shade val="100000"/></a:schemeClr></a:gs>' +
    '<a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="99000"/><a:satMod val="120000"/><a:shade val="78000"/></a:schemeClr></a:gs>' +
    '</a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill>' +
    '</a:fillStyleLst>' +
    '<a:lnStyleLst>' +
    '<a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>' +
    '<a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>' +
    '<a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>' +
    '</a:lnStyleLst>' +
    '<a:effectStyleLst>' +
    '<a:effectStyle><a:effectLst/></a:effectStyle>' +
    '<a:effectStyle><a:effectLst/></a:effectStyle>' +
    '<a:effectStyle><a:effectLst/></a:effectStyle>' +
    '</a:effectStyleLst>' +
    '<a:bgFillStyleLst>' +
    '<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>' +
    '<a:solidFill><a:schemeClr val="phClr"><a:tint val="95000"/><a:satMod val="170000"/></a:schemeClr></a:solidFill>' +
    '<a:gradFill rotWithShape="1"><a:gsLst>' +
    '<a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="93000"/><a:satMod val="150000"/><a:shade val="98000"/><a:lumMod val="102000"/></a:schemeClr></a:gs>' +
    '<a:gs pos="50000"><a:schemeClr val="phClr"><a:tint val="98000"/><a:satMod val="130000"/><a:shade val="90000"/><a:lumMod val="103000"/></a:schemeClr></a:gs>' +
    '<a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="63000"/><a:satMod val="120000"/></a:schemeClr></a:gs>' +
    '</a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill>' +
    '</a:bgFillStyleLst>' +
    '</a:fmtScheme>' +
    '</a:themeElements>' +
    '</a:theme>';
}
/* ===== SvgParser.gs ===== */



function svgpIsSpace(c) {
  return c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f';
}


function svgpDecode(str) {
  if (str.indexOf('&') === -1) return str;
  return str.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, function (m, body) {
    if (body.charAt(0) === '#') {
      var code;
      if (body.charAt(1) === 'x' || body.charAt(1) === 'X') {
        code = parseInt(body.slice(2), 16);
      } else {
        code = parseInt(body.slice(1), 10);
      }
      if (isNaN(code)) return m;
      try { return String.fromCodePoint(code); } catch (e) { return m; }
    }
    switch (body) {
      case 'amp': return '&';
      case 'lt': return '<';
      case 'gt': return '>';
      case 'quot': return '"';
      case 'apos': return "'";
      case 'nbsp': return ' ';
      default: return m; 
    }
  });
}


function svgpSkipBang(text, lt) {
  var i = lt + 2;
  var len = text.length;
  var depth = 0;
  while (i < len) {
    var c = text.charAt(i);
    if (c === '[') depth++;
    else if (c === ']') depth--;
    else if (c === '>' && depth <= 0) return i + 1;
    i++;
  }
  return len;
}


function svgpParseTag(text, lt) {
  var i = lt + 1;
  var len = text.length;
  var start = i;
  while (i < len && !svgpIsSpace(text.charAt(i)) && text.charAt(i) !== '>' && text.charAt(i) !== '/') i++;
  var name = text.slice(start, i);
  var attrs = {};
  var selfClose = false;

  while (i < len) {
    while (i < len && svgpIsSpace(text.charAt(i))) i++;
    if (i >= len) break;
    var c = text.charAt(i);
    if (c === '>') { i++; break; }
    if (c === '/') {
      selfClose = true;
      i++;
      while (i < len && svgpIsSpace(text.charAt(i))) i++;
      if (text.charAt(i) === '>') { i++; }
      break;
    }
    
    var anStart = i;
    while (i < len && !svgpIsSpace(text.charAt(i)) && text.charAt(i) !== '=' &&
           text.charAt(i) !== '>' && text.charAt(i) !== '/') i++;
    var aname = text.slice(anStart, i);
    while (i < len && svgpIsSpace(text.charAt(i))) i++;
    var avalue = '';
    if (text.charAt(i) === '=') {
      i++;
      while (i < len && svgpIsSpace(text.charAt(i))) i++;
      var q = text.charAt(i);
      if (q === '"' || q === "'") {
        i++;
        var vStart = i;
        while (i < len && text.charAt(i) !== q) i++;
        avalue = text.slice(vStart, i);
        i++; 
      } else {
        var vStart2 = i;
        while (i < len && !svgpIsSpace(text.charAt(i)) && text.charAt(i) !== '>' && text.charAt(i) !== '/') i++;
        avalue = text.slice(vStart2, i);
      }
    }
    if (aname) attrs[aname] = svgpDecode(avalue);
  }
  return { name: name, attrs: attrs, selfClose: selfClose, end: i };
}


function svgpCloseTag(stack, cname) {
  for (var k = stack.length - 1; k >= 1; k--) {
    if (stack[k].name === cname) {
      stack.length = k; 
      return;
    }
  }
  
}


function parseXml(text) {
  var pos = 0;
  var len = text.length;
  var root = { type: 'root', name: '#root', attrs: {}, children: [] };
  var stack = [root];

  function cur() { return stack[stack.length - 1]; }
  function addText(node, raw, decode) {
    if (raw.length === 0) return;
    node.children.push({ type: 'text', text: decode ? svgpDecode(raw) : raw });
  }

  while (pos < len) {
    var lt = text.indexOf('<', pos);
    if (lt === -1) { addText(cur(), text.slice(pos), true); break; }
    if (lt > pos) addText(cur(), text.slice(pos, lt), true);

    if (text.substr(lt, 4) === '<!--') {
      var ce = text.indexOf('-->', lt + 4);
      pos = ce === -1 ? len : ce + 3;
      continue;
    }
    if (text.substr(lt, 9) === '<![CDATA[') {
      var dend = text.indexOf(']]>', lt + 9);
      var cdata = text.slice(lt + 9, dend === -1 ? len : dend);
      addText(cur(), cdata, false);
      pos = dend === -1 ? len : dend + 3;
      continue;
    }
    var c1 = text.charAt(lt + 1);
    if (c1 === '!') { pos = svgpSkipBang(text, lt); continue; }
    if (c1 === '?') {
      var qe = text.indexOf('?>', lt + 2);
      pos = qe === -1 ? len : qe + 2;
      continue;
    }
    if (c1 === '/') {
      var gt = text.indexOf('>', lt);
      if (gt === -1) { pos = len; break; }
      svgpCloseTag(stack, text.slice(lt + 2, gt).trim());
      pos = gt + 1;
      continue;
    }
    
    var tag = svgpParseTag(text, lt);
    var node = { type: 'element', name: tag.name, attrs: tag.attrs, children: [] };
    cur().children.push(node);
    if (!tag.selfClose) stack.push(node);
    pos = tag.end;
    if (pos <= lt) pos = lt + 1; 
  }

  return root;
}




function localName(name) {
  if (!name) return '';
  var idx = name.indexOf(':');
  return idx === -1 ? name : name.slice(idx + 1);
}


function elementChildren(node) {
  var out = [];
  if (!node || !node.children) return out;
  for (var i = 0; i < node.children.length; i++) {
    if (node.children[i].type === 'element') out.push(node.children[i]);
  }
  return out;
}


function getAttr(node, name) {
  if (!node || !node.attrs) return null;
  if (Object.prototype.hasOwnProperty.call(node.attrs, name)) return node.attrs[name];
  
  for (var k in node.attrs) {
    if (Object.prototype.hasOwnProperty.call(node.attrs, k) && localName(k) === name) {
      return node.attrs[k];
    }
  }
  return null;
}


function documentElement(root) {
  var els = elementChildren(root);
  return els.length ? els[0] : null;
}


function textContent(node) {
  if (!node) return '';
  if (node.type === 'text') return node.text;
  var s = '';
  if (node.children) {
    for (var i = 0; i < node.children.length; i++) s += textContent(node.children[i]);
  }
  return s;
}
/* ===== SvgGeom.gs ===== */





function matIdentity() { return [1, 0, 0, 1, 0, 0]; }


function matMul(m1, m2) {
  var a1 = m1[0], b1 = m1[1], c1 = m1[2], d1 = m1[3], e1 = m1[4], f1 = m1[5];
  var a2 = m2[0], b2 = m2[1], c2 = m2[2], d2 = m2[3], e2 = m2[4], f2 = m2[5];
  return [
    a1 * a2 + c1 * b2,
    b1 * a2 + d1 * b2,
    a1 * c2 + c1 * d2,
    b1 * c2 + d1 * d2,
    a1 * e2 + c1 * f2 + e1,
    b1 * e2 + d1 * f2 + f1
  ];
}

function applyMatrix(m, x, y) {
  return { x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] };
}


function matIsAxisAligned(m) {
  var eps = 1e-6;
  return Math.abs(m[1]) < eps && Math.abs(m[2]) < eps;
}


function parseTransform(str) {
  var m = matIdentity();
  if (!str) return m;
  var re = /(matrix|translate|scale|rotate|skewX|skewY)\s*\(([^)]*)\)/g;
  var match;
  while ((match = re.exec(str))) {
    var name = match[1];
    var args = match[2].split(/[\s,]+/).filter(function (s) { return s.length; }).map(parseFloat);
    var t = matIdentity();
    switch (name) {
      case 'matrix':
        if (args.length >= 6) t = [args[0], args[1], args[2], args[3], args[4], args[5]];
        break;
      case 'translate':
        t = [1, 0, 0, 1, args[0] || 0, args.length > 1 ? args[1] : 0];
        break;
      case 'scale': {
        var sx = (args.length > 0) ? args[0] : 1;
        var sy = (args.length > 1) ? args[1] : sx;
        t = [sx, 0, 0, sy, 0, 0];
        break;
      }
      case 'rotate': {
        var ang = (args[0] || 0) * Math.PI / 180;
        var ca = Math.cos(ang), sa = Math.sin(ang);
        if (args.length >= 3) {
          var ox = args[1], oy = args[2];
          t = matMul([1, 0, 0, 1, ox, oy], matMul([ca, sa, -sa, ca, 0, 0], [1, 0, 0, 1, -ox, -oy]));
        } else {
          t = [ca, sa, -sa, ca, 0, 0];
        }
        break;
      }
      case 'skewX': { var ax = (args[0] || 0) * Math.PI / 180; t = [1, 0, Math.tan(ax), 1, 0, 0]; break; }
      case 'skewY': { var ay = (args[0] || 0) * Math.PI / 180; t = [1, Math.tan(ay), 0, 1, 0, 0]; break; }
    }
    m = matMul(m, t);
  }
  return m;
}



function _ellipsePt(cx, cy, rx, ry, cosPhi, sinPhi, ang) {
  var x = rx * Math.cos(ang), y = ry * Math.sin(ang);
  return { x: cx + cosPhi * x - sinPhi * y, y: cy + sinPhi * x + cosPhi * y };
}
function _ellipseDeriv(rx, ry, cosPhi, sinPhi, ang) {
  var dx = -rx * Math.sin(ang), dy = ry * Math.cos(ang);
  return { x: cosPhi * dx - sinPhi * dy, y: sinPhi * dx + cosPhi * dy };
}


function arcToCubic(x0, y0, rx, ry, phiDeg, laf, sf, x, y) {
  var out = [];
  if (rx === 0 || ry === 0 || (x0 === x && y0 === y)) {
    out.push({ cmd: 'L', x: x, y: y });
    return out;
  }
  rx = Math.abs(rx); ry = Math.abs(ry);
  var phi = (phiDeg % 360) * Math.PI / 180;
  var cosPhi = Math.cos(phi), sinPhi = Math.sin(phi);

  var dx = (x0 - x) / 2, dy = (y0 - y) / 2;
  var x1p = cosPhi * dx + sinPhi * dy;
  var y1p = -sinPhi * dx + cosPhi * dy;

  var lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
  if (lambda > 1) { var s = Math.sqrt(lambda); rx *= s; ry *= s; }

  var sign = (laf !== sf) ? 1 : -1;
  var num = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
  var den = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
  var co = sign * Math.sqrt(Math.max(0, num / den));
  var cxp = co * (rx * y1p) / ry;
  var cyp = co * (-ry * x1p) / rx;

  var cx = cosPhi * cxp - sinPhi * cyp + (x0 + x) / 2;
  var cy = sinPhi * cxp + cosPhi * cyp + (y0 + y) / 2;

  function angle(ux, uy, vx, vy) {
    var dot = ux * vx + uy * vy;
    var lenp = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
    var a = Math.acos(Math.max(-1, Math.min(1, dot / lenp)));
    if (ux * vy - uy * vx < 0) a = -a;
    return a;
  }
  var ux = (x1p - cxp) / rx, uy = (y1p - cyp) / ry;
  var vx = (-x1p - cxp) / rx, vy = (-y1p - cyp) / ry;
  var theta1 = angle(1, 0, ux, uy);
  var dtheta = angle(ux, uy, vx, vy);
  if (!sf && dtheta > 0) dtheta -= 2 * Math.PI;
  if (sf && dtheta < 0) dtheta += 2 * Math.PI;

  var segs = Math.ceil(Math.abs(dtheta) / (Math.PI / 2 + 1e-9));
  if (segs === 0) segs = 1;
  var delta = dtheta / segs;
  var tval = (4 / 3) * Math.tan(delta / 4);

  for (var idx = 0; idx < segs; idx++) {
    var a1 = theta1 + idx * delta;
    var a2 = theta1 + (idx + 1) * delta;
    var p1 = _ellipsePt(cx, cy, rx, ry, cosPhi, sinPhi, a1);
    var p2 = _ellipsePt(cx, cy, rx, ry, cosPhi, sinPhi, a2);
    var d1 = _ellipseDeriv(rx, ry, cosPhi, sinPhi, a1);
    var d2 = _ellipseDeriv(rx, ry, cosPhi, sinPhi, a2);
    out.push({
      cmd: 'C',
      x1: p1.x + tval * d1.x, y1: p1.y + tval * d1.y,
      x2: p2.x - tval * d2.x, y2: p2.y - tval * d2.y,
      x: p2.x, y: p2.y
    });
  }
  return out;
}




function parsePathData(d) {
  if (!d) return [];
  var i = 0, len = d.length;
  var cmds = [];
  var cx = 0, cy = 0, sx = 0, sy = 0;
  var lastCubic = null;  
  var lastQuad = null;   
  var prevU = '';        
  var prevRaw = '';      

  function skipSep() {
    while (i < len) {
      var ch = d.charAt(i);
      if (ch === ' ' || ch === ',' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\f') i++;
      else break;
    }
  }
  function readNumber() {
    skipSep();
    var start = i;
    if (d.charAt(i) === '+' || d.charAt(i) === '-') i++;
    while (i < len && d.charAt(i) >= '0' && d.charAt(i) <= '9') i++;
    if (d.charAt(i) === '.') { i++; while (i < len && d.charAt(i) >= '0' && d.charAt(i) <= '9') i++; }
    if (d.charAt(i) === 'e' || d.charAt(i) === 'E') {
      i++;
      if (d.charAt(i) === '+' || d.charAt(i) === '-') i++;
      while (i < len && d.charAt(i) >= '0' && d.charAt(i) <= '9') i++;
    }
    if (i === start) { i++; return NaN; }       
    return parseFloat(d.slice(start, i));
  }
  function readFlag() {
    skipSep();
    var ch = d.charAt(i);
    i++;
    return ch === '1' ? 1 : 0;
  }
  function isCmd(ch) { return 'MmLlHhVvCcSsQqTtAaZz'.indexOf(ch) !== -1; }

  skipSep();
  var guard = 0;
  while (i < len && guard++ < 100000) {
    var ch = d.charAt(i);
    var cmd;
    if (isCmd(ch)) { cmd = ch; i++; }
    else if (prevRaw) { cmd = (prevRaw === 'M') ? 'L' : (prevRaw === 'm') ? 'l' : prevRaw; }
    else { break; }

    var rel = (cmd >= 'a' && cmd <= 'z');
    var C = cmd.toUpperCase();
    var x, y, x1, y1, x2, y2;

    if (C === 'M') {
      x = readNumber(); y = readNumber();
      if (rel) { x += cx; y += cy; }
      if (isNaN(x) || isNaN(y)) break;
      cx = x; cy = y; sx = x; sy = y;
      cmds.push({ cmd: 'M', x: x, y: y });
      lastCubic = lastQuad = null;
    } else if (C === 'L') {
      x = readNumber(); y = readNumber();
      if (rel) { x += cx; y += cy; }
      if (isNaN(x) || isNaN(y)) break;
      cx = x; cy = y; cmds.push({ cmd: 'L', x: x, y: y }); lastCubic = lastQuad = null;
    } else if (C === 'H') {
      x = readNumber(); if (rel) x += cx; if (isNaN(x)) break;
      cx = x; cmds.push({ cmd: 'L', x: x, y: cy }); lastCubic = lastQuad = null;
    } else if (C === 'V') {
      y = readNumber(); if (rel) y += cy; if (isNaN(y)) break;
      cy = y; cmds.push({ cmd: 'L', x: cx, y: y }); lastCubic = lastQuad = null;
    } else if (C === 'C') {
      x1 = readNumber(); y1 = readNumber(); x2 = readNumber(); y2 = readNumber(); x = readNumber(); y = readNumber();
      if (rel) { x1 += cx; y1 += cy; x2 += cx; y2 += cy; x += cx; y += cy; }
      if (isNaN(y)) break;
      cmds.push({ cmd: 'C', x1: x1, y1: y1, x2: x2, y2: y2, x: x, y: y });
      lastCubic = { x: x2, y: y2 }; lastQuad = null; cx = x; cy = y;
    } else if (C === 'S') {
      x2 = readNumber(); y2 = readNumber(); x = readNumber(); y = readNumber();
      if (rel) { x2 += cx; y2 += cy; x += cx; y += cy; }
      if (isNaN(y)) break;
      if (lastCubic && (prevU === 'C' || prevU === 'S')) { x1 = 2 * cx - lastCubic.x; y1 = 2 * cy - lastCubic.y; }
      else { x1 = cx; y1 = cy; }
      cmds.push({ cmd: 'C', x1: x1, y1: y1, x2: x2, y2: y2, x: x, y: y });
      lastCubic = { x: x2, y: y2 }; lastQuad = null; cx = x; cy = y;
    } else if (C === 'Q') {
      var qx = readNumber(), qy = readNumber(); x = readNumber(); y = readNumber();
      if (rel) { qx += cx; qy += cy; x += cx; y += cy; }
      if (isNaN(y)) break;
      cmds.push(_quadToCubic(cx, cy, qx, qy, x, y));
      lastQuad = { x: qx, y: qy }; lastCubic = null; cx = x; cy = y;
    } else if (C === 'T') {
      x = readNumber(); y = readNumber();
      if (rel) { x += cx; y += cy; }
      if (isNaN(y)) break;
      var tqx, tqy;
      if (lastQuad && (prevU === 'Q' || prevU === 'T')) { tqx = 2 * cx - lastQuad.x; tqy = 2 * cy - lastQuad.y; }
      else { tqx = cx; tqy = cy; }
      cmds.push(_quadToCubic(cx, cy, tqx, tqy, x, y));
      lastQuad = { x: tqx, y: tqy }; lastCubic = null; cx = x; cy = y;
    } else if (C === 'A') {
      var rx = readNumber(), ry = readNumber(), rot = readNumber(), laf = readFlag(), sf = readFlag();
      x = readNumber(); y = readNumber();
      if (rel) { x += cx; y += cy; }
      if (isNaN(y)) break;
      var arcs = arcToCubic(cx, cy, rx, ry, rot, laf, sf, x, y);
      for (var a = 0; a < arcs.length; a++) cmds.push(arcs[a]);
      lastCubic = lastQuad = null; cx = x; cy = y;
    } else if (C === 'Z') {
      cmds.push({ cmd: 'Z' });
      cx = sx; cy = sy; lastCubic = lastQuad = null;
    } else {
      break;
    }
    prevU = C;
    prevRaw = cmd;
    skipSep();
  }
  return cmds;
}

function _quadToCubic(cx, cy, qx, qy, x, y) {
  return {
    cmd: 'C',
    x1: cx + (2 / 3) * (qx - cx), y1: cy + (2 / 3) * (qy - cy),
    x2: x + (2 / 3) * (qx - x), y2: y + (2 / 3) * (qy - y),
    x: x, y: y
  };
}
/* ===== SvgToPptx.gs ===== */


var EMU_PER_PT = 12700;
var DEFAULT_SLIDE_W = 12192000; 
var DEFAULT_SLIDE_H = 6858000;  


var SVG_COLOR_NAMES = {
  black: '000000', silver: 'C0C0C0', gray: '808080', grey: '808080', white: 'FFFFFF',
  maroon: '800000', red: 'FF0000', purple: '800080', fuchsia: 'FF00FF', magenta: 'FF00FF',
  green: '008000', lime: '00FF00', olive: '808000', yellow: 'FFFF00', navy: '000080',
  blue: '0000FF', teal: '008080', aqua: '00FFFF', cyan: '00FFFF', orange: 'FFA500',
  pink: 'FFC0CB', brown: 'A52A2A', gold: 'FFD700', indigo: '4B0082', violet: 'EE82EE',
  darkgray: 'A9A9A9', darkgrey: 'A9A9A9', lightgray: 'D3D3D3', lightgrey: 'D3D3D3',
  lightblue: 'ADD8E6', darkblue: '00008B', lightgreen: '90EE90', darkgreen: '006400',
  darkred: '8B0000', skyblue: '87CEEB', steelblue: '4682B4', tomato: 'FF6347',
  crimson: 'DC143C', coral: 'FF7F50', salmon: 'FA8072', khaki: 'F0E68C',
  turquoise: '40E0D0', tan: 'D2B48C', beige: 'F5F5DC', ivory: 'FFFFF0',
  lavender: 'E6E6FA', plum: 'DDA0DD', orchid: 'DA70D6', slategray: '708090',
  dimgray: '696969', whitesmoke: 'F5F5F5', gainsboro: 'DCDCDC', transparent: null
};


function parseColor(str, currentColor) {
  if (str == null) return null;
  str = String(str).trim();
  if (str === '' ) return null;
  var low = str.toLowerCase();
  if (low === 'none') return { type: 'none' };
  if (low === 'transparent') return { type: 'none' };
  if (low === 'currentcolor') return parseColor(currentColor || '#000000', '#000000');
  if (low.charAt(0) === '#') {
    var h = low.slice(1);
    if (h.length === 3) h = h.charAt(0) + h.charAt(0) + h.charAt(1) + h.charAt(1) + h.charAt(2) + h.charAt(2);
    if (h.length === 4) { 
      var a4 = parseInt(h.charAt(3) + h.charAt(3), 16) / 255;
      h = h.charAt(0) + h.charAt(0) + h.charAt(1) + h.charAt(1) + h.charAt(2) + h.charAt(2);
      return { hex: h.toUpperCase(), alpha: a4 };
    }
    if (h.length === 8) { 
      var a8 = parseInt(h.slice(6, 8), 16) / 255;
      return { hex: h.slice(0, 6).toUpperCase(), alpha: a8 };
    }
    if (h.length === 6) return { hex: h.toUpperCase(), alpha: 1 };
    return { hex: '000000', alpha: 1 };
  }
  var m = low.match(/^rgba?\(([^)]+)\)$/);
  if (m) {
    var parts = m[1].split(',').map(function (s) { return s.trim(); });
    function ch(v) {
      if (v.indexOf('%') !== -1) return Math.round(parseFloat(v) * 255 / 100);
      return Math.round(parseFloat(v));
    }
    var r = Math.max(0, Math.min(255, ch(parts[0] || '0')));
    var g = Math.max(0, Math.min(255, ch(parts[1] || '0')));
    var b = Math.max(0, Math.min(255, ch(parts[2] || '0')));
    var a = parts.length > 3 ? Math.max(0, Math.min(1, parseFloat(parts[3]))) : 1;
    return { hex: toHex2(r) + toHex2(g) + toHex2(b), alpha: a };
  }
  if (low.indexOf('url(') === 0) return { hex: 'CCCCCC', alpha: 1, _isRef: true }; 
  if (Object.prototype.hasOwnProperty.call(SVG_COLOR_NAMES, low)) {
    var named = SVG_COLOR_NAMES[low];
    if (named === null) return { type: 'none' };
    return { hex: named, alpha: 1 };
  }
  return null; 
}
function toHex2(n) { var s = n.toString(16).toUpperCase(); return s.length < 2 ? '0' + s : s; }



var GRAD_REGISTRY = {};





var CSS_REGISTRY = [];

function collectCssRules(node, rules) {
  if (!node) return rules;
  if (node.type === 'element' && localName(node.name) === 'style') {
    try { _parseCssText(textContent(node) || '', rules); } catch (e) {}
  }
  var kids = elementChildren(node);
  for (var i = 0; i < kids.length; i++) collectCssRules(kids[i], rules);
  return rules;
}

function _parseCssText(css, rules) {
  css = String(css).replace(/\/\*[\s\S]*?\*\//g, ''); 
  var n = css.length, i = 0;
  while (i < n) {
    
    var start = i;
    while (i < n) {
      var ch = css.charAt(i);
      if (ch === '"' || ch === "'") { var q = ch; i++; while (i < n && css.charAt(i) !== q) i++; i++; continue; }
      if (ch === '{' || ch === ';' || ch === '}') break;
      i++;
    }
    if (i >= n) break;
    var d = css.charAt(i);
    if (d === ';') { i++; continue; }   
    if (d === '}') { i++; continue; }   
    
    var selGroup = css.slice(start, i).trim();
    i++; 
    var depth = 1, bStart = i;
    while (i < n && depth > 0) {
      var cc = css.charAt(i);
      if (cc === '"' || cc === "'") { var q2 = cc; i++; while (i < n && css.charAt(i) !== q2) i++; i++; continue; }
      if (cc === '{') depth++;
      else if (cc === '}') { depth--; if (depth === 0) break; }
      i++;
    }
    var block = css.slice(bStart, i);
    i++; 
    if (!selGroup) continue;
    if (selGroup.charAt(0) === '@') {                 
      if (/^@media\b/i.test(selGroup)) _parseCssText(block, rules); 
      continue;                                       
    }
    var decls = parseStyleAttr(block);
    var hasDecl = false; for (var _k in decls) { hasDecl = true; break; }
    if (!hasDecl) continue;
    var sels = selGroup.split(',');
    for (var s = 0; s < sels.length; s++) {
      var sel = sels[s].trim();
      if (sel) rules.push({ sel: sel, decls: decls });
    }
  }
  return rules;
}

function _cssRuleMatches(sel, tag, classes, id) {
  if (sel === '*') return true;
  var m = /^([a-zA-Z][\w-]*)?((?:[.#][\w-]+)*)$/.exec(sel);
  if (!m) return false; 
  if (m[1] && m[1].toLowerCase() !== tag) return false;
  var tokens = (m[2] || '').match(/[.#][\w-]+/g) || [];
  for (var i = 0; i < tokens.length; i++) {
    var t = tokens[i];
    if (t.charAt(0) === '.') { if (classes.indexOf(t.slice(1)) < 0) return false; }
    else if (id !== t.slice(1)) return false;
  }
  return true;
}





var FILTER_REGISTRY = {};

function collectFilters(node, reg) {
  if (!node) return reg;
  if (node.type === 'element' && localName(node.name) === 'filter') {
    var id = getAttr(node, 'id');
    if (id) {
      var fds = _findFeDropShadow(node);
      if (fds) reg[id] = { dropShadow: fds };
    }
  }
  var kids = elementChildren(node);
  for (var i = 0; i < kids.length; i++) collectFilters(kids[i], reg);
  return reg;
}

function _findFeDropShadow(filterNode) {
  var kids = elementChildren(filterNode);
  for (var i = 0; i < kids.length; i++) {
    if (localName(kids[i].name) === 'feDropShadow') {
      var n = kids[i];
      var dx = parseFloat(getAttr(n, 'dx')); if (isNaN(dx)) dx = 2;
      var dy = parseFloat(getAttr(n, 'dy')); if (isNaN(dy)) dy = 2;
      var std = parseFloat(getAttr(n, 'stdDeviation')); if (isNaN(std)) std = 2;
      var col = getAttr(n, 'flood-color') || '#000000';
      var op = parseFloat(getAttr(n, 'flood-opacity')); if (isNaN(op)) op = 1;
      return { dx: dx, dy: dy, std: std, color: col, opacity: op };
    }
  }
  return null;
}

function shadowInnerXml(style, scale) {
  var raw = style && style['filter'];
  if (!raw || !/^\s*url\(/i.test(raw)) return '';
  var id = extractRefId(raw);
  var f = id ? FILTER_REGISTRY[id] : null;
  if (!f || !f.dropShadow) return '';
  var d = f.dropShadow;
  if (!(scale > 0)) scale = 1;
  var dist = Math.round(Math.sqrt(d.dx * d.dx + d.dy * d.dy) * scale);
  var blur = Math.max(0, Math.round(d.std * scale * 2)); 
  var dir = Math.round(((Math.atan2(d.dy, d.dx) * 180 / Math.PI) + 360) % 360 * 60000);
  var c = parseColor(d.color, null) || { hex: '000000' };
  var alpha = Math.max(0, Math.min(100000, Math.round((d.opacity == null ? 1 : d.opacity) * 100000)));
  return '<a:outerShdw blurRad="' + blur + '" dist="' + dist + '" dir="' + dir + '" rotWithShape="0">' +
    '<a:srgbClr val="' + c.hex + '"><a:alpha val="' + alpha + '"/></a:srgbClr></a:outerShdw>';
}


function extractRefId(v) {
  if (!v) return null;
  var m = /url\(\s*["']?#([^"')\s]+)["']?\s*\)/i.exec(String(v));
  return m ? m[1] : null;
}


function parseGradCoord(v, dflt) {
  if (v == null || v === '') return dflt;
  v = String(v).trim();
  if (v.charAt(v.length - 1) === '%') return parseFloat(v) / 100;
  return parseFloat(v);
}


function collectGradients(node, reg) {
  if (!node) return reg;
  if (node.type === 'element') {
    var ln = localName(node.name);
    if (ln === 'linearGradient' || ln === 'radialGradient') {
      var id = getAttr(node, 'id');
      if (id) reg[id] = parseGradientEl(node, ln);
    }
  }
  var kids = elementChildren(node);
  for (var i = 0; i < kids.length; i++) collectGradients(kids[i], reg);
  return reg;
}

function parseGradientEl(node, kind) {
  var stops = [];
  var kids = elementChildren(node);
  for (var i = 0; i < kids.length; i++) {
    if (localName(kids[i].name) !== 'stop') continue;
    var st = kids[i];
    var sStyle = parseStyleAttr(getAttr(st, 'style'));
    var col = sStyle['stop-color'] != null ? sStyle['stop-color'] : getAttr(st, 'stop-color');
    var opv = sStyle['stop-opacity'] != null ? sStyle['stop-opacity'] : getAttr(st, 'stop-opacity');
    var c = parseColor(col == null ? '#000000' : col, '#000000');
    if (!c || c.type === 'none') c = { hex: '000000', alpha: 1 };
    var so = (opv != null && opv !== '') ? parseFloat(opv) : 1;
    if (isNaN(so)) so = 1;
    stops.push({
      offset: Math.max(0, Math.min(1, parseGradCoord(getAttr(st, 'offset'), 0))),
      hex: c.hex,
      alpha: (c.alpha == null ? 1 : c.alpha) * so
    });
  }
  return {
    type: kind === 'radialGradient' ? 'radial' : 'linear',
    x1: parseGradCoord(getAttr(node, 'x1'), 0),
    y1: parseGradCoord(getAttr(node, 'y1'), 0),
    x2: parseGradCoord(getAttr(node, 'x2'), 1), 
    y2: parseGradCoord(getAttr(node, 'y2'), 0),
    stops: stops
  };
}


function gradAngle(g) {
  var dx = g.x2 - g.x1, dy = g.y2 - g.y1;
  if (dx === 0 && dy === 0) return 5400000; 
  var deg = Math.atan2(dy, dx) * 180 / Math.PI;
  if (deg < 0) deg += 360;
  return Math.round(deg * 60000) % 21600000;
}


function gradFillXml(g, opacityMul) {
  if (opacityMul == null) opacityMul = 1;
  var stops = g.stops.slice();
  if (stops.length === 0) stops = [{ offset: 0, hex: '000000', alpha: 1 }];
  if (stops.length === 1) stops = [
    { offset: 0, hex: stops[0].hex, alpha: stops[0].alpha },
    { offset: 1, hex: stops[0].hex, alpha: stops[0].alpha }
  ];
  stops.sort(function (a, b) { return a.offset - b.offset; });
  var gsLst = '';
  for (var i = 0; i < stops.length; i++) {
    var pos = Math.max(0, Math.min(100000, Math.round(stops[i].offset * 100000)));
    gsLst += '<a:gs pos="' + pos + '">' + srgbXml({ hex: stops[i].hex, alpha: stops[i].alpha * opacityMul }) + '</a:gs>';
  }
  if (g.type === 'radial') {
    
    
    
    return '<a:gradFill><a:gsLst>' + gsLst + '</a:gsLst>' +
      '<a:path path="circle"><a:fillToRect l="50%" t="50%" r="50%" b="50%"/></a:path><a:tileRect/></a:gradFill>';
  }
  return '<a:gradFill flip="none" rotWithShape="1"><a:gsLst>' + gsLst + '</a:gsLst>' +
    '<a:lin ang="' + gradAngle(g) + '" scaled="1"/></a:gradFill>';
}



function blurPxOf(node) {
  var f = getAttr(node, 'filter');
  var st = parseStyleAttr(getAttr(node, 'style'));
  if (st['filter'] != null) f = st['filter'];
  if (!f) return 0;
  var m = /blur\(\s*([\d.]+)\s*px?\s*\)/i.exec(f);
  return m ? parseFloat(m[1]) : 0;
}


function effectXmlFor(node, vp, style, scale) {
  var inner = '';
  var bpx = blurPxOf(node);
  if (bpx > 0) inner += '<a:blur rad="' + Math.max(0, Math.round(bpx * vp.S)) + '"/>';
  if (style) inner += shadowInnerXml(style, scale);
  if (!inner) return '';
  return '<a:effectLst>' + inner + '</a:effectLst>';
}


function resolveFillColor(style) {
  var raw = style['fill'];
  if (raw && /^\s*url\(/i.test(raw)) return null;
  var c = parseColor(raw == null ? '#000000' : raw, style['color']);
  if (!c || c.type === 'none') return null;
  var opMul = styleNumber(style, 'fill-opacity', 1) * (style._op == null ? 1 : style._op);
  return { hex: c.hex, alpha: (c.alpha == null ? 1 : c.alpha) * opMul };
}


function _erf(x) {
  var s = x < 0 ? -1 : 1; x = Math.abs(x);
  var t = 1 / (1 + 0.3275911 * x);
  var y = 1 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * Math.exp(-x * x);
  return s * y;
}
function _erfc(x) { return 1 - _erf(x); }


function _gaussProfile(t, sigma, baseAlpha) {
  return baseAlpha * 0.5 * _erfc(t / (sigma * Math.SQRT2));
}


function emitGlowRadial(ctx, x, y, cx, cy, hex, baseAlpha) {
  baseAlpha = Math.max(0.02, Math.min(1, baseAlpha || 0.3));
  var grad = gradFillXml({
    type: 'radial',
    stops: [
      { offset: 0,    hex: hex, alpha: baseAlpha },
      { offset: 0.80, hex: hex, alpha: baseAlpha },
      { offset: 1,    hex: hex, alpha: 0 }
    ]
  }, 1);
  return spShape(ctx.id++, 'glow', xfrmXml(x, y, cx, cy),
    '<a:prstGeom prst="ellipse"><a:avLst/></a:prstGeom>', grad, '', '');
}


var GLOW_ROUT = 1.0; 
var GLOW_RIN = 2.0;  
var GLOW_N = 18;     


function emitGlowGroup(ctx, isEllipse, centerX, centerY, W0, H0, sigma, hex, baseAlpha) {
  baseAlpha = Math.max(0.02, Math.min(1, baseAlpha || 0.3));
  if (!(sigma > 0)) sigma = 1;
  var N = GLOW_N, Rout = sigma * GLOW_ROUT, Rin = sigma * GLOW_RIN;
  var prst = isEllipse ? 'ellipse' : 'roundRect';
  var av = isEllipse ? '<a:avLst/>' : '<a:avLst><a:gd name="adj" fmla="val 25000"/></a:avLst>';
  var children = '', prevCum = 0;
  for (var i = 0; i < N; i++) {
    var t = Rout - i * (Rout + Rin) / (N - 1);     
    
    var cum = (i === N - 1) ? baseAlpha : _gaussProfile(t, sigma, baseAlpha);
    var ai = (i === 0) ? cum : (1 - (1 - cum) / (1 - prevCum));
    prevCum = cum;
    ai = Math.max(0, Math.min(1, ai));
    if (ai < 0.0005) continue;                      
    var Wi = W0 + 2 * t, Hi = H0 + 2 * t;
    if (Wi <= 0 || Hi <= 0) continue;
    var ox = centerX - Wi / 2, oy = centerY - Hi / 2;
    children += '<p:sp><p:nvSpPr><p:cNvPr id="' + (ctx.id++) + '" name="glow"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>' +
      '<p:spPr>' + xfrmXml(ox, oy, Wi, Hi) +
      '<a:prstGeom prst="' + prst + '">' + av + '</a:prstGeom>' +
      '<a:solidFill>' + srgbXml({ hex: hex, alpha: ai }) + '</a:solidFill></p:spPr></p:sp>';
  }
  var outerW = Math.max(1, Math.round(W0 + 2 * Rout)), outerH = Math.max(1, Math.round(H0 + 2 * Rout));
  var gx = Math.round(centerX - outerW / 2), gy = Math.round(centerY - outerH / 2);
  var gid = ctx.id++;
  return '<p:grpSp><p:nvGrpSpPr><p:cNvPr id="' + gid + '" name="glow"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
    '<p:grpSpPr><a:xfrm><a:off x="' + gx + '" y="' + gy + '"/><a:ext cx="' + outerW + '" cy="' + outerH + '"/>' +
    '<a:chOff x="' + gx + '" y="' + gy + '"/><a:chExt cx="' + outerW + '" cy="' + outerH + '"/></a:xfrm></p:grpSpPr>' +
    children + '</p:grpSp>';
}


function emitGlowPath(ctx, emuCmds, hex, baseAlpha, sigma) {
  baseAlpha = Math.max(0.02, Math.min(1, baseAlpha || 0.3));
  if (!(sigma > 0)) sigma = 1;

  var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
  for (var ci = 0; ci < emuCmds.length; ci++) {
    var cc = emuCmds[ci];
    if (cc.cmd === 'M' || cc.cmd === 'L') { if (cc.x < minX) minX = cc.x; if (cc.y < minY) minY = cc.y; if (cc.x > maxX) maxX = cc.x; if (cc.y > maxY) maxY = cc.y; }
    else if (cc.cmd === 'C') { var pts = [[cc.x1,cc.y1],[cc.x2,cc.y2],[cc.x,cc.y]]; for (var pi=0;pi<pts.length;pi++){if(pts[pi][0]<minX)minX=pts[pi][0];if(pts[pi][1]<minY)minY=pts[pi][1];if(pts[pi][0]>maxX)maxX=pts[pi][0];if(pts[pi][1]>maxY)maxY=pts[pi][1];} }
  }
  if (!isFinite(minX)) return '';
  var cx = (minX + maxX) / 2, cy = (minY + maxY) / 2;
  var W = Math.max(1, maxX - minX), H = Math.max(1, maxY - minY);
  var shortEdge = Math.min(W, H);

  function scaleCmd(cmd, s) {
    if (cmd.cmd === 'M' || cmd.cmd === 'L') return { cmd: cmd.cmd, x: cx + (cmd.x - cx) * s, y: cy + (cmd.y - cy) * s };
    if (cmd.cmd === 'C') return { cmd: 'C', x1: cx+(cmd.x1-cx)*s, y1: cy+(cmd.y1-cy)*s, x2: cx+(cmd.x2-cx)*s, y2: cy+(cmd.y2-cy)*s, x: cx+(cmd.x-cx)*s, y: cy+(cmd.y-cy)*s };
    return cmd; 
  }

  var N = GLOW_N, Rout = sigma * GLOW_ROUT, Rin = sigma * GLOW_RIN;
  var children = '', prevCum = 0, sMax = 1;
  for (var i = 0; i < N; i++) {
    var t = Rout - i * (Rout + Rin) / (N - 1);              
    var s = 1 + 2 * t / shortEdge;                          
    if (s <= 0) continue;
    if (i === 0) sMax = s;
    var cum = (i === N - 1) ? baseAlpha : _gaussProfile(t, sigma, baseAlpha);
    var ai = (i === 0) ? cum : (1 - (1 - cum) / (1 - prevCum));
    prevCum = cum;
    ai = Math.max(0, Math.min(1, ai));
    if (ai < 0.0005) continue;
    var scaled = [];
    for (var j = 0; j < emuCmds.length; j++) scaled.push(scaleCmd(emuCmds[j], s));
    var cg = buildCustGeom(scaled);
    children += spShape(ctx.id++, 'glow',
      xfrmXml(cg.xfrm.x, cg.xfrm.y, cg.xfrm.cx, cg.xfrm.cy),
      cg.geom,
      '<a:solidFill>' + srgbXml({ hex: hex, alpha: ai }) + '</a:solidFill>',
      '', '');
  }
  var outerW = Math.max(1, Math.round(W * sMax)), outerH = Math.max(1, Math.round(H * sMax));
  var ox = Math.round(cx - outerW / 2), oy = Math.round(cy - outerH / 2);
  var gid = ctx.id++;
  return '<p:grpSp><p:nvGrpSpPr><p:cNvPr id="' + gid + '" name="glow"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
    '<p:grpSpPr><a:xfrm><a:off x="' + ox + '" y="' + oy + '"/><a:ext cx="' + outerW + '" cy="' + outerH + '"/>' +
    '<a:chOff x="' + ox + '" y="' + oy + '"/><a:chExt cx="' + outerW + '" cy="' + outerH + '"/></a:xfrm></p:grpSpPr>' +
    children + '</p:grpSp>';
}


function xmlEscape(s) {
  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;').replace(/'/g, '&apos;');
}


var STYLE_PROPS = ['fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin',
  'stroke-dasharray', 'fill-opacity', 'stroke-opacity', 'color', 'font-size',
  'font-family', 'font-weight', 'font-style', 'text-anchor', 'fill-rule', 'visibility', 'display',
  'marker-start', 'marker-end', 'dominant-baseline', 'alignment-baseline', 'filter'];

function parseStyleAttr(str) {
  var o = {};
  if (!str) return o;
  var decls = str.split(';');
  for (var i = 0; i < decls.length; i++) {
    var d = decls[i];
    var c = d.indexOf(':');
    if (c === -1) continue;
    var k = d.slice(0, c).trim().toLowerCase();
    var v = d.slice(c + 1).trim();
    if (k) o[k] = v;
  }
  return o;
}


function computeStyle(node, inherited) {
  var s = {};
  for (var k in inherited) if (Object.prototype.hasOwnProperty.call(inherited, k)) s[k] = inherited[k];
  
  for (var i = 0; i < STYLE_PROPS.length; i++) {
    var p = STYLE_PROPS[i];
    var v = getAttr(node, p);
    if (v != null && v !== '') s[p] = v;
  }
  
  if (CSS_REGISTRY && CSS_REGISTRY.length) {
    var _tag = localName(node.name);
    var _clsAttr = getAttr(node, 'class');
    var _classes = _clsAttr ? String(_clsAttr).split(/\s+/) : [];
    var _id = getAttr(node, 'id') || '';
    for (var ci = 0; ci < CSS_REGISTRY.length; ci++) {
      var rule = CSS_REGISTRY[ci];
      if (_cssRuleMatches(rule.sel, _tag, _classes, _id)) {
        for (var dk in rule.decls) if (Object.prototype.hasOwnProperty.call(rule.decls, dk)) s[dk] = rule.decls[dk];
      }
    }
  }
  
  var st = parseStyleAttr(getAttr(node, 'style'));
  for (var kk in st) if (Object.prototype.hasOwnProperty.call(st, kk)) s[kk] = st[kk];
  
  var ownOpacity = getAttr(node, 'opacity');
  if (st['opacity'] != null) ownOpacity = st['opacity'];
  var parentOp = (inherited && inherited._op != null) ? inherited._op : 1;
  var op = ownOpacity != null ? parseFloat(ownOpacity) : 1;
  if (isNaN(op)) op = 1;
  s._op = parentOp * op;
  return s;
}

function styleNumber(s, key, dflt) {
  var v = s[key];
  if (v == null) return dflt;
  var f = parseFloat(v);
  return isNaN(f) ? dflt : f;
}


function srgbXml(c) {
  var alpha = (c.alpha == null) ? 1 : c.alpha;
  if (alpha >= 1) return '<a:srgbClr val="' + c.hex + '"/>';
  return '<a:srgbClr val="' + c.hex + '"><a:alpha val="' + Math.round(alpha * 100000) + '"/></a:srgbClr>';
}

function fillXmlFor(style) {
  var raw = style['fill'];
  var opMul = styleNumber(style, 'fill-opacity', 1) * (style._op == null ? 1 : style._op);
  
  if (raw && /^\s*url\(/i.test(raw)) {
    var id = extractRefId(raw);
    var g = id ? GRAD_REGISTRY[id] : null;
    if (g) return gradFillXml(g, opMul);
    raw = '#CCCCCC'; 
  }
  
  var c = parseColor(raw == null ? '#000000' : raw, style['color']);
  if (!c) c = { hex: '000000', alpha: 1 };
  if (c.type === 'none') return '<a:noFill/>';
  var alpha = (c.alpha == null ? 1 : c.alpha) * opMul;
  return '<a:solidFill>' + srgbXml({ hex: c.hex, alpha: alpha }) + '</a:solidFill>';
}

function strokeLnXml(style, strokeEmu) {
  var raw = style['stroke'];
  if (raw == null || String(raw).trim().toLowerCase() === 'none' || String(raw).trim() === '') return '';
  if (strokeEmu <= 0) strokeEmu = 3175; 
  var opMul = styleNumber(style, 'stroke-opacity', 1) * (style._op == null ? 1 : style._op);
  var paint;
  if (/^\s*url\(/i.test(raw)) {
    var g = GRAD_REGISTRY[extractRefId(raw)];
    paint = g ? gradFillXml(g, opMul) : '<a:solidFill><a:srgbClr val="CCCCCC"/></a:solidFill>';
  } else {
    var c = parseColor(raw, style['color']);
    if (!c || c.type === 'none') return '';
    paint = '<a:solidFill>' + srgbXml({ hex: c.hex, alpha: (c.alpha == null ? 1 : c.alpha) * opMul }) + '</a:solidFill>';
  }
  var cap = mapLineCap(style['stroke-linecap']);
  var join = mapLineJoin(style['stroke-linejoin']);
  var dash = (style['stroke-dasharray'] && style['stroke-dasharray'] !== 'none')
    ? '<a:prstDash val="dash"/>' : '<a:prstDash val="solid"/>';
  var ends = '';
  if (style['marker-start'] && style['marker-start'] !== 'none') {
    ends += '<a:headEnd type="triangle" w="med" len="med"/>';
  }
  if (style['marker-end'] && style['marker-end'] !== 'none') {
    ends += '<a:tailEnd type="triangle" w="med" len="med"/>';
  }
  return '<a:ln w="' + Math.round(strokeEmu) + '" cap="' + cap + '">' +
    paint + dash + join + ends + '</a:ln>';
}
function mapLineCap(v) {
  if (v === 'round') return 'rnd';
  if (v === 'square') return 'sq';
  return 'flat';
}
function mapLineJoin(v) {
  if (v === 'round') return '<a:round/>';
  if (v === 'bevel') return '<a:bevel/>';
  return '<a:miter lim="800000"/>';
}


function parseLength(v, dflt) {
  if (v == null) return dflt;
  var f = parseFloat(v);
  return isNaN(f) ? dflt : f;
}


function contentSize(svgEl) {
  var w, h;
  var vb = getAttr(svgEl, 'viewBox');
  if (vb) {
    var p = vb.trim().split(/[\s,]+/).map(parseFloat);
    if (p.length >= 4 && !isNaN(p[2]) && !isNaN(p[3])) { w = p[2]; h = p[3]; }
  }
  if (!w) w = parseLength(getAttr(svgEl, 'width'), 0);
  if (!h) h = parseLength(getAttr(svgEl, 'height'), 0);
  if (!w || w <= 0) w = 300;
  if (!h || h <= 0) h = 150;
  return { w: w, h: h };
}


function chooseSlideSize(aspect, content) {
  if (aspect === '16:9') return { w: 12192000, h: 6858000 };
  if (aspect === '4:3') return { w: 9144000, h: 6858000 };
  
  var H = 6858000;
  var W = Math.round(H * content.w / content.h);
  var MAXW = 12192000;
  if (W > MAXW) { W = MAXW; H = Math.round(W * content.h / content.w); }
  if (W < 914400) W = 914400;
  if (H < 914400) H = 914400;
  return { w: W, h: H };
}

function makeViewport(svgEl, slideW, slideH) {
  var vbMinX = 0, vbMinY = 0, contentW, contentH;
  var vb = getAttr(svgEl, 'viewBox');
  if (vb) {
    var p = vb.trim().split(/[\s,]+/).map(parseFloat);
    if (p.length >= 4 && !isNaN(p[2]) && !isNaN(p[3])) {
      vbMinX = p[0] || 0; vbMinY = p[1] || 0; contentW = p[2]; contentH = p[3];
    }
  }
  if (!contentW) contentW = parseLength(getAttr(svgEl, 'width'), 0);
  if (!contentH) contentH = parseLength(getAttr(svgEl, 'height'), 0);
  if (!contentW || contentW <= 0) contentW = 300;
  if (!contentH || contentH <= 0) contentH = 150;
  var S = Math.min(slideW / contentW, slideH / contentH);
  return {
    S: S,
    ox: (slideW - contentW * S) / 2,
    oy: (slideH - contentH * S) / 2,
    vbMinX: vbMinX, vbMinY: vbMinY,
    contentW: contentW, contentH: contentH,
    slideW: slideW, slideH: slideH   
  };
}



function shClipPoly(pts, x0, y0, x1, y1) {
  if (!pts.length) return pts;
  function clipEdge(poly, inside, intersect) {
    if (!poly.length) return poly;
    var out = [], prev = poly[poly.length - 1];
    for (var i = 0; i < poly.length; i++) {
      var curr = poly[i];
      if (inside(curr)) { if (!inside(prev)) out.push(intersect(prev, curr)); out.push(curr); }
      else if (inside(prev)) out.push(intersect(prev, curr));
      prev = curr;
    }
    return out;
  }
  function ix(a, b, x) { var t = (x - a.x) / (b.x - a.x); return { x: x, y: a.y + t * (b.y - a.y) }; }
  function iy(a, b, y) { var t = (y - a.y) / (b.y - a.y); return { x: a.x + t * (b.x - a.x), y: y }; }
  var p = pts;
  p = clipEdge(p, function(v) { return v.x >= x0; }, function(a, b) { return ix(a, b, x0); });
  p = clipEdge(p, function(v) { return v.x <= x1; }, function(a, b) { return ix(a, b, x1); });
  p = clipEdge(p, function(v) { return v.y >= y0; }, function(a, b) { return iy(a, b, y0); });
  p = clipEdge(p, function(v) { return v.y <= y1; }, function(a, b) { return iy(a, b, y1); });
  return p;
}


function clipCmdsToSlide(emuCmds, vp) {
  var x0 = 0, y0 = 0, x1 = vp.slideW, y1 = vp.slideH;
  var result = [];
  var pending = [], hasBezier = false;

  function flushAsIs() { for (var i = 0; i < pending.length; i++) result.push(pending[i]); }
  
  function flushOpenPoly() {
    if (!pending.length) return;
    var hasBez = false;
    for (var pi = 0; pi < pending.length; pi++) if (pending[pi].cmd === 'C') { hasBez = true; break; }
    if (hasBez) { flushAsIs(); return; }
    
    var started = false;
    for (var pj = 1; pj < pending.length; pj++) {
      var pa = { x: pending[pj-1].x, y: pending[pj-1].y };
      var pb = { x: pending[pj].x,   y: pending[pj].y   };
      var seg = shClipPoly([pa, pb], x0, y0, x1, y1);
      if (seg.length >= 2) {
        if (!started) { result.push({ cmd: 'M', x: seg[0].x, y: seg[0].y }); started = true; }
        else           { result.push({ cmd: 'L', x: seg[0].x, y: seg[0].y }); }
        result.push({ cmd: 'L', x: seg[1].x, y: seg[1].y });
      } else {
        started = false; 
      }
    }
  }

  for (var i = 0; i <= emuCmds.length; i++) {
    var c = (i < emuCmds.length) ? emuCmds[i] : null;
    if (!c || c.cmd === 'M') {
      
      if (pending.length) flushOpenPoly();
      pending = c ? [c] : [];
      hasBezier = false;
    } else if (c.cmd === 'Z') {
      pending.push(c);
      if (!hasBezier && pending.length >= 3) {
        
        var pts = [];
        for (var j = 0; j < pending.length; j++) {
          if (pending[j].cmd === 'M' || pending[j].cmd === 'L') pts.push({ x: pending[j].x, y: pending[j].y });
        }
        var clipped = shClipPoly(pts, x0, y0, x1, y1);
        if (clipped.length >= 3) {
          result.push({ cmd: 'M', x: clipped[0].x, y: clipped[0].y });
          for (var k = 1; k < clipped.length; k++) result.push({ cmd: 'L', x: clipped[k].x, y: clipped[k].y });
          result.push({ cmd: 'Z' });
        }
        
      } else {
        flushAsIs(); 
      }
      pending = [];
      hasBezier = false;
    } else {
      if (c.cmd === 'C') hasBezier = true;
      pending.push(c);
    }
  }
  return result.length ? result : emuCmds; 
}
function vpEmu(vp, wx, wy) {
  return {
    x: Math.round(vp.ox + (wx - vp.vbMinX) * vp.S),
    y: Math.round(vp.oy + (wy - vp.vbMinY) * vp.S)
  };
}

function ctmScale(m) { return Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2])) || 1; }



function cmdsToEmu(cmds, ctm, vp) {
  var out = [];
  for (var i = 0; i < cmds.length; i++) {
    var c = cmds[i];
    if (c.cmd === 'M' || c.cmd === 'L') {
      var w = applyMatrix(ctm, c.x, c.y); var e = vpEmu(vp, w.x, w.y);
      out.push({ cmd: c.cmd, x: e.x, y: e.y });
    } else if (c.cmd === 'C') {
      var w1 = applyMatrix(ctm, c.x1, c.y1), e1 = vpEmu(vp, w1.x, w1.y);
      var w2 = applyMatrix(ctm, c.x2, c.y2), e2 = vpEmu(vp, w2.x, w2.y);
      var w3 = applyMatrix(ctm, c.x, c.y), e3 = vpEmu(vp, w3.x, w3.y);
      out.push({ cmd: 'C', x1: e1.x, y1: e1.y, x2: e2.x, y2: e2.y, x: e3.x, y: e3.y });
    } else if (c.cmd === 'Z') {
      out.push({ cmd: 'Z' });
    }
  }
  return out;
}


function normalizeEmuPath(cmds) {
  var out = [];
  var penX = null, penY = null, startX = null, startY = null, open = false;
  for (var i = 0; i < cmds.length; i++) {
    var c = cmds[i];
    if (c.cmd === 'M') {
      out.push(c); penX = c.x; penY = c.y; startX = c.x; startY = c.y; open = true;
    } else if (c.cmd === 'Z') {
      out.push({ cmd: 'Z' }); penX = startX; penY = startY; open = false;
    } else {
      if (!open) {
        if (penX == null) { penX = c.x; penY = c.y; startX = c.x; startY = c.y; }
        out.push({ cmd: 'M', x: penX, y: penY });
        startX = penX; startY = penY; open = true;
      }
      out.push(c); penX = c.x; penY = c.y;
    }
  }
  return out;
}


function buildCustGeom(emuCmds) {
  var cmds = normalizeEmuPath(emuCmds);
  var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
  function acc(x, y) { if (x < minX) minX = x; if (y < minY) minY = y; if (x > maxX) maxX = x; if (y > maxY) maxY = y; }
  for (var i = 0; i < cmds.length; i++) {
    var c = cmds[i];
    if (c.cmd === 'M' || c.cmd === 'L') acc(c.x, c.y);
    else if (c.cmd === 'C') { acc(c.x1, c.y1); acc(c.x2, c.y2); acc(c.x, c.y); }
  }
  if (!isFinite(minX)) { minX = 0; minY = 0; maxX = 1; maxY = 1; }
  var W = Math.max(1, maxX - minX), H = Math.max(1, maxY - minY);
  var body = '';
  for (var j = 0; j < cmds.length; j++) {
    var k = cmds[j];
    if (k.cmd === 'M') body += '<a:moveTo><a:pt x="' + (k.x - minX) + '" y="' + (k.y - minY) + '"/></a:moveTo>';
    else if (k.cmd === 'L') body += '<a:lnTo><a:pt x="' + (k.x - minX) + '" y="' + (k.y - minY) + '"/></a:lnTo>';
    else if (k.cmd === 'C') body += '<a:cubicBezTo>' +
      '<a:pt x="' + (k.x1 - minX) + '" y="' + (k.y1 - minY) + '"/>' +
      '<a:pt x="' + (k.x2 - minX) + '" y="' + (k.y2 - minY) + '"/>' +
      '<a:pt x="' + (k.x - minX) + '" y="' + (k.y - minY) + '"/></a:cubicBezTo>';
    else if (k.cmd === 'Z') body += '<a:close/>';
  }
  var geom = '<a:custGeom><a:avLst/><a:gdLst/><a:ahLst/><a:cxnLst/>' +
    '<a:rect l="0" t="0" r="' + W + '" b="' + H + '"/>' +
    '<a:pathLst><a:path w="' + W + '" h="' + H + '">' + body + '</a:path></a:pathLst></a:custGeom>';
  return { xfrm: { x: minX, y: minY, cx: W, cy: H }, geom: geom };
}


function xfrmXml(x, y, cx, cy) {
  return '<a:xfrm><a:off x="' + Math.round(x) + '" y="' + Math.round(y) + '"/>' +
    '<a:ext cx="' + Math.max(1, Math.round(cx)) + '" cy="' + Math.max(1, Math.round(cy)) + '"/></a:xfrm>';
}

function spShape(id, name, xfrm, geom, fill, ln, effect) {
  return '<p:sp>' +
    '<p:nvSpPr><p:cNvPr id="' + id + '" name="' + xmlEscape(name) + '"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>' +
    '<p:spPr>' + xfrm + geom + fill + ln + (effect || '') + '</p:spPr>' +
    '</p:sp>';
}


function presetBox(id, name, prst, advLst, x, y, cx, cy, fill, ln, effect) {
  var geom = '<a:prstGeom prst="' + prst + '">' + (advLst || '<a:avLst/>') + '</a:prstGeom>';
  return spShape(id, name, xfrmXml(x, y, cx, cy), geom, fill, ln, effect);
}


function convertRect(node, ctm, style, vp, ctx) {
  var x = parseLength(getAttr(node, 'x'), 0);
  var y = parseLength(getAttr(node, 'y'), 0);
  var w = parseLength(getAttr(node, 'width'), 0);
  var h = parseLength(getAttr(node, 'height'), 0);
  if (w <= 0 || h <= 0) return '';
  var rx = getAttr(node, 'rx'), ry = getAttr(node, 'ry');
  var rxn = (rx != null) ? parseFloat(rx) : (ry != null ? parseFloat(ry) : 0);
  var ryn = (ry != null) ? parseFloat(ry) : (rx != null ? parseFloat(rx) : 0);
  var fill = fillXmlFor(style);
  var strokeEmu = styleNumber(style, 'stroke-width', 1) * ctmScale(ctm) * vp.S;
  var ln = strokeLnXml(style, strokeEmu);
  var effect = effectXmlFor(node, vp, style, ctmScale(ctm) * vp.S);

  if (matIsAxisAligned(ctm)) {
    var p1 = applyMatrix(ctm, x, y), p2 = applyMatrix(ctm, x + w, y + h);
    var a = vpEmu(vp, Math.min(p1.x, p2.x), Math.min(p1.y, p2.y));
    var b = vpEmu(vp, Math.max(p1.x, p2.x), Math.max(p1.y, p2.y));
    var bpxR = blurPxOf(node), solidR = resolveFillColor(style);
    if (bpxR > 0 && solidR) {
      var sigmaR = bpxR * ctmScale(ctm) * vp.S;    
      var W0R = b.x - a.x, H0R = b.y - a.y;
      var cXR = (a.x + b.x) / 2, cYR = (a.y + b.y) / 2;
      return emitGlowGroup(ctx, false, cXR, cYR, W0R, H0R, sigmaR, solidR.hex, solidR.alpha);
    }
    if (rxn > 0 || ryn > 0) {
      var r = Math.min(rxn, ryn) || Math.max(rxn, ryn);
      var adj = Math.max(0, Math.min(50000, Math.round(r / Math.min(w, h) * 100000)));
      return presetBox(ctx.id++, 'roundRect', 'roundRect',
        '<a:avLst><a:gd name="adj" fmla="val ' + adj + '"/></a:avLst>',
        a.x, a.y, b.x - a.x, b.y - a.y, fill, ln, effect);
    }
    return presetBox(ctx.id++, 'rect', 'rect', '<a:avLst/>', a.x, a.y, b.x - a.x, b.y - a.y, fill, ln, effect);
  }
  
  var cmds = [
    { cmd: 'M', x: x, y: y }, { cmd: 'L', x: x + w, y: y },
    { cmd: 'L', x: x + w, y: y + h }, { cmd: 'L', x: x, y: y + h }, { cmd: 'Z' }
  ];
  var cg = buildCustGeom(cmdsToEmu(cmds, ctm, vp));
  return spShape(ctx.id++, 'rect', xfrmXml(cg.xfrm.x, cg.xfrm.y, cg.xfrm.cx, cg.xfrm.cy), cg.geom, fill, ln, effect);
}

function convertEllipse(node, ctm, style, vp, ctx, isCircle) {
  var cx = parseLength(getAttr(node, 'cx'), 0);
  var cy = parseLength(getAttr(node, 'cy'), 0);
  var rx, ry;
  if (isCircle) { rx = ry = parseLength(getAttr(node, 'r'), 0); }
  else { rx = parseLength(getAttr(node, 'rx'), 0); ry = parseLength(getAttr(node, 'ry'), 0); }
  if (rx <= 0 || ry <= 0) return '';
  var fill = fillXmlFor(style);
  var strokeEmu = styleNumber(style, 'stroke-width', 1) * ctmScale(ctm) * vp.S;
  var ln = strokeLnXml(style, strokeEmu);
  var effect = effectXmlFor(node, vp, style, ctmScale(ctm) * vp.S);

  if (matIsAxisAligned(ctm)) {
    var p1 = applyMatrix(ctm, cx - rx, cy - ry), p2 = applyMatrix(ctm, cx + rx, cy + ry);
    var a = vpEmu(vp, Math.min(p1.x, p2.x), Math.min(p1.y, p2.y));
    var b = vpEmu(vp, Math.max(p1.x, p2.x), Math.max(p1.y, p2.y));
    var bpx = blurPxOf(node), solid = resolveFillColor(style);
    if (bpx > 0 && solid) {
      var sigma = bpx * ctmScale(ctm) * vp.S;       
      var W0 = b.x - a.x, H0 = b.y - a.y;
      var cX = (a.x + b.x) / 2, cY = (a.y + b.y) / 2;
      var isCircularEllipse = Math.abs(rx - ry) / Math.max(rx, ry) < 0.05;
      if (isCircularEllipse) {
        var rOut = sigma * GLOW_ROUT;               
        return emitGlowRadial(ctx, cX - W0 / 2 - rOut, cY - H0 / 2 - rOut, W0 + 2 * rOut, H0 + 2 * rOut, solid.hex, solid.alpha);
      } else {
        return emitGlowGroup(ctx, true, cX, cY, W0, H0, sigma, solid.hex, solid.alpha);
      }
    }
    return presetBox(ctx.id++, 'ellipse', 'ellipse', '<a:avLst/>', a.x, a.y, b.x - a.x, b.y - a.y, fill, ln, effect);
  }
  
  var K = 0.5522847498307936;
  var ox = rx * K, oy = ry * K;
  var cmds = [
    { cmd: 'M', x: cx + rx, y: cy },
    { cmd: 'C', x1: cx + rx, y1: cy + oy, x2: cx + ox, y2: cy + ry, x: cx, y: cy + ry },
    { cmd: 'C', x1: cx - ox, y1: cy + ry, x2: cx - rx, y2: cy + oy, x: cx - rx, y: cy },
    { cmd: 'C', x1: cx - rx, y1: cy - oy, x2: cx - ox, y2: cy - ry, x: cx, y: cy - ry },
    { cmd: 'C', x1: cx + ox, y1: cy - ry, x2: cx + rx, y2: cy - oy, x: cx + rx, y: cy },
    { cmd: 'Z' }
  ];
  var cg = buildCustGeom(cmdsToEmu(cmds, ctm, vp));
  return spShape(ctx.id++, 'ellipse', xfrmXml(cg.xfrm.x, cg.xfrm.y, cg.xfrm.cx, cg.xfrm.cy), cg.geom, fill, ln, effect);
}

function convertLine(node, ctm, style, vp, ctx) {
  var x1 = parseLength(getAttr(node, 'x1'), 0), y1 = parseLength(getAttr(node, 'y1'), 0);
  var x2 = parseLength(getAttr(node, 'x2'), 0), y2 = parseLength(getAttr(node, 'y2'), 0);
  var strokeEmu = styleNumber(style, 'stroke-width', 1) * ctmScale(ctm) * vp.S;
  var ln = strokeLnXml(style, strokeEmu);
  if (!ln) return ''; 
  var effect = effectXmlFor(node, vp, style, ctmScale(ctm) * vp.S);
  var cmds = [{ cmd: 'M', x: x1, y: y1 }, { cmd: 'L', x: x2, y: y2 }];
  var emuLine = clipCmdsToSlide(cmdsToEmu(cmds, ctm, vp), vp);
  if (!emuLine.length) return '';
  var cg = buildCustGeom(emuLine);
  return spShape(ctx.id++, 'line', xfrmXml(cg.xfrm.x, cg.xfrm.y, cg.xfrm.cx, cg.xfrm.cy), cg.geom, '<a:noFill/>', ln, effect);
}

function parsePoints(str) {
  if (!str) return [];
  var nums = str.trim().split(/[\s,]+/).map(parseFloat).filter(function (n) { return !isNaN(n); });
  var pts = [];
  for (var i = 0; i + 1 < nums.length; i += 2) pts.push({ x: nums[i], y: nums[i + 1] });
  return pts;
}

function convertPoly(node, ctm, style, vp, ctx, closed) {
  var pts = parsePoints(getAttr(node, 'points'));
  if (pts.length < 2) return '';
  var cmds = [{ cmd: 'M', x: pts[0].x, y: pts[0].y }];
  for (var i = 1; i < pts.length; i++) cmds.push({ cmd: 'L', x: pts[i].x, y: pts[i].y });
  if (closed) cmds.push({ cmd: 'Z' });
  var fill = fillXmlFor(style); 
  var strokeEmu = styleNumber(style, 'stroke-width', 1) * ctmScale(ctm) * vp.S;
  var ln = strokeLnXml(style, strokeEmu);
  var effect = effectXmlFor(node, vp, style, ctmScale(ctm) * vp.S);
  var emuCmds = clipCmdsToSlide(cmdsToEmu(cmds, ctm, vp), vp); 
  if (!emuCmds.length) return '';
  var bpxPoly = blurPxOf(node), solidPoly = resolveFillColor(style);
  if (bpxPoly > 0 && solidPoly) {
    var sigmaPoly = bpxPoly * ctmScale(ctm) * vp.S;
    return emitGlowPath(ctx, emuCmds, solidPoly.hex, solidPoly.alpha, sigmaPoly);
  }
  var cg = buildCustGeom(emuCmds);
  return spShape(ctx.id++, closed ? 'polygon' : 'polyline',
    xfrmXml(cg.xfrm.x, cg.xfrm.y, cg.xfrm.cx, cg.xfrm.cy), cg.geom, fill, ln, effect);
}

function convertPath(node, ctm, style, vp, ctx) {
  var d = getAttr(node, 'd');
  var cmds = parsePathData(d);
  if (!cmds.length) return '';
  var fill = fillXmlFor(style);
  var strokeEmu = styleNumber(style, 'stroke-width', 1) * ctmScale(ctm) * vp.S;
  var ln = strokeLnXml(style, strokeEmu);
  var effect = effectXmlFor(node, vp, style, ctmScale(ctm) * vp.S);
  var emuCmds = clipCmdsToSlide(cmdsToEmu(cmds, ctm, vp), vp); 
  if (!emuCmds.length) return '';
  var bpxPath = blurPxOf(node), solidPath = resolveFillColor(style);
  if (bpxPath > 0 && solidPath) {
    var sigmaPath = bpxPath * ctmScale(ctm) * vp.S;
    return emitGlowPath(ctx, emuCmds, solidPath.hex, solidPath.alpha, sigmaPath);
  }
  var cg = buildCustGeom(emuCmds);
  return spShape(ctx.id++, 'path', xfrmXml(cg.xfrm.x, cg.xfrm.y, cg.xfrm.cx, cg.xfrm.cy), cg.geom, fill, ln, effect);
}

function convertText(node, ctm, style, vp, ctx) {
  var txt = textContent(node).replace(/\s+/g, ' ').trim();
  if (!txt) return '';
  var x = parseLength(getAttr(node, 'x'), 0), y = parseLength(getAttr(node, 'y'), 0);
  var fontSize = styleNumber(style, 'font-size', 16);
  var anchor = style['text-anchor'] || 'start';
  var family = (style['font-family'] || 'Arial').split(',')[0].replace(/["']/g, '').trim();
  
  var fw = String(style['font-weight'] || 'normal').toLowerCase();
  var isBold = (fw === 'bold' || fw === 'bolder' || parseInt(fw, 10) >= 600);
  var fst = String(style['font-style'] || 'normal').toLowerCase();
  var isItalic = (fst === 'italic' || fst === 'oblique');

  var sizeEmu = fontSize * ctmScale(ctm) * vp.S;
  var sizePt = sizeEmu / EMU_PER_PT;
  var sz = Math.max(100, Math.round(sizePt * 100)); 
  
  var measuredW = parseFloat(getAttr(node, 'data-measured-width'));
  var txtW = 0;
  if (!isNaN(measuredW) && measuredW > 0) {
    txtW = measuredW * ctmScale(ctm) * vp.S;
  } else {
    for (var i = 0; i < txt.length; i++) {
      txtW += (txt.charCodeAt(i) > 255) ? sizeEmu * 1.15 : sizeEmu * 0.65;
    }
  }
  
  var boxW = Math.max(txtW + sizeEmu * 1.5, sizeEmu * 2);
  var boxH = sizeEmu * 1.4;

  var anchorPt = applyMatrix(ctm, x, y);
  var base = vpEmu(vp, anchorPt.x, anchorPt.y);
  
  
  var algn = (anchor === 'middle') ? 'ctr' : (anchor === 'end' ? 'r' : 'l');
  var left = (anchor === 'middle') ? (base.x - boxW / 2) : (anchor === 'end' ? (base.x - boxW) : base.x);
  
  var db = String(style['dominant-baseline'] || style['alignment-baseline'] || '').toLowerCase();
  var vAnchor, top;
  if (db === 'middle' || db === 'central') { vAnchor = 'ctr'; top = base.y - boxH / 2; } 
  else if (db === 'hanging' || db === 'text-before-edge') { vAnchor = 't'; top = base.y; } 
  else { vAnchor = 't'; top = base.y - sizeEmu; } 

  
  var raw = style['fill'];
  var c = parseColor(raw == null ? '#000000' : raw, style['color']);
  if (!c || c.type === 'none') c = { hex: '000000', alpha: 1 };
  var fo = styleNumber(style, 'fill-opacity', 1) * (style._op == null ? 1 : style._op);
  var rPrFill = '<a:solidFill>' + srgbXml({ hex: c.hex, alpha: (c.alpha == null ? 1 : c.alpha) * fo }) + '</a:solidFill>';

  
  var effect = effectXmlFor(node, vp, style, ctmScale(ctm) * vp.S);

  var txBody = '<p:txBody>' +
    '<a:bodyPr wrap="square" lIns="0" tIns="0" rIns="0" bIns="0" anchor="' + vAnchor + '"><a:noAutofit/></a:bodyPr>' +
    '<a:lstStyle/>' +
    '<a:p><a:pPr algn="' + algn + '"/>' +
    '<a:r><a:rPr lang="ja-JP" sz="' + sz + '" dirty="0"' + (isBold ? ' b="1"' : '') + (isItalic ? ' i="1"' : '') + '>' + rPrFill +
    '<a:latin typeface="' + xmlEscape(family) + '"/><a:ea typeface="' + xmlEscape(family) + '"/></a:rPr>' +
    '<a:t>' + xmlEscape(txt) + '</a:t></a:r></a:p>' +
    '</p:txBody>';

  return '<p:sp>' +
    '<p:nvSpPr><p:cNvPr id="' + (ctx.id++) + '" name="text"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>' +
    '<p:spPr>' + xfrmXml(left, top, boxW, boxH) +
    '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/>' + (effect || '') + '</p:spPr>' +
    txBody + '</p:sp>';
}


function walkNode(node, ctm, inherited, vp, ctx) {
  var name = localName(node.name);
  if (name === 'defs' || name === 'title' || name === 'desc' || name === 'metadata' ||
    name === 'symbol' || name === 'style' || name === 'clipPath' || name === 'mask' ||
    name === 'linearGradient' || name === 'radialGradient' || name === 'pattern' ||
    name === 'filter' || name === 'marker') {
    return; 
  }
  var style = computeStyle(node, inherited);
  if (style['display'] === 'none' || style['visibility'] === 'hidden') {
    if (style['display'] === 'none') return;
  }
  var tf = getAttr(node, 'transform');
  var localCtm = tf ? matMul(ctm, parseTransform(tf)) : ctm;

  switch (name) {
    case 'g': case 'a': case 'svg':
      var kids = elementChildren(node);
      for (var i = 0; i < kids.length; i++) walkNode(kids[i], localCtm, style, vp, ctx);
      return;
    case 'rect': ctx.shapes.push(convertRect(node, localCtm, style, vp, ctx)); return;
    case 'circle': ctx.shapes.push(convertEllipse(node, localCtm, style, vp, ctx, true)); return;
    case 'ellipse': ctx.shapes.push(convertEllipse(node, localCtm, style, vp, ctx, false)); return;
    case 'line': ctx.shapes.push(convertLine(node, localCtm, style, vp, ctx)); return;
    case 'polyline': ctx.shapes.push(convertPoly(node, localCtm, style, vp, ctx, false)); return;
    case 'polygon': ctx.shapes.push(convertPoly(node, localCtm, style, vp, ctx, true)); return;
    case 'path': ctx.shapes.push(convertPath(node, localCtm, style, vp, ctx)); return;
    case 'text': ctx.shapes.push(convertText(node, localCtm, style, vp, ctx)); return;
    case 'image': ctx.shapes.push(convertImage(node, localCtm, style, vp, ctx)); return;
    default:
      
      var kids2 = elementChildren(node);
      for (var j = 0; j < kids2.length; j++) walkNode(kids2[j], localCtm, style, vp, ctx);
      return;
  }
}


function buildSlideXml(shapesXml) {
  return XML_DECL +
    '<p:sld xmlns:a="' + NS_A + '" xmlns:r="' + NS_R + '" xmlns:p="' + NS_P + '">' +
    '<p:cSld><p:spTree>' +
    '<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
    '<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>' +
    shapesXml +
    '</p:spTree></p:cSld>' +
    '<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>' +
    '</p:sld>';
}


function buildPptxParts(svgText, options) {
  options = options || {};

  var root = parseXml(svgText);
  var svgEl = documentElement(root);
  if (!svgEl || localName(svgEl.name) !== 'svg') {
    
    svgEl = findSvg(root);
  }
  if (!svgEl) throw new Error('有効な <svg> 要素が見つかりませんでした。');

  GRAD_REGISTRY = collectGradients(root, {}); 
  CSS_REGISTRY = collectCssRules(root, []);   
  FILTER_REGISTRY = collectFilters(root, {}); 

  
  var content = contentSize(svgEl);
  var slide = chooseSlideSize(options.aspect, content);
  var slideW = slide.w, slideH = slide.h;
  var vp = makeViewport(svgEl, slideW, slideH);
  var ctx = { id: 2, shapes: [], warnings: [], media: [] };
  var initialStyle = { fill: '#000000', stroke: 'none', color: '#000000', _op: 1 };

  var kids = elementChildren(svgEl);
  for (var i = 0; i < kids.length; i++) {
    walkNode(kids[i], matIdentity(), initialStyle, vp, ctx);
  }
  var shapesXml = ctx.shapes.filter(function (s) { return s; }).join('');
  var shapeCount = ctx.shapes.filter(function (s) { return s; }).length;

  var parts = {
    '[Content_Types].xml': ooxmlContentTypes(),
    '_rels/.rels': ooxmlRootRels(),
    'ppt/presentation.xml': ooxmlPresentation(slideW, slideH),
    'ppt/_rels/presentation.xml.rels': ooxmlPresentationRels(),
    'ppt/presProps.xml': ooxmlPresProps(),
    'ppt/theme/theme1.xml': ooxmlTheme(),
    'ppt/slideMasters/slideMaster1.xml': ooxmlSlideMaster(),
    'ppt/slideMasters/_rels/slideMaster1.xml.rels': ooxmlSlideMasterRels(),
    'ppt/slideLayouts/slideLayout1.xml': ooxmlSlideLayout(),
    'ppt/slideLayouts/_rels/slideLayout1.xml.rels': ooxmlSlideLayoutRels(),
    'ppt/slides/slide1.xml': buildSlideXml(shapesXml),
    'ppt/slides/_rels/slide1.xml.rels': svgSlideRels(ctx.media)
  };
  return { parts: parts, shapeCount: shapeCount, warnings: ctx.warnings, media: ctx.media };
}

function svgSlideRels(mediaList) {
  var rels = '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>';
  for (var i = 0; i < mediaList.length; i++) {
    var ext = mediaList[i].path.split('.').pop();
    rels += '<Relationship Id="rId' + (i + 2) + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image' + (i + 1) + '.' + ext + '"/>';
  }
  return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + rels + '</Relationships>';
}

function convertImage(node, ctm, style, vp, ctx) {
  var href = getAttr(node, 'href') || getAttr(node, 'xlink:href');
  if (!href) return '';
  var w = parseLength(getAttr(node, 'width'), 0);
  var h = parseLength(getAttr(node, 'height'), 0);
  if (w <= 0 || h <= 0) return '';
  var x = parseLength(getAttr(node, 'x'), 0);
  var y = parseLength(getAttr(node, 'y'), 0);

  var pt = applyMatrix(ctm, x, y);
  var base = vpEmu(vp, pt.x, pt.y);
  var wEmu = w * ctmScale(ctm) * vp.S;
  var hEmu = h * ctmScale(ctm) * vp.S;

  var idx = ctx.media.length + 1;
  var isData = href.indexOf('data:') === 0;
  var mime = 'image/png';
  var ext = 'png';
  if (!isData) {
    var m = href.match(/\.(png|jpe?g|gif|svg)(\?.*)?$/i);
    if (m) {
      ext = m[1].toLowerCase().replace('jpeg', 'jpg');
      mime = ext === 'jpg' ? 'image/jpeg' : (ext === 'gif' ? 'image/gif' : (ext === 'svg' ? 'image/svg+xml' : 'image/png'));
    }
  } else {
    var m = href.match(/^data:(image\/[a-zA-Z0-9+-]+);/i);
    if (m) {
      mime = m[1].toLowerCase();
      if (mime.indexOf('jpeg') >= 0) ext = 'jpg';
      else if (mime.indexOf('gif') >= 0) ext = 'gif';
      else if (mime.indexOf('svg') >= 0) ext = 'svg';
    }
  }

  ctx.media.push({
    path: 'ppt/media/image' + idx + '.' + ext,
    dataUrl: isData ? href : null,
    url: !isData ? href : null,
    mime: mime
  });

  var rId = 'rId' + (idx + 1);
  var id = ctx.id++;
  
  return '<p:pic><p:nvPicPr><p:cNvPr id="' + id + '" name="image' + idx + '"/>' +
    '<p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr>' +
    '<p:blipFill><a:blip r:embed="' + rId + '"/><a:stretch><a:fillRect/></a:stretch></p:blipFill>' +
    '<p:spPr>' + xfrmXml(base.x, base.y, wEmu, hEmu) + '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>';
}

function findSvg(node) {
  if (node.type === 'element' && localName(node.name) === 'svg') return node;
  var kids = elementChildren(node);
  for (var i = 0; i < kids.length; i++) {
    var r = findSvg(kids[i]);
    if (r) return r;
  }
  return null;
}
/* ===== SvgToPptxMulti.gs ===== */

function buildMultiSlidePptxParts(svgTexts, options) {
  options = options || {};
  var aspect = options.aspect || '16:9';
  var slideSize = options.slideSizeEmu || chooseSlideSize(aspect, { w: 960, h: 540 });
  var slideW = slideSize.w, slideH = slideSize.h;

  var slidesXml = [], slideRels = [], warnings = [], totalShapes = 0, mediaTotal = 0;

  for (var s = 0; s < svgTexts.length; s++) {
    var root = parseXml(svgTexts[s]);
    var svgEl = documentElement(root);
    if (!svgEl || localName(svgEl.name) !== 'svg') svgEl = findSvg(root);
    if (!svgEl) throw new Error('slide ' + (s + 1) + ': <svg> not found');
    GRAD_REGISTRY = collectGradients(root, {});            
    CSS_REGISTRY = collectCssRules(root, []);              
    FILTER_REGISTRY = collectFilters(root, {});            
    var vp = makeViewport(svgEl, slideW, slideH);           
    var sctx = { id: 2, shapes: [], warnings: [], media: [] };
    var initialStyle = { fill: '#000000', stroke: 'none', color: '#000000', _op: 1 };
    var kids = elementChildren(svgEl);
    for (var i = 0; i < kids.length; i++) walkNode(kids[i], matIdentity(), initialStyle, vp, sctx);
    var shapesXml = sctx.shapes.filter(function (x) { return x; }).join('');
    totalShapes += sctx.shapes.filter(function (x) { return x; }).length;
    mediaTotal += sctx.media.length;
    warnings = warnings.concat(sctx.warnings.map(function (w) { return 'slide' + (s + 1) + ': ' + w; }));
    slidesXml.push(buildSlideXml(shapesXml));
    slideRels.push('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="' + REL + '/slideLayout" Target="../slideLayouts/slideLayout1.xml"/></Relationships>');
  }

  var n = svgTexts.length;
  var sldIds = '';
  for (var k = 0; k < n; k++) sldIds += '<p:sldId id="' + (256 + k) + '" r:id="rId' + (2 + k) + '"/>';
  var presentation = XML_DECL +
    '<p:presentation xmlns:a="' + NS_A + '" xmlns:r="' + NS_R + '" xmlns:p="' + NS_P + '" saveSubsetFonts="1">' +
    '<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>' +
    '<p:sldIdLst>' + sldIds + '</p:sldIdLst>' +
    '<p:sldSz cx="' + slideW + '" cy="' + slideH + '" type="screen16x9"/>' +
    '<p:notesSz cx="6858000" cy="9144000"/></p:presentation>';

  var presRels = '<Relationship Id="rId1" Type="' + REL + '/slideMaster" Target="slideMasters/slideMaster1.xml"/>';
  for (var k = 0; k < n; k++) presRels += '<Relationship Id="rId' + (2 + k) + '" Type="' + REL + '/slide" Target="slides/slide' + (k + 1) + '.xml"/>';
  presRels += '<Relationship Id="rId' + (2 + n) + '" Type="' + REL + '/presProps" Target="presProps.xml"/>';
  presRels += '<Relationship Id="rId' + (3 + n) + '" Type="' + REL + '/theme" Target="theme/theme1.xml"/>';
  var presentationRels = XML_DECL + '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + presRels + '</Relationships>';

  var slideOverrides = '';
  for (var k = 0; k < n; k++) slideOverrides += '<Override PartName="/ppt/slides/slide' + (k + 1) + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>';
  var contentTypes = XML_DECL +
    '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
    '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
    '<Default Extension="xml" ContentType="application/xml"/>' +
    '<Default Extension="png" ContentType="image/png"/>' +
    '<Default Extension="jpeg" ContentType="image/jpeg"/>' +
    '<Default Extension="jpg" ContentType="image/jpeg"/>' +
    '<Default Extension="gif" ContentType="image/gif"/>' +
    '<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>' +
    '<Override PartName="/ppt/presProps.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"/>' +
    '<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>' +
    '<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>' +
    slideOverrides +
    '<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>' +
    '</Types>';

  var parts = {
    '[Content_Types].xml': contentTypes,
    '_rels/.rels': ooxmlRootRels(),
    'ppt/presentation.xml': presentation,
    'ppt/_rels/presentation.xml.rels': presentationRels,
    'ppt/presProps.xml': ooxmlPresProps(),
    'ppt/theme/theme1.xml': ooxmlTheme(),
    'ppt/slideMasters/slideMaster1.xml': ooxmlSlideMaster(),
    'ppt/slideMasters/_rels/slideMaster1.xml.rels': ooxmlSlideMasterRels(),
    'ppt/slideLayouts/slideLayout1.xml': ooxmlSlideLayout(),
    'ppt/slideLayouts/_rels/slideLayout1.xml.rels': ooxmlSlideLayoutRels()
  };
  for (var k = 0; k < n; k++) {
    parts['ppt/slides/slide' + (k + 1) + '.xml'] = slidesXml[k];
    parts['ppt/slides/_rels/slide' + (k + 1) + '.xml.rels'] = slideRels[k];
  }
  return { parts: parts, shapeCount: totalShapes, mediaCount: mediaTotal, warnings: warnings, slideCount: n };
}
/* ===== RestServices.gs ===== */



function _restToken_() {
  return ScriptApp.getOAuthToken();
}


function _restCheck_(resp, ctx) {
  var code = resp.getResponseCode();
  if (code >= 400) {
    var body = '';
    try { body = resp.getContentText(); } catch (e) {}
    throw new Error('[REST ' + ctx + '] HTTP ' + code + ': ' + body);
  }
  return resp;
}


function _restJson_(resp) {
  try { return JSON.parse(resp.getContentText() || '{}'); } catch (e) { return {}; }
}


var Slides = {
  Presentations: {
    
    batchUpdate: function (resource, presentationId) {
      var url = 'https://slides.googleapis.com/v1/presentations/' + encodeURIComponent(presentationId) + ':batchUpdate';
      var resp = UrlFetchApp.fetch(url, {
        method: 'post',
        contentType: 'application/json',
        payload: JSON.stringify(resource || {}),
        headers: { Authorization: 'Bearer ' + _restToken_() },
        muteHttpExceptions: true
      });
      _restCheck_(resp, 'presentations.batchUpdate');
      return _restJson_(resp);
    },
    
    get: function (presentationId, optFields) {
      var fields = optFields || 'slides.objectId';
      var url = 'https://slides.googleapis.com/v1/presentations/' + encodeURIComponent(presentationId) + '?fields=' + encodeURIComponent(fields);
      var resp = UrlFetchApp.fetch(url, {
        method: 'get',
        headers: { Authorization: 'Bearer ' + _restToken_() },
        muteHttpExceptions: true
      });
      _restCheck_(resp, 'presentations.get');
      return _restJson_(resp);
    }
  }
};


var Drive = {
  Files: {
    
    create: function (metadata, mediaBlob, optionalArgs) {
      return _driveMultipartUpload_('post', null, metadata, mediaBlob, optionalArgs);
    },
    
    update: function (metadata, fileId, mediaBlob, optionalArgs) {
      return _driveMultipartUpload_('patch', fileId, metadata, mediaBlob, optionalArgs);
    },
    
    remove: function (fileId) {
      var url = 'https://www.googleapis.com/drive/v3/files/' + encodeURIComponent(fileId);
      var resp = UrlFetchApp.fetch(url, {
        method: 'delete',
        headers: { Authorization: 'Bearer ' + _restToken_() },
        muteHttpExceptions: true
      });
      var code = resp.getResponseCode();
      if (code !== 200 && code !== 204) {
        _restCheck_(resp, 'files.remove'); 
      }
    }
  }
};


function _driveMultipartUpload_(method, fileId, metadata, mediaBlob, optionalArgs) {
  var fields = (optionalArgs && optionalArgs.fields) ? optionalArgs.fields : 'id,name,webViewLink';
  var base = 'https://www.googleapis.com/upload/drive/v3/files';
  var url = (method === 'patch')
    ? (base + '/' + encodeURIComponent(fileId) + '?uploadType=multipart&fields=' + encodeURIComponent(fields))
    : (base + '?uploadType=multipart&fields=' + encodeURIComponent(fields));

  var boundary = 'majin_boundary_' + Utilities.getUuid().replace(/-/g, '');
  var NL = '\r\n';

  
  var part1 = '--' + boundary + NL
    + 'Content-Type: application/json; charset=UTF-8' + NL + NL
    + JSON.stringify(metadata || {}) + NL;
  
  var mediaType = (mediaBlob && mediaBlob.getContentType && mediaBlob.getContentType())
    || 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
  var part2Header = '--' + boundary + NL
    + 'Content-Type: ' + mediaType + NL + NL;
  var closing = NL + '--' + boundary + '--';

  
  var head = Utilities.newBlob(part1 + part2Header).getBytes();
  var media = mediaBlob.getBytes();
  var tail = Utilities.newBlob(closing).getBytes();
  var body = [].concat(head, media, tail);

  var resp = UrlFetchApp.fetch(url, {
    method: method,
    contentType: 'multipart/related; boundary=' + boundary,
    payload: body,
    headers: { Authorization: 'Bearer ' + _restToken_() },
    muteHttpExceptions: true
  });
  _restCheck_(resp, 'files.' + (method === 'patch' ? 'update' : 'create'));
  return _restJson_(resp);
}
/* ===== SeedMasterPptx.gs ===== */



var SEED_MASTER_PPTX_B64 = [
  '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',
  'm4x5Dysrlpi5zY+IW8poD6al4jgJIVLhIrMhvnqPF2i9IRrbJDtFVEQU/7XMpl/fQFr44DzReL7fh1Uxr/BiJ8AkEfynE20A7b1wz1JfKgJ8TW3SsupEGHoW',
  'mU3LtSb+I2PlueNvRrYBIMwLPIq1WlAtUUXHqnHhINOlg0xXDjJ1HWTquLjInWRqOwh17SBTz0GmvoNMN00zPQkkMIeZSh2rRuMf3mGaDUeTBMXO/suoVHSs',
  'Gk0yVU0/E4msJdvxahZmYtT2aFYDUainJCB7Rq5fFYVSXk86AUdVbHKrXibweXiUv9UtAS/+LYZfUEsHCDMb3emSAQAAmwwAAFBLAQIUABQACAgIAEsN3Vyr',
  'LNrRnQUAAFgbAAAUAAAAAAAAAAAAAAAAAAAAAABwcHQvdGhlbWUvdGhlbWUxLnhtbFBLAQIUABQACAgIAEsN3Vz21EWakwUAAHEbAAAUAAAAAAAAAAAAAAAA',
  'AN8FAABwcHQvdGhlbWUvdGhlbWUyLnhtbFBLAQIUABQACAgIAEsN3Vzhw5ablAUAAM4jAAAhAAAAAAAAAAAAAAAAALQLAABwcHQvbm90ZXNNYXN0ZXJzL25v',
  'dGVzTWFzdGVyMS54bWxQSwECFAAUAAgICABLDd1cCcFWfrIAAAAjAQAALAAAAAAAAAAAAAAAAACXEQAAcHB0L25vdGVzTWFzdGVycy9fcmVscy9ub3Rlc01h',
  'c3RlcjEueG1sLnJlbHNQSwECFAAUAAgICABLDd1cCFk4e3gDAADqCQAAHwAAAAAAAAAAAAAAAACjEgAAcHB0L25vdGVzU2xpZGVzL25vdGVzU2xpZGUxLnht',
  'bFBLAQIUABQACAgIAEsN3VyoK8T3twAAADYBAAAqAAAAAAAAAAAAAAAAAGgWAABwcHQvbm90ZXNTbGlkZXMvX3JlbHMvbm90ZXNTbGlkZTEueG1sLnJlbHNQ',
  'SwECFAAUAAgICABLDd1cFfzToIMEAADiGwAAIQAAAAAAAAAAAAAAAAB3FwAAcHB0L3NsaWRlTGF5b3V0cy9zbGlkZUxheW91dDEueG1sUEsBAhQAFAAICAgA',
  'Sw3dXERQ6zy1AAAANgEAACwAAAAAAAAAAAAAAAAASRwAAHBwdC9zbGlkZUxheW91dHMvX3JlbHMvc2xpZGVMYXlvdXQxLnhtbC5yZWxzUEsBAhQAFAAICAgA',
  'Sw3dXHCTTvIgBAAARxIAACEAAAAAAAAAAAAAAAAAWB0AAHBwdC9zbGlkZUxheW91dHMvc2xpZGVMYXlvdXQyLnhtbFBLAQIUABQACAgIAEsN3VxEUOs8tQAA',
  'ADYBAAAsAAAAAAAAAAAAAAAAAMchAABwcHQvc2xpZGVMYXlvdXRzL19yZWxzL3NsaWRlTGF5b3V0Mi54bWwucmVsc1BLAQIUABQACAgIAEsN3Vwuxozi5AQA',
  'AGoaAAAhAAAAAAAAAAAAAAAAANYiAABwcHQvc2xpZGVMYXlvdXRzL3NsaWRlTGF5b3V0My54bWxQSwECFAAUAAgICABLDd1cRFDrPLUAAAA2AQAALAAAAAAA',
  'AAAAAAAAAAAJKAAAcHB0L3NsaWRlTGF5b3V0cy9fcmVscy9zbGlkZUxheW91dDMueG1sLnJlbHNQSwECFAAUAAgICABLDd1c6iqcXzYFAAAbJAAAIQAAAAAA',
  'AAAAAAAAAAAYKQAAcHB0L3NsaWRlTGF5b3V0cy9zbGlkZUxheW91dDQueG1sUEsBAhQAFAAICAgASw3dXERQ6zy1AAAANgEAACwAAAAAAAAAAAAAAAAAnS4A',
  'AHBwdC9zbGlkZUxheW91dHMvX3JlbHMvc2xpZGVMYXlvdXQ0LnhtbC5yZWxzUEsBAhQAFAAICAgASw3dXCl0K40BBAAAgREAACEAAAAAAAAAAAAAAAAArC8A',
  'AHBwdC9zbGlkZUxheW91dHMvc2xpZGVMYXlvdXQ1LnhtbFBLAQIUABQACAgIAEsN3VxEUOs8tQAAADYBAAAsAAAAAAAAAAAAAAAAAPwzAABwcHQvc2xpZGVM',
  'YXlvdXRzL19yZWxzL3NsaWRlTGF5b3V0NS54bWwucmVsc1BLAQIUABQACAgIAEsN3VwzGhH8+QQAABYbAAAhAAAAAAAAAAAAAAAAAAs1AABwcHQvc2xpZGVM',
  'YXlvdXRzL3NsaWRlTGF5b3V0Ni54bWxQSwECFAAUAAgICABLDd1cRFDrPLUAAAA2AQAALAAAAAAAAAAAAAAAAABTOgAAcHB0L3NsaWRlTGF5b3V0cy9fcmVs',
  'cy9zbGlkZUxheW91dDYueG1sLnJlbHNQSwECFAAUAAgICABLDd1cWf5eSREEAADNEQAAIQAAAAAAAAAAAAAAAABiOwAAcHB0L3NsaWRlTGF5b3V0cy9zbGlk',
  'ZUxheW91dDcueG1sUEsBAhQAFAAICAgASw3dXERQ6zy1AAAANgEAACwAAAAAAAAAAAAAAAAAwj8AAHBwdC9zbGlkZUxheW91dHMvX3JlbHMvc2xpZGVMYXlv',
  'dXQ3LnhtbC5yZWxzUEsBAhQAFAAICAgASw3dXFn5Pf/nBQAApicAACEAAAAAAAAAAAAAAAAA0UAAAHBwdC9zbGlkZUxheW91dHMvc2xpZGVMYXlvdXQ4Lnht',
  'bFBLAQIUABQACAgIAEsN3VxEUOs8tQAAADYBAAAsAAAAAAAAAAAAAAAAAAdHAABwcHQvc2xpZGVMYXlvdXRzL19yZWxzL3NsaWRlTGF5b3V0OC54bWwucmVs',
  'c1BLAQIUABQACAgIAEsN3VyUywec9gMAAOUMAAAhAAAAAAAAAAAAAAAAABZIAABwcHQvc2xpZGVMYXlvdXRzL3NsaWRlTGF5b3V0OS54bWxQSwECFAAUAAgI',
  'CABLDd1cRFDrPLUAAAA2AQAALAAAAAAAAAAAAAAAAABbTAAAcHB0L3NsaWRlTGF5b3V0cy9fcmVscy9zbGlkZUxheW91dDkueG1sLnJlbHNQSwECFAAUAAgI',
  'CABLDd1cs5d6AhMFAADBGwAAIgAAAAAAAAAAAAAAAABqTQAAcHB0L3NsaWRlTGF5b3V0cy9zbGlkZUxheW91dDEwLnhtbFBLAQIUABQACAgIAEsN3VxEUOs8',
  'tQAAADYBAAAtAAAAAAAAAAAAAAAAAM1SAABwcHQvc2xpZGVMYXlvdXRzL19yZWxzL3NsaWRlTGF5b3V0MTAueG1sLnJlbHNQSwECFAAUAAgICABLDd1cB6aV',
  '2I4DAAAWCgAAIgAAAAAAAAAAAAAAAADdUwAAcHB0L3NsaWRlTGF5b3V0cy9zbGlkZUxheW91dDExLnhtbFBLAQIUABQACAgIAEsN3VxEUOs8tQAAADYBAAAt',
  'AAAAAAAAAAAAAAAAALtXAABwcHQvc2xpZGVMYXlvdXRzL19yZWxzL3NsaWRlTGF5b3V0MTEueG1sLnJlbHNQSwECFAAUAAgICABLDd1cWTX9o1wHAABrXwAA',
  'IQAAAAAAAAAAAAAAAADLWAAAcHB0L3NsaWRlTWFzdGVycy9zbGlkZU1hc3RlcjEueG1sUEsBAhQAFAAICAgASw3dXBhneycLAQAAxgcAACwAAAAAAAAAAAAA',
  'AAAAdmAAAHBwdC9zbGlkZU1hc3RlcnMvX3JlbHMvc2xpZGVNYXN0ZXIxLnhtbC5yZWxzUEsBAhQAFAAICAgASw3dXJiNdrQFAwAAOAoAABUAAAAAAAAAAAAA',
  'AAAA22EAAHBwdC9zbGlkZXMvc2xpZGUxLnhtbFBLAQIUABQACAgIAEsN3VxYQG48zwAAAM0BAAAgAAAAAAAAAAAAAAAAACNlAABwcHQvc2xpZGVzL19yZWxz',
  'L3NsaWRlMS54bWwucmVsc1BLAQIUABQACAgIAEsN3VwVAYKMOgEAAM0DAAARAAAAAAAAAAAAAAAAAEBmAABwcHQvcHJlc1Byb3BzLnhtbFBLAQIUABQACAgI',
  'AEsN3VzY+VtqxAEAAPgEAAARAAAAAAAAAAAAAAAAALlnAABwcHQvdmlld1Byb3BzLnhtbFBLAQIUABQACAgIAEsN3Vzf8a51dQMAAFIYAAAUAAAAAAAAAAAA',
  'AAAAALxpAABwcHQvcHJlc2VudGF0aW9uLnhtbFBLAQIUABQACAgIAEsN3VzApJLM/wAAAN0DAAAfAAAAAAAAAAAAAAAAAHNtAABwcHQvX3JlbHMvcHJlc2Vu',
  'dGF0aW9uLnhtbC5yZWxzUEsBAhQAFAAICAgASw3dXABMO5CyAAAALQEAAAsAAAAAAAAAAAAAAAAAv24AAF9yZWxzLy5yZWxzUEsBAhQAFAAICAgASw3dXDMb',
  '3emSAQAAmwwAABMAAAAAAAAAAAAAAAAAqm8AAFtDb250ZW50X1R5cGVzXS54bWxQSwUGAAAAACYAJgDaCwAAfXEAAAAA'
].join("");
/* ===== NativeConvert.gs ===== */



var PLACEHOLDER_PNG_B64_NATIVE =
  'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M8AAAMBAQDJ/pLvAAAAAElFTkSuQmCC';



var MAJIN_DIGIT_GLYPHS = {
  upm: 2048, figTop: 1476, figH: 1496,
  '0': { adv: 1175, d: 'M1079 602Q1079 300 954.0 140.0Q829 -20 588 -20Q350 -20 224.0 137.0Q98 294 95 587V855Q95 1160 221.5 1318.0Q348 1476 586.0 1476.0Q824 1476 950.0 1319.5Q1076 1163 1079 870ZM790 896Q790 1077 740.5 1159.5Q691 1242 586 1242Q484 1242 435.5 1163.5Q387 1085 384 918V564Q384 386 432.5 299.5Q481 213 588 213Q694 213 741.0 296.0Q788 379 790 550Z' },
  '1': { adv: 1175, d: 'M801 0H512V1114L167 1007V1242L770 1458H801Z' },
  '2': { adv: 1175, d: 'M1097 0H99V198L570 700Q667 806 713.5 885.0Q760 964 760 1035Q760 1132 711.0 1187.5Q662 1243 571 1243Q473 1243 416.5 1175.5Q360 1108 360 998H70Q70 1131 133.5 1241.0Q197 1351 313.0 1413.5Q429 1476 576 1476Q801 1476 925.5 1368.0Q1050 1260 1050 1063Q1050 955 994.0 843.0Q938 731 802 582L471 233H1097Z' },
  '3': { adv: 1175, d: 'M393 856H547Q657 856 710.0 911.0Q763 966 763 1057Q763 1145 710.5 1194.0Q658 1243 566 1243Q483 1243 427.0 1197.5Q371 1152 371 1079H82Q82 1193 143.5 1283.5Q205 1374 315.5 1425.0Q426 1476 559 1476Q790 1476 921.0 1365.5Q1052 1255 1052 1061Q1052 961 991.0 877.0Q930 793 831 748Q954 704 1014.5 616.0Q1075 528 1075 408Q1075 214 933.5 97.0Q792 -20 559 -20Q341 -20 202.5 95.0Q64 210 64 399H353Q353 317 414.5 265.0Q476 213 566 213Q669 213 727.5 267.5Q786 322 786 412Q786 630 546 630H393Z' },
  '4': { adv: 1175, d: 'M954 548H1119V315H954V0H665V315H68L55 497L662 1456H954ZM343 548H665V1062L646 1029Z' },
  '5': { adv: 1175, d: 'M142 716 226 1456H1042V1215H463L427 902Q530 957 646 957Q854 957 972.0 828.0Q1090 699 1090 467Q1090 326 1030.5 214.5Q971 103 860.0 41.5Q749 -20 598 -20Q466 -20 353.0 33.5Q240 87 174.5 184.0Q109 281 105 405H391Q400 314 454.5 263.5Q509 213 597 213Q695 213 748.0 283.5Q801 354 801 483Q801 607 740.0 673.0Q679 739 567 739Q464 739 400 685L372 659Z' },
  '6': { adv: 1175, d: 'M883 1471V1233H855Q659 1230 539.5 1131.0Q420 1032 396 856Q512 974 689 974Q879 974 991.0 838.0Q1103 702 1103 480Q1103 338 1041.5 223.0Q980 108 867.5 44.0Q755 -20 613 -20Q383 -20 241.5 140.0Q100 300 100 567V671Q100 908 189.5 1089.5Q279 1271 446.5 1370.5Q614 1470 835 1471ZM601 742Q531 742 474.0 705.5Q417 669 390 609V521Q390 376 447.0 294.5Q504 213 607 213Q700 213 757.5 286.5Q815 360 815 477Q815 596 757.0 669.0Q699 742 601 742Z' },
  '7': { adv: 1175, d: 'M1089 1294 526 0H221L785 1222H61V1456H1089Z' },
  '8': { adv: 1175, d: 'M1048 1069Q1048 963 995.0 881.0Q942 799 849 750Q955 699 1017.0 609.5Q1079 520 1079 399Q1079 205 947.0 92.5Q815 -20 588.0 -20.0Q361 -20 228.0 93.0Q95 206 95 399Q95 520 157.0 610.0Q219 700 324 750Q231 799 178.5 881.0Q126 963 126 1069Q126 1255 250.0 1365.5Q374 1476 587 1476Q799 1476 923.5 1366.5Q1048 1257 1048 1069ZM789 420Q789 515 734.0 572.0Q679 629 586 629Q494 629 439.0 572.5Q384 516 384 420Q384 327 438.0 270.0Q492 213 588 213Q682 213 735.5 268.0Q789 323 789 420ZM759 1055Q759 1140 714.0 1191.5Q669 1243 587 1243Q506 1243 461.0 1193.0Q416 1143 416 1055Q416 968 461.0 915.0Q506 862 588.0 862.0Q670 862 714.5 915.0Q759 968 759 1055Z' },
  '9': { adv: 1175, d: 'M775 582Q662 471 511 471Q318 471 202.0 603.5Q86 736 86 961Q86 1104 148.5 1223.5Q211 1343 323.0 1409.5Q435 1476 575 1476Q719 1476 831.0 1404.0Q943 1332 1005.0 1197.0Q1067 1062 1068 888V781Q1068 417 887.0 209.0Q706 1 374 -13L303 -14V227L367 228Q744 245 775 582ZM582 692Q652 692 702.5 728.0Q753 764 779 815V934Q779 1081 723.0 1162.0Q667 1243 573 1243Q486 1243 430.0 1163.5Q374 1084 374 964Q374 845 428.5 768.5Q483 692 582 692Z' },
};



function zipPptxWithMedia(parts, media, filename) {
  var blobs = [];
  var ordered = Object.keys(parts).sort(function (a, b) {
    if (a === '[Content_Types].xml') return -1;
    if (b === '[Content_Types].xml') return 1;
    return 0;
  });
  for (var i = 0; i < ordered.length; i++) {
    blobs.push(Utilities.newBlob(parts[ordered[i]], 'application/xml', ordered[i]));
  }
  for (var j = 0; j < (media || []).length; j++) {
    var m = media[j];
    
    
    if (m.blob) {
      try {
        blobs.push(m.blob.setName(m.path).setContentType(m.mime || 'image/png'));
        continue;
      } catch (eBlob) { 
        blobs.push(Utilities.newBlob(Utilities.base64Decode(PLACEHOLDER_PNG_B64_NATIVE), m.mime || 'image/png', m.path));
        continue;
      }
    }
    var bytes = null;
    try {
      if (m.dataUrl) {
        bytes = Utilities.base64Decode(String(m.dataUrl).replace(/^data:[^,]*,/, ''));
      } else if (m.url) {
        if (!/^https?:\/\//i.test(m.url)) {
          var fileName = m.url.split('/').pop();
          var files = DriveApp.getFilesByName(fileName);
          if (files.hasNext()) bytes = files.next().getBlob().getBytes();
        } else {
          var fopts = { muteHttpExceptions: true, followRedirects: true,
            headers: { 'User-Agent': 'Mozilla/5.0 (compatible; Google Apps Script)' } };
          var resp = UrlFetchApp.fetch(m.url, fopts);
          if (resp.getResponseCode() >= 200 && resp.getResponseCode() < 300) bytes = resp.getContent();
        }
      }
    } catch (e) { bytes = null; }
    if (!bytes) bytes = Utilities.base64Decode(PLACEHOLDER_PNG_B64_NATIVE);
    blobs.push(Utilities.newBlob(bytes, m.mime || 'image/png', m.path));
  }
  var zip = Utilities.zip(blobs, filename);
  return zip.setContentType('application/vnd.openxmlformats-officedocument.presentationml.presentation');
}




function _slideElementIdsRest_(presentationId) {
  try {
    if (!(typeof Slides !== 'undefined' && Slides && Slides.Presentations)) return null;
    var r = Slides.Presentations.get(presentationId, 'slides.objectId,slides.pageElements.objectId');
    if (!r || !r.slides) return null;
    return r.slides.map(function (s) {
      return (s.pageElements || []).map(function (el) { return el.objectId; });
    });
  } catch (e) { return null; }
}


function _npT_(label, fn) {
  if (typeof perfTime === 'function') return perfTime(label, fn);
  return fn();
}


function convertPptxToSlides(pptxBlob, name, existingFileId) {
  var meta = { name: name, mimeType: 'application/vnd.google-apps.presentation' };
  var file;
  if (existingFileId) {
    
    
    file = Drive.Files.update(meta, existingFileId, pptxBlob, { fields: 'id,name,webViewLink' });
    if (!file || !file.id) throw new Error('既存デッキへの変換上書き(Files.update)に失敗しました。');
  } else {
    file = Drive.Files.create(meta, pptxBlob, { fields: 'id,name,webViewLink' });
    if (!file || !file.id) throw new Error('Google スライドへの変換に失敗しました。');
  }
  return file;
}

function _escXmlNative(s) {
  return String(s == null ? '' : s)
    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}







function _waitForConvertedSlides_(fileId, wantSlideCount) {
  var hasSlidesApi = (typeof Slides !== 'undefined' && Slides && Slides.Presentations);
  if (!hasSlidesApi) return null; 
  
  var waits = [60, 90, 150, 250, 400, 650, 800, 900, 900]; 
  for (var i = 0; i < waits.length; i++) {
    try {
      var chk = Slides.Presentations.get(fileId, 'slides.objectId,slides.pageElements.objectId');
      if (chk && chk.slides && chk.slides.length === wantSlideCount) {
        return chk.slides.map(function (sl) {
          return (sl.pageElements || []).map(function (el) { return el.objectId; });
        });
      }
    } catch (e) {  }
    Utilities.sleep(waits[i]);
  }
  return null;
}



var __PPTX_MIME = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
function exportSlidesAsPptxBlob(fileId) {
  
  var __t1 = Date.now();
  try {
    var f = DriveApp.getFileById(fileId);
    var b = f.getAs(__PPTX_MIME);
    if (b && b.getBytes().length > 0) {
      if (typeof perfAdd === 'function') perfAdd('tplExport(getAs)', Date.now() - __t1);
      return b.setName('template_export.pptx');
    }
  } catch (e1) {  }
  if (typeof perfAdd === 'function') perfAdd('tplExport(getAs失敗)', Date.now() - __t1);
  
  
  var __t15 = Date.now();
  try {
    var durl = 'https://docs.google.com/presentation/d/' + encodeURIComponent(fileId) + '/export/pptx';
    var dresp = UrlFetchApp.fetch(durl, {
      headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
      followRedirects: true,
      muteHttpExceptions: true
    });
    if (dresp.getResponseCode() === 200) {
      var dblob = dresp.getBlob();
      var dct = String(dblob.getContentType() || '');
      if (dblob.getBytes().length > 0 && dct.indexOf('presentationml') >= 0) {
        if (typeof perfAdd === 'function') perfAdd('tplExport(docs)', Date.now() - __t15);
        return dblob.setContentType(__PPTX_MIME).setName('template_export.pptx');
      }
    }
  } catch (eDocs) {  }
  if (typeof perfAdd === 'function') perfAdd('tplExport(docs失敗)', Date.now() - __t15);
  var __t2 = Date.now();
  
  var url = 'https://www.googleapis.com/drive/v3/files/' + encodeURIComponent(fileId)
    + '/export?mimeType=' + encodeURIComponent(__PPTX_MIME)
    + '&supportsAllDrives=true';
  var resp = UrlFetchApp.fetch(url, {
    headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
    muteHttpExceptions: true
  });
  var code = resp.getResponseCode();
  if (code !== 200) throw new Error('テンプレPPTXエクスポート失敗(HTTP ' + code + '): ' + resp.getContentText().slice(0, 300));
  if (typeof perfAdd === 'function') perfAdd('tplExport(rest)', Date.now() - __t2);
  return resp.getBlob().setName('template_export.pptx');
}



var __MAJIN_CACHE_FOLDER_NAME = 'まじん式スライド_キャッシュ';
function _getMajinCacheFolder_() {
  var props = PropertiesService.getScriptProperties();
  var fid = props.getProperty('majinCacheFolderId');
  if (fid) {
    try { return DriveApp.getFolderById(fid); } catch (e) {  }
  }
  var folder = DriveApp.createFolder(__MAJIN_CACHE_FOLDER_NAME);
  props.setProperty('majinCacheFolderId', folder.getId());
  return folder;
}







function _dropEmbeddedFontsFromMap_(tmap) {
  var fontNames = Object.keys(tmap).filter(function (n) { return /^ppt\/fonts\//i.test(n); });
  if (!fontNames.length) return { tmap: tmap, removed: 0 };

  var out = {};
  Object.keys(tmap).forEach(function (n) { if (!/^ppt\/fonts\//i.test(n)) out[n] = tmap[n]; });
  function put(name, xml) { out[name] = Utilities.newBlob(xml, 'application/xml', name); }

  if (out['ppt/presentation.xml']) {
    put('ppt/presentation.xml', out['ppt/presentation.xml'].getDataAsString()
      .replace(/<p:embeddedFontLst>[\s\S]*?<\/p:embeddedFontLst>/g, '')
      .replace(/<p:embeddedFontLst\s*\/>/g, ''));
  }
  if (out['ppt/_rels/presentation.xml.rels']) {
    put('ppt/_rels/presentation.xml.rels', out['ppt/_rels/presentation.xml.rels'].getDataAsString()
      .replace(/<Relationship\b[^>]*Target="[^"]*fonts\/[^"]*"[^>]*\/>/g, ''));
  }
  if (out['[Content_Types].xml']) {
    put('[Content_Types].xml', out['[Content_Types].xml'].getDataAsString()
      .replace(/<Default[^>]*Extension="fntdata"[^>]*\/>/gi, '')
      .replace(/<Override[^>]*PartName="\/ppt\/fonts\/[^"]*"[^>]*\/>/gi, ''));
  }
  return { tmap: out, removed: fontNames.length };
}






function _seedPartsFromEmbedded_() {
  try {
    if (typeof SEED_MASTER_PPTX_B64 !== 'string' || !SEED_MASTER_PPTX_B64) return null;
    var entries = Utilities.unzip(Utilities.newBlob(
      Utilities.base64Decode(SEED_MASTER_PPTX_B64), 'application/zip', 'seed.zip'));
    var m = {};
    for (var i = 0; i < entries.length; i++) m[entries[i].getName()] = entries[i];
    return m['ppt/presentation.xml'] ? m : null;
  } catch (e) {
    Logger.log('[native][seed] 埋め込み種PPTXの取り出しに失敗→Driveの種デッキへ: ' + ((e && e.message) || e));
    return null;
  }
}



var __TPL_MTIME_CACHE = {};
function _templateLastUpdatedMs_(fileId) {
  if (!fileId) return null;
  if (Object.prototype.hasOwnProperty.call(__TPL_MTIME_CACHE, fileId)) return __TPL_MTIME_CACHE[fileId];
  var v = null;
  try { v = DriveApp.getFileById(fileId).getLastUpdated().getTime(); } catch (e) { v = null; }
  __TPL_MTIME_CACHE[fileId] = v;
  return v;
}








function _slimTemplatePartsForCache_(tmap) {
  var m = _dropEmbeddedFontsFromMap_(tmap).tmap;

  var nums = [];
  Object.keys(m).forEach(function (nm) {
    var mm = nm.match(/^ppt\/slides\/slide(\d+)\.xml$/);
    if (mm) nums.push(parseInt(mm[1], 10));
  });
  nums.sort(function (a, b) { return a - b; });
  var keepSlide = {};
  nums.slice(0, TEMPLATE_CATEGORY_SLIDE_COUNT).forEach(function (num) { keepSlide[num] = true; });

  var out = {};
  Object.keys(m).forEach(function (nm) {
    var sm = nm.match(/^ppt\/slides\/(?:_rels\/)?slide(\d+)\.xml(?:\.rels)?$/);
    if (sm && !keepSlide[parseInt(sm[1], 10)]) return; 
    out[nm] = m[nm];
  });

  var used = {};
  Object.keys(out).forEach(function (nm) {
    if (!/\.rels$/i.test(nm)) return;
    var rx; try { rx = out[nm].getDataAsString(); } catch (e) { return; }
    (rx.match(/Target="[^"]*\/media\/[^"\/]+"/g) || []).forEach(function (t) {
      var b = t.match(/\/media\/([^"\/]+)"/); if (b) used['ppt/media/' + b[1]] = true;
    });
  });
  var kept = {};
  Object.keys(out).forEach(function (nm) {
    if (/^ppt\/media\//.test(nm) && !used[nm]) return; 
    kept[nm] = out[nm];
  });
  return kept;
}


var TEMPLATE_CACHE_VERSION = 2;















function _tplCacheMetaKey_(templateId) { return 'majin_tplcache_' + templateId; }
function _tplCacheFileName_(templateId) { return 'tplcache_' + templateId + '.pptx'; }
function _readTplCacheMeta_(templateId) {
  try {
    var raw = PropertiesService.getScriptProperties().getProperty(_tplCacheMetaKey_(templateId));
    return raw ? JSON.parse(raw) : null;
  } catch (e) { return null; }
}
function _writeTplCacheMeta_(templateId, fileId, mtime, builtAt) {
  var meta = { fileId: fileId, mtime: mtime, v: TEMPLATE_CACHE_VERSION, builtAt: builtAt || Date.now(), inTrash: true };
  try { PropertiesService.getScriptProperties().setProperty(_tplCacheMetaKey_(templateId), JSON.stringify(meta)); } catch (e) {}
  return meta;
}

function _deleteDriveFileHard_(fileId) {
  if (!fileId) return false;
  try {
    var resp = UrlFetchApp.fetch('https://www.googleapis.com/drive/v3/files/' + encodeURIComponent(fileId) + '?supportsAllDrives=true', {
      method: 'delete', headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() }, muteHttpExceptions: true
    });
    var code = resp.getResponseCode();
    if (code === 204 || code === 404) return true;
  } catch (e) {  }
  try { DriveApp.getFileById(fileId).setTrashed(true); return true; } catch (e2) { return false; }
}




function _tplCacheDescription_(templateId, mtime, builtAt) {
  return JSON.stringify({ majinTplCache: 1, tpl: templateId, mtime: mtime, v: TEMPLATE_CACHE_VERSION, builtAt: builtAt || Date.now() });
}
function _parseTplCacheDescription_(desc) {
  try {
    var d = JSON.parse(String(desc || ''));
    return (d && d.majinTplCache) ? d : null;
  } catch (e) { return null; }
}



function _findTplCacheFilesInDrive_(templateId) {
  var name = _tplCacheFileName_(templateId);
  var found = [];
  try {
    var q = "name = '" + name.replace(/'/g, "\\'") + "' and 'me' in owners and (trashed = true or trashed = false)";
    var url = 'https://www.googleapis.com/drive/v3/files?q=' + encodeURIComponent(q)
      + '&fields=' + encodeURIComponent('files(id,name,description,size,trashed,modifiedTime)')
      + '&pageSize=50&orderBy=modifiedTime desc&supportsAllDrives=true';
    var resp = UrlFetchApp.fetch(url, { headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() }, muteHttpExceptions: true });
    if (resp.getResponseCode() === 200) {
      var files = (JSON.parse(resp.getContentText()) || {}).files || [];
      for (var i = 0; i < files.length; i++) {
        found.push({ id: files[i].id, desc: _parseTplCacheDescription_(files[i].description), trashed: !!files[i].trashed });
      }
      return found;
    }
    Logger.log('[native][tplcache] Drive検索に失敗(HTTP ' + resp.getResponseCode() + ')→DriveAppで探します');
  } catch (eRest) { Logger.log('[native][tplcache] Drive検索に失敗(' + ((eRest && eRest.message) || eRest) + ')→DriveAppで探します'); }
  try {
    var it = DriveApp.getFilesByName(name); 
    while (it.hasNext()) {
      var f = it.next();
      var d = null; try { d = _parseTplCacheDescription_(f.getDescription()); } catch (eD) {}
      found.push({ id: f.getId(), desc: d, trashed: (function () { try { return f.isTrashed(); } catch (e) { return null; } })() });
    }
  } catch (eApp) { Logger.log('[native][tplcache] DriveAppの名前検索も失敗: ' + ((eApp && eApp.message) || eApp)); }
  return found;
}





function ensureTemplateCache_(templateId, opts) {
  opts = opts || {};
  var t0 = Date.now();
  var out = { status: 'error', templateId: templateId, reason: '' };
  function toMap(bytes) {
    var entries = Utilities.unzip(Utilities.newBlob(bytes, 'application/zip', 't.zip'));
    var m = {};
    for (var i = 0; i < entries.length; i++) m[entries[i].getName()] = entries[i];
    return m;
  }
  var mtime = _templateLastUpdatedMs_(templateId); 
  if (mtime === null) {
    out.reason = 'テンプレの最終更新時刻を取得できません（削除済み／アクセス権なし）';
    if (opts.wantParts) throw new Error(out.reason);
    out.elapsedMs = Date.now() - t0;
    return out;
  }
  out.tplMtime = mtime;

  
  function adopt(fileId, builtAt, via) {
    var cachedBytes = null;
    try {
      var cf = DriveApp.getFileById(fileId); 
      if (opts.wantParts) cachedBytes = _npT_('tpl:getbytes', function () { return cf.getBlob().getBytes(); });
      else cf.getSize(); 
    } catch (eGone) { return null; }
    out.status = 'hit'; out.via = via; out.cacheFileId = fileId; out.builtAt = builtAt || null;
    if (opts.wantParts) {
      if (typeof perfCount === 'function') perfCount('tpl:srcKB', Math.round(cachedBytes.length / 1024));
      Logger.log('[native][tplcache] ヒット(' + via + '): tpl=' + templateId + ' cache=' + fileId);
      out.parts = _npT_('tpl:unzip', function () { return toMap(cachedBytes); });
    }
    out.elapsedMs = Date.now() - t0;
    return out;
  }

  
  var meta = _readTplCacheMeta_(templateId);
  var reason = null;
  if (!meta || !meta.fileId) reason = 'キャッシュ未作成';
  else if (meta.v !== TEMPLATE_CACHE_VERSION) reason = 'キャッシュの形式が旧版';
  else if (meta.mtime !== mtime) reason = 'テンプレートがキャッシュ作成後に更新された';
  if (!reason) {
    var r1 = adopt(meta.fileId, meta.builtAt, 'meta');
    if (r1) return r1;
    reason = 'キャッシュファイルが見つからない（完全に削除された等）';
  }
  out.reason = reason;

  
  
  var driveHits = null;
  try {
    driveHits = _npT_('tpl:cacheFind', function () { return _findTplCacheFilesInDrive_(templateId); });
    for (var i = 0; i < driveHits.length; i++) {
      var h = driveHits[i];
      if (!h.desc || h.desc.v !== TEMPLATE_CACHE_VERSION || h.desc.mtime !== mtime) continue; 
      if (meta && meta.fileId === h.id) continue; 
      var r2 = adopt(h.id, h.desc.builtAt, 'drive');
      if (r2) {
        _writeTplCacheMeta_(templateId, h.id, mtime, h.desc.builtAt);
        Logger.log('[native][tplcache] 別のまじん式のキャッシュを共有して使います: tpl=' + templateId + ' cache=' + h.id);
        r2.reason = reason + ' → 自分のドライブで使えるキャッシュを見つけました';
        return r2;
      }
    }
  } catch (eFind) { Logger.log('[native][tplcache] ドライブ探索に失敗(作り直しへ): ' + ((eFind && eFind.message) || eFind)); }

  
  var lock = null, locked = false;
  try { lock = LockService.getScriptLock(); locked = lock.tryLock(opts.lockWaitMs || 0); } catch (eL) { locked = false; }
  if (!locked && !opts.wantParts) { out.status = 'busy'; out.elapsedMs = Date.now() - t0; return out; }
  try {
    if (locked && !opts.wantParts) {
      
      var meta2 = _readTplCacheMeta_(templateId);
      if (meta2 && meta2.fileId && meta2.v === TEMPLATE_CACHE_VERSION && meta2.mtime === mtime) {
        var r3 = adopt(meta2.fileId, meta2.builtAt, 'meta');
        if (r3) return r3;
      }
    }
    var blob = _npT_('tpl:export', function () { return exportSlidesAsPptxBlob(templateId); });
    var rawBytes = _npT_('tpl:getbytes', function () { return blob.getBytes(); });
    if (typeof perfCount === 'function') perfCount('tpl:srcKB', Math.round(rawBytes.length / 1024));
    var slimMap = _slimTemplatePartsForCache_(_npT_('tpl:unzip', function () { return toMap(rawBytes); }));
    if (locked) {
      try {
        _npT_('tpl:cacheSave', function () {
          var folder = _getMajinCacheFolder_();
          var name = _tplCacheFileName_(templateId);
          
          var kill = {};
          if (meta && meta.fileId) kill[meta.fileId] = true;
          (driveHits || []).forEach(function (h) { kill[h.id] = true; });
          try { var old = folder.getFilesByName(name); while (old.hasNext()) kill[old.next().getId()] = true; } catch (eOld) {}
          Object.keys(kill).forEach(function (fid) { try { _deleteDriveFileHard_(fid); } catch (eDel) {} });

          var builtAt = Date.now();
          var blobs = Object.keys(slimMap).map(function (k) { return slimMap[k]; });
          var saved = folder.createFile(Utilities.zip(blobs, name).setContentType(__PPTX_MIME).setName(name));
          
          try { saved.setDescription(_tplCacheDescription_(templateId, mtime, builtAt)); } catch (eDesc) { Logger.log('[native][tplcache] 説明欄を書けず(共有は効かないが動作に影響なし): ' + eDesc.message); }
          try { saved.setTrashed(true); } catch (eTrash) { Logger.log('[native][tplcache] ゴミ箱へ移せず(フォルダに残す): ' + eTrash.message); }
          _writeTplCacheMeta_(templateId, saved.getId(), mtime, builtAt);
          out.cacheFileId = saved.getId(); out.builtAt = builtAt;
          Logger.log('[native][tplcache] 軽量版を作成(ゴミ箱内): tpl=' + templateId + ' cache=' + saved.getId() + ' 理由=' + reason);
        });
      } catch (eSave) { Logger.log('[native][tplcache] 保存失敗(生成は継続): ' + eSave.message); out.reason = reason + ' / 保存失敗: ' + eSave.message; }
    } else {
      Logger.log('[native][tplcache] 別の実行が作成中のため保存せず、その場の書き出しで続行: tpl=' + templateId);
    }
    out.status = 'rebuilt'; out.via = 'rebuilt';
    if (opts.wantParts) out.parts = slimMap;
    out.elapsedMs = Date.now() - t0;
    return out;
  } catch (eBuild) {
    out.status = 'error'; out.reason = reason + ' / 作成失敗: ' + ((eBuild && eBuild.message) || String(eBuild));
    if (opts.wantParts) throw eBuild;
    out.elapsedMs = Date.now() - t0;
    return out;
  } finally {
    if (locked) { try { lock.releaseLock(); } catch (eR) {} }
  }
}


function _getTemplatePartsCached_(templateId) {
  var r = ensureTemplateCache_(templateId, { wantParts: true, lockWaitMs: 0 });
  if (!r || !r.parts) throw new Error('テンプレの部品を取得できません: ' + ((r && r.reason) || (r && r.status) || '不明'));
  return r.parts;
}



function warmTemplateCache(urlOrId) {
  try {
    var id = _extractTemplateIdNative(urlOrId);
    if (!id) return { ok: false, status: 'error', reason: 'テンプレートのIDを読み取れません' };
    var r = ensureTemplateCache_(id, { wantParts: false, lockWaitMs: 200 });
    r.ok = (r.status === 'hit' || r.status === 'rebuilt');
    return r;
  } catch (e) {
    return { ok: false, status: 'error', reason: (e && e.message) || String(e) };
  }
}



function unzipPptxToMap(pptxBlob) {
  var zip = Utilities.newBlob(pptxBlob.getBytes(), 'application/zip', 'template.zip');
  var entries = Utilities.unzip(zip);
  var map = {};
  for (var i = 0; i < entries.length; i++) { map[entries[i].getName()] = entries[i]; }
  return map;
}



function probeTemplateOoxml(templateId) {
  var out = { ok: false, id: templateId };
  
  try { out.effectiveUser = Session.getEffectiveUser().getEmail(); } catch (e0) {}
  try {
    var __df = DriveApp.getFileById(templateId);
    out.driveName = __df.getName();
    try { out.driveOwner = __df.getOwner() ? __df.getOwner().getEmail() : '(none)'; } catch (eo) { out.driveOwner = 'ownerErr:' + eo.message; }
    out.driveMime = __df.getMimeType();
  } catch (eDrive) { out.driveErr = (eDrive && eDrive.message) || String(eDrive); }
  try {
    var blob = exportSlidesAsPptxBlob(templateId);
    out.blobBytes = blob.getBytes().length;
    var map = unzipPptxToMap(blob);
    var names = Object.keys(map);
    out.entryCount = names.length;
    out.masters = names.filter(function (n) { return /^ppt\/slideMasters\/slideMaster\d+\.xml$/.test(n); });
    out.layouts = names.filter(function (n) { return /^ppt\/slideLayouts\/slideLayout\d+\.xml$/.test(n); });
    out.themes = names.filter(function (n) { return /^ppt\/theme\/theme\d+\.xml$/.test(n); });
    out.mediaCount = names.filter(function (n) { return /^ppt\/media\//.test(n); }).length;
    out.media = names.filter(function (n) { return /^ppt\/media\//.test(n); }).slice(0, 10);
    
    out.slideCount = names.filter(function (n) { return /^ppt\/slides\/slide\d+\.xml$/.test(n); }).length;
    out.mediaBytes = {};
    var __tot = 0;
    names.filter(function (n) { return /^ppt\/media\//.test(n); }).forEach(function (n) {
      var b = map[n].getBytes().length; out.mediaBytes[n] = b; __tot += b;
    });
    out.mediaTotalBytes = __tot;
    
    var __all = names.map(function (n) {
      var b = 0; try { b = map[n].getBytes().length; } catch (e) {}
      return { n: n, b: b };
    }).sort(function (a, c) { return c.b - a.b; });
    out.topEntries = __all.slice(0, 20).map(function (x) { return x.n + ' = ' + x.b; });
    out.allEntriesBytes = __all.reduce(function (a, c) { return a + c.b; }, 0);
    out.refsByRel = {};
    names.filter(function (n) { return /\.rels$/i.test(n); }).forEach(function (rn) {
      var rx = ''; try { rx = map[rn].getDataAsString(); } catch (e) { return; }
      var ms = (rx.match(/Target="[^"]*\/media\/[^"\/]+"/g) || []).map(function (t) {
        var b = t.match(/\/media\/([^"\/]+)"/); return b ? b[1] : null;
      }).filter(Boolean);
      if (ms.length) out.refsByRel[rn] = ms;
    });
    
    if (map['ppt/theme/theme1.xml']) {
      var t = map['ppt/theme/theme1.xml'].getDataAsString();
      var m = t.match(/<a:theme[^>]*name="([^"]*)"/);
      out.theme1Name = m ? m[1] : '(none)';
      var clr = t.match(/<a:clrScheme[^>]*name="([^"]*)"/);
      out.clrSchemeName = clr ? clr[1] : '(none)';
    }
    
    if (map['ppt/slideMasters/slideMaster1.xml']) {
      var ms = map['ppt/slideMasters/slideMaster1.xml'].getDataAsString();
      out.masterLayoutRefs = (ms.match(/<p:sldLayoutId\b/g) || []).length;
      var mn = ms.match(/<p:cSld[^>]*name="([^"]*)"/);
      out.master1Name = mn ? mn[1] : '(none)';
    }
    out.ok = true;
  } catch (e) { out.error = (e && e.message) || String(e); out.stack = (e && e.stack) ? String(e.stack).slice(0, 500) : ''; }
  return out;
}


function _sectionLayoutNative(opts) {
  opts = opts || {};
  var hasCfg = (typeof CONFIG !== 'undefined' && CONFIG && CONFIG.POS_PX);
  var P = hasCfg ? CONFIG.POS_PX : null;
  var ghostColor = (hasCfg && CONFIG.COLORS && CONFIG.COLORS.ghost_gray) ? CONFIG.COLORS.ghost_gray : '#9aa7c7';
  return {
    ghost: (P && P.sectionSlide && P.sectionSlide.ghostNum) || { left: 35, top: 120, width: 400, height: 200 },
    title: (P && P.sectionSlide && P.sectionSlide.title) || { left: 55, top: 230, width: 840, height: 80 },
    bar: (P && P.bottomBar) || { left: 0, top: 534, width: 960, height: 6 },
    
    
    
    fam: _escXmlNative(opts.fontFamily || (hasCfg && CONFIG.FONTS && CONFIG.FONTS.family) || 'Noto Sans JP'),
    titleSize: opts.titleSize || (hasCfg && CONFIG.FONTS && CONFIG.FONTS.sizes && CONFIG.FONTS.sizes.sectionTitle) || 38,
    titleColor: _escXmlNative(opts.titleColor || (hasCfg && CONFIG.COLORS && CONFIG.COLORS.text_primary) || '#333333'),
    ghostColor: _escXmlNative(opts.ghostColor || ghostColor),
    ghostSize: opts.ghostSize || (hasCfg && CONFIG.FONTS && CONFIG.FONTS.sizes && CONFIG.FONTS.sizes.ghostNum) || 180,
    gradStart: _escXmlNative(opts.gradientStart || '#4285F4'),
    gradEnd: _escXmlNative(opts.gradientEnd || '#ff52df'),
    ghostTopAlpha: (typeof opts.ghostTopAlpha === 'number' && !isNaN(opts.ghostTopAlpha)) ? Math.max(0, Math.min(1, opts.ghostTopAlpha)) : 0.30,
    ghostBottomAlpha: (typeof opts.ghostBottomAlpha === 'number' && !isNaN(opts.ghostBottomAlpha)) ? Math.max(0, Math.min(1, opts.ghostBottomAlpha)) : 0.10
  };
}


function buildSectionDecoSvgMajin(num, opts) {
  var L = _sectionLayoutNative(opts);
  var n2 = ('0' + num).slice(-2);
  var gcx = L.ghost.left + L.ghost.width / 2;
  var gfs = Math.round(L.ghost.height * 0.95);
  var gby = Math.round(L.ghost.top + L.ghost.height * 0.82);
  
  var bgRect = (opts && opts.transparentBg) ? '' : '<rect x="0" y="0" width="960" height="540" fill="#FFFFFF"/>';
  return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540">'
    + '<defs><linearGradient id="barN' + num + '" x1="0" y1="0" x2="1" y2="0">'
    + '<stop offset="0" stop-color="' + L.gradStart + '"/><stop offset="1" stop-color="' + L.gradEnd + '"/></linearGradient></defs>'
    + bgRect
    + '<text x="' + gcx + '" y="' + gby + '" font-size="' + gfs + '" font-family="' + L.fam + '" font-weight="bold" fill="' + L.ghostColor + '" opacity="' + L.ghostTopAlpha + '" text-anchor="middle">' + n2 + '</text>'
    + '<rect x="' + L.bar.left + '" y="' + L.bar.top + '" width="' + L.bar.width + '" height="' + L.bar.height + '" fill="url(#barN' + num + ')"/>';
}


function buildSectionFullSvgMajin(num, title, opts) {
  var L = _sectionLayoutNative(opts);
  var ty = Math.round(L.title.top + L.title.height / 2 + L.titleSize * 0.35);
  return buildSectionDecoSvgMajin(num, opts)
    + '<text x="' + L.title.left + '" y="' + ty + '" font-size="' + L.titleSize + '" font-family="' + L.fam + '" font-weight="bold" fill="' + L.titleColor + '">' + _escXmlNative(title) + '</text>'
    + '</svg>';
}

function buildSectionDecoSvgMajinClosed(num, opts) {
  return buildSectionDecoSvgMajin(num, opts) + '</svg>';
}


function _chooseTemplateLayout(tmap) {
  var names = Object.keys(tmap).filter(function (n) { return /^ppt\/slideLayouts\/slideLayout\d+\.xml$/.test(n); });
  for (var i = 0; i < names.length; i++) {
    try { if (/type="blank"/.test(tmap[names[i]].getDataAsString())) return names[i].split('/').pop(); } catch (e) {}
  }
  return names.length ? names[0].split('/').pop() : 'slideLayout1.xml';
}

function _extractTemplateIdNative(urlOrId) {
  if (!urlOrId) return null;
  var s = String(urlOrId).trim();
  var m = s.match(/\/d\/([-\w]{20,})/);
  if (m) return m[1];
  if (/^[-\w]{20,}$/.test(s)) return s;
  return null;
}
function _guessMimeByExtNative(name) {
  var ext = (name.split('.').pop() || '').toLowerCase();
  if (ext === 'png') return 'image/png';
  if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg';
  if (ext === 'gif') return 'image/gif';
  if (ext === 'emf') return 'image/x-emf';
  if (ext === 'wmf') return 'image/x-wmf';
  if (ext === 'svg') return 'image/svg+xml';
  if (ext === 'bmp') return 'image/bmp';
  if (ext === 'tiff' || ext === 'tif') return 'image/tiff';
  return 'application/octet-stream';
}




function _maxCNvPrId(xml) {
  var max = 1, re = /<p:cNvPr\b[^>]*?\bid="(\d+)"/g, m;
  while ((m = re.exec(xml)) !== null) { var v = parseInt(m[1], 10); if (v > max) max = v; }
  return max;
}


function _extractMajinShapesXml(slideXml) {
  if (!slideXml) return '';
  var GP = '</p:grpSpPr>';
  var gp = slideXml.indexOf(GP);              
  var sp = slideXml.lastIndexOf('</p:spTree>'); 
  if (gp < 0 || sp < 0 || sp <= gp) return '';
  return slideXml.substring(gp + GP.length, sp);
}


function _remapShapeIds(shapesXml, offset) {
  if (!shapesXml || !offset || offset <= 0) return shapesXml;
  return shapesXml.replace(/(<p:cNvPr\b[^>]*?\bid=")(\d+)(")/g, function (s, a, d, c) {
    return a + (parseInt(d, 10) + offset) + c;
  });
}


function _injectShapesIntoSpTree(tplSlideXml, shapesXml) {
  if (!shapesXml) return tplSlideXml;
  var pos = tplSlideXml.lastIndexOf('</p:spTree>');
  if (pos < 0) return tplSlideXml; 
  return tplSlideXml.slice(0, pos) + shapesXml + tplSlideXml.slice(pos);
}




var TEMPLATE_CATEGORY_SLIDE_COUNT = 4;

function _buildMergedTemplateSlides(tmap, built, slideTypes, n) {
  
  var nums = [];
  Object.keys(tmap).forEach(function (nm) {
    var m = nm.match(/^ppt\/slides\/slide(\d+)\.xml$/);
    if (m) nums.push(parseInt(m[1], 10));
  });
  nums.sort(function (a, b) { return a - b; });
  if (!nums.length) throw new Error('テンプレにスライドがありません(マージ不可)');

  var tplSlides = nums.map(function (num) {
    var relName = 'ppt/slides/_rels/slide' + num + '.xml.rels';
    return {
      xml: tmap['ppt/slides/slide' + num + '.xml'].getDataAsString(),
      relsXml: tmap[relName] ? tmap[relName].getDataAsString()
        : (XML_DECL + '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>')
    };
  });

  function at(i) { return tplSlides[i] || null; }
  var byCat = {
    title:   at(0) || tplSlides[0],
    section: at(1) || at(0) || tplSlides[0],
    content: at(2) || at(0) || tplSlides[0],
    closing: at(3) || at(2) || at(0) || tplSlides[0]
  };

  var slidesXml = [], slideRels = [];
  for (var k = 0; k < n; k++) {
    var cat = (typeof getTemplateCategoryForType === 'function')
      ? getTemplateCategoryForType(slideTypes && slideTypes[k]) : 'content';
    var tpl = byCat[cat] || byCat.content || tplSlides[0];
    var majin = _extractMajinShapesXml(built.parts['ppt/slides/slide' + (k + 1) + '.xml']);
    majin = _remapShapeIds(majin, _maxCNvPrId(tpl.xml)); 
    slidesXml.push(_injectShapesIntoSpTree(tpl.xml, majin));
    slideRels.push(tpl.relsXml);
  }
  return { slidesXml: slidesXml, slideRels: slideRels };
}




var _SEED_LAYOUT_ORDER = ['TITLE', 'SECTION_HEADER', 'TITLE_AND_BODY', 'TITLE_AND_TWO_COLUMNS',
  'TITLE_ONLY', 'ONE_COLUMN_TEXT', 'MAIN_POINT', 'SECTION_TITLE_AND_DESCRIPTION',
  'CAPTION_ONLY', 'BIG_NUMBER', 'BLANK'];

function _reorderSeedLayoutsNative(finalParts) {
  try {
    var masterName = 'ppt/slideMasters/slideMaster1.xml';
    var relsName = 'ppt/slideMasters/_rels/slideMaster1.xml.rels';
    var masterXml = finalParts[masterName], relsXml = finalParts[relsName];
    if (!masterXml || !relsXml) return false;
    
    var relMap = {};
    (relsXml.match(/<Relationship\b[^>]*\/>/g) || []).forEach(function (rel) {
      var idM = rel.match(/Id="(rId\d+)"/);
      var tM = rel.match(/Target="[^"]*\/slideLayouts\/(slideLayout\d+\.xml)"/);
      if (idM && tM) relMap[idM[1]] = 'ppt/slideLayouts/' + tM[1];
    });
    var listM = masterXml.match(/<p:sldLayoutIdLst>([\s\S]*?)<\/p:sldLayoutIdLst>/);
    if (!listM) return false;
    var entries = listM[1].match(/<p:sldLayoutId\b[^>]*\/>/g) || [];
    if (entries.length < 2) return false;
    var rank = function (entry) {
      var ridM = entry.match(/r:id="(rId\d+)"/);
      if (!ridM) return 900;
      var lf = relMap[ridM[1]];
      if (!lf || !finalParts[lf]) return 900;
      var nameM = finalParts[lf].match(/<p:cSld\b[^>]*name="([^"]*)"/);
      var nm = nameM ? nameM[1] : '';
      var i = _SEED_LAYOUT_ORDER.indexOf(nm);
      return i < 0 ? 900 : i; 
    };
    var ranked = entries.map(function (e, idx) { return { e: e, r: rank(e), idx: idx }; });
    ranked.sort(function (a, b) { return (a.r - b.r) || (a.idx - b.idx); });
    var newList = '<p:sldLayoutIdLst>' + ranked.map(function (x) { return x.e; }).join('') + '</p:sldLayoutIdLst>';
    finalParts[masterName] = masterXml.replace(/<p:sldLayoutIdLst>[\s\S]*?<\/p:sldLayoutIdLst>/, newList);
    return true;
  } catch (e) { return false; } 
}


function buildTemplatePptxWithNativeSlides(templateId, nativeSvgs, opts) {
  opts = opts || {};
  var preserveSlides = !!opts.preserveSlides;       
  var slideTypes = opts.slideTypes || [];           
  
  
  
  
  var tmap = null;
  if (opts.useEmbeddedSeed) tmap = _npT_('seed:embedded', function () { return _seedPartsFromEmbedded_(); });
  if (!tmap) tmap = _dropEmbeddedFontsFromMap_(_getTemplatePartsCached_(templateId)).tmap;
  if (!tmap['ppt/presentation.xml']) throw new Error('テンプレPPTXに presentation.xml がありません');

  
  var presTpl = tmap['ppt/presentation.xml'].getDataAsString();
  var szM = presTpl.match(/<p:sldSz[^>]*cx="(\d+)"[^>]*cy="(\d+)"/);
  var slideSizeEmu = szM ? { w: parseInt(szM[1], 10), h: parseInt(szM[2], 10) } : NATIVE_BASE_SLIDE_SIZE_EMU;

  var built = _npT_('tpl:svgparts', function () {
    return buildMultiSlidePptxParts(nativeSvgs, { aspect: '16:9', slideSizeEmu: slideSizeEmu });
  });
  var n = built.slideCount;
  var layoutTarget = _chooseTemplateLayout(tmap);
  
  var slideMerge = preserveSlides
    ? _npT_('tpl:merge', function () { return _buildMergedTemplateSlides(tmap, built, slideTypes, n); })
    : null;

  
  
  
  
  
  var keepMedia = {};
  function collectMediaFromRelsXml(rx) {
    var ms = rx.match(/Target="[^"]*\/media\/[^"\/]+"/g) || [];
    ms.forEach(function (t) { var b = t.match(/\/media\/([^"\/]+)"/); if (b) keepMedia['ppt/media/' + b[1]] = true; });
  }
  Object.keys(tmap).forEach(function (rn) {
    if (!/\.rels$/i.test(rn)) return;
    if (/\/slides\/_rels\//.test(rn)) return; 
    var rx;
    try { rx = tmap[rn].getDataAsString(); } catch (e) { return; }
    collectMediaFromRelsXml(rx);
  });
  if (slideMerge) slideMerge.slideRels.forEach(collectMediaFromRelsXml);

  
  var finalParts = {};
  var media = [];
  var names = Object.keys(tmap);
  var __tPart = Date.now();
  for (var j = 0; j < names.length; j++) {
    var nm = names[j];
    if (/^ppt\/slides\//.test(nm)) continue; 
    if (/^ppt\/fonts\//i.test(nm)) continue;  
    if (/\.(xml|rels)$/i.test(nm)) {
      finalParts[nm] = tmap[nm].getDataAsString();
    } else if (/^ppt\/media\//.test(nm)) {
      if (!keepMedia[nm]) continue; 
      var bb = tmap[nm];
      var mime = _guessMimeByExtNative(nm);
      media.push({ path: nm, mime: mime, blob: bb }); 
    } else {
      
      var bb2 = tmap[nm];
      var mime2 = _guessMimeByExtNative(nm);
      media.push({ path: nm, mime: mime2, blob: bb2 }); 
    }
  }

  if (typeof perfAdd === 'function') perfAdd('tpl:partition', Date.now() - __tPart);

  
  
  if (!preserveSlides) { try { _reorderSeedLayoutsNative(finalParts); } catch (eRo) {} }

  
  
  
  for (var k = 0; k < n; k++) {
    if (preserveSlides && slideMerge) {
      finalParts['ppt/slides/slide' + (k + 1) + '.xml'] = slideMerge.slidesXml[k];
      finalParts['ppt/slides/_rels/slide' + (k + 1) + '.xml.rels'] = slideMerge.slideRels[k];
    } else {
      finalParts['ppt/slides/slide' + (k + 1) + '.xml'] = built.parts['ppt/slides/slide' + (k + 1) + '.xml'];
      finalParts['ppt/slides/_rels/slide' + (k + 1) + '.xml.rels'] =
        XML_DECL + '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
        + '<Relationship Id="rId1" Type="' + REL + '/slideLayout" Target="../slideLayouts/' + layoutTarget + '"/></Relationships>';
    }
  }

  
  var relsXml = finalParts['ppt/_rels/presentation.xml.rels'];
  var allRels = relsXml.match(/<Relationship\b[^>]*\/>/g) || [];
  var keptRels = [], maxId = 0;
  for (var r = 0; r < allRels.length; r++) {
    var rel = allRels[r];
    var idM = rel.match(/Id="rId(\d+)"/);
    if (idM) maxId = Math.max(maxId, parseInt(idM[1], 10));
    var typeM = rel.match(/Type="([^"]*)"/);
    if (typeM && /\/slide$/.test(typeM[1])) continue; 
    keptRels.push(rel);
  }
  var slideRIds = [], slideRelXmls = [];
  for (var k2 = 0; k2 < n; k2++) {
    var rid = 'rId' + (maxId + 1 + k2);
    slideRIds.push(rid);
    slideRelXmls.push('<Relationship Id="' + rid + '" Type="' + REL + '/slide" Target="slides/slide' + (k2 + 1) + '.xml"/>');
  }
  finalParts['ppt/_rels/presentation.xml.rels'] =
    XML_DECL + '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
    + keptRels.join('') + slideRelXmls.join('') + '</Relationships>';

  
  var presXml = finalParts['ppt/presentation.xml'];
  var sldIds = '';
  for (var k3 = 0; k3 < n; k3++) sldIds += '<p:sldId id="' + (256 + k3) + '" r:id="' + slideRIds[k3] + '"/>';
  var newSldIdLst = '<p:sldIdLst>' + sldIds + '</p:sldIdLst>';
  if (/<p:sldIdLst[\s\S]*?<\/p:sldIdLst>/.test(presXml)) {
    presXml = presXml.replace(/<p:sldIdLst[\s\S]*?<\/p:sldIdLst>/, newSldIdLst);
  } else if (/<p:sldIdLst\s*\/>/.test(presXml)) {
    presXml = presXml.replace(/<p:sldIdLst\s*\/>/, newSldIdLst);
  } else {
    presXml = presXml.replace(/(<\/p:sldMasterIdLst>)/, '$1' + newSldIdLst);
  }
  finalParts['ppt/presentation.xml'] = presXml;

  
  var ct = finalParts['[Content_Types].xml'];
  ct = ct.replace(/<Override PartName="\/ppt\/slides\/slide\d+\.xml"[^>]*\/>/g, '');
  var slideOv = '';
  for (var k4 = 0; k4 < n; k4++) slideOv += '<Override PartName="/ppt/slides/slide' + (k4 + 1) + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>';
  ct = ct.replace(/<\/Types>/, slideOv + '</Types>');
  finalParts['[Content_Types].xml'] = ct;

  return {
    blob: _npT_('tpl:zip', function () {
      var z = zipPptxWithMedia(finalParts, media, 'majin_template_native.pptx');
      if (typeof perfCount === 'function') perfCount('tpl:outKB', Math.round(z.getBytes().length / 1024));
      return z;
    }),
    slideCount: n, layoutTarget: layoutTarget, mediaCount: media.length, shapeCount: built.shapeCount,
    merged: preserveSlides
  };
}



function tplTestNativeOnTemplate(templateId, keep) {
  var out = { ok: false, id: templateId };
  try {
    var svgs = [
      buildSectionFullSvgMajin(1, 'テンプレ土台テスト 章1', { transparentBg: true }),
      buildSectionFullSvgMajin(2, 'テンプレ土台テスト 章2', { transparentBg: true }),
      buildSectionFullSvgMajin(3, 'テンプレ土台テスト 章3', { transparentBg: true })
    ];
    var __t0 = Date.now();
    var r = buildTemplatePptxWithNativeSlides(templateId, svgs);
    out.buildMs = Date.now() - __t0;
    out.layoutTarget = r.layoutTarget; out.mediaCount = r.mediaCount; out.shapeCount = r.shapeCount;
    var __t1 = Date.now();
    var file = convertPptxToSlides(r.blob, 'テンプレ土台ネイティブPoC ' + Utilities.formatDate(new Date(), 'Asia/Tokyo', 'HHmmss'), null);
    out.convertMs = Date.now() - __t1;
    out.deckId = file.id;
    out.url = 'https://docs.google.com/presentation/d/' + file.id + '/edit';
    
    for (var t = 0; t < 6; t++) {
      try { if (typeof Slides !== 'undefined' && Slides.Presentations) { var chk = Slides.Presentations.get(file.id); if (chk && chk.slides && chk.slides.length === svgs.length) break; } } catch (e) {}
      Utilities.sleep(700);
    }
    var p = SlidesApp.openById(file.id);
    out.masters = p.getMasters().length;
    out.layouts = p.getLayouts().length;
    out.layoutNames = p.getLayouts().map(function (l) { try { return l.getLayoutName(); } catch (e) { return '?'; } });
    out.slides = p.getSlides().length;
    out.elementsPerSlide = p.getSlides().map(function (s) { return s.getPageElements().length; });
    if (!keep) { try { Drive.Files.remove(file.id); } catch (e) { try { DriveApp.getFileById(file.id).setTrashed(true); } catch (e2) {} } out.trashed = true; }
    out.ok = true;
  } catch (e) { out.error = (e && e.message) || String(e); out.stack = (e && e.stack) ? String(e.stack).slice(0, 600) : ''; }
  return out;
}


function createSectionDeckNative(slideData, opts) {
  opts = opts || {};
  var out = { ok: false, ts: new Date().toISOString() };
  try {
    var sections = (slideData || []).filter(function (d) { return d && d.type === 'section'; });
    if (!sections.length) throw new Error('type:"section" のスライドがありません');
    var svgs = sections.map(function (d, i) {
      var no = (typeof d.sectionNo === 'number' && isFinite(d.sectionNo)) ? d.sectionNo : (i + 1);
      return buildSectionFullSvgMajin(no, d.title || '', opts);
    });
    var t0 = Date.now();
    var built = buildMultiSlidePptxParts(svgs, { aspect: '16:9' });
    out.buildMs = Date.now() - t0;
    t0 = Date.now();
    var blob = zipPptxWithMedia(built.parts, [], 'majin_section_native.pptx');
    out.zipMs = Date.now() - t0;
    t0 = Date.now();
    var file = convertPptxToSlides(blob, (opts.title || 'まじんsectionネイティブ') + '_' + built.slideCount);
    out.convertMs = Date.now() - t0;
    out.slideCount = built.slideCount;
    out.shapeCount = built.shapeCount;
    out.warnings = built.warnings;
    out.url = 'https://docs.google.com/presentation/d/' + file.id + '/edit';
    out.ok = true;
  } catch (e) { out.error = e.message + (e.stack ? (' ' + e.stack) : ''); }
  Logger.log(JSON.stringify(out, null, 2));
  return out;
}


function testSectionNative() {
  var sample = [
    { type: 'section', sectionNo: 1, title: 'イントロダクション' },
    { type: 'section', sectionNo: 2, title: '提供価値とソリューション' },
    { type: 'section', sectionNo: 3, title: '導入効果と次のアクション' }
  ];
  return createSectionDeckNative(sample, {});
}




var NATIVE_BASE_SLIDE_SIZE_EMU = { w: 9144000, h: 5143500 };


function _resolveSectionNumNative(data, sectionCounter) {
  if (data && typeof data.sectionNo === 'number' && isFinite(data.sectionNo)) return Number(data.sectionNo);
  var m = String((data && data.title) || '').match(/^\s*(\d+)[\.．]/);
  return m ? Number(m[1]) : sectionCounter;
}


function _ghostInnerMajin(num, opts) {
  var L = _sectionLayoutNative(opts);
  var n2 = String(num).padStart(2, '0'); 
  var G = MAJIN_DIGIT_GLYPHS;
  
  
  var unitsPerPt = 960 / (NATIVE_BASE_SLIDE_SIZE_EMU.w / 12700);
  var emUnits = L.ghostSize * unitsPerPt;        
  var s = emUnits / G.upm;                        
  var capUnits = G.figTop * s;                    
  var advTotal = 0;
  for (var i = 0; i < n2.length; i++) { advTotal += (G[n2.charAt(i)] || G['0']).adv * s; }
  var penX = L.ghost.left + (L.ghost.width - advTotal) / 2;       
  var baseY = (L.ghost.top + L.ghost.height / 2) + capUnits / 2;  
  var glyphs = '';
  for (var j = 0; j < n2.length; j++) {
    var g = G[n2.charAt(j)] || G['0'];
    
    glyphs += '<g transform="translate(' + penX.toFixed(2) + ',' + baseY.toFixed(2) + ') scale(' + s.toFixed(5) + ',' + (-s).toFixed(5) + ')">'
      + '<path d="' + g.d + '" fill="url(#ghostGradN)"/></g>';
    penX += g.adv * s;
  }
  var gc = L.ghostColor;
  
  return '<defs><linearGradient id="ghostGradN" x1="0" y1="0" x2="0" y2="1">'
    + '<stop offset="0" stop-color="' + gc + '" stop-opacity="' + L.ghostTopAlpha + '"/>'
    + '<stop offset="1" stop-color="' + gc + '" stop-opacity="' + L.ghostBottomAlpha + '"/></linearGradient></defs>'
    + glyphs;
}


function buildSectionGhostOnlySvgMajin(num, opts) {
  return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540">' + _ghostInnerMajin(num, opts) + '</svg>';
}





var NATIVE_NO_BOTTOMBAR_TYPES = { section: true, closing: true, fullImage: true };
function slideHasBottomBarNative(type, settings) {
  return !!(settings && settings.showBottomBar) && !NATIVE_NO_BOTTOMBAR_TYPES[type];
}


function _bottomBarInnerMajin(settings, vbRect) {
  var x = vbRect.x, y = vbRect.y, w = vbRect.w, h = vbRect.h;
  if (settings && settings.enableGradient) {
    var gs = _escXmlNative(settings.gradientStart || '#4285F4'), ge = _escXmlNative(settings.gradientEnd || '#ff52df'); 
    return '<defs><linearGradient id="barGradDecor" x1="0" y1="0" x2="1" y2="0">'
      + '<stop offset="0" stop-color="' + gs + '"/><stop offset="1" stop-color="' + ge + '"/></linearGradient></defs>'
      + '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" fill="url(#barGradDecor)"/>';
  }
  var pc = _escXmlNative((settings && settings.primaryColor) || '#4285F4'); 
  return '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" fill="' + pc + '"/>';
}



function _bottomBarVbRectMajin(settings) {
  var P = (typeof CONFIG !== 'undefined' && CONFIG && CONFIG.POS_PX && CONFIG.POS_PX.bottomBar)
    || { left: 0, top: 534, width: 960, height: 6 };
  if (typeof createLayoutManager === 'function') {
    try {
      var pageWpt = NATIVE_BASE_SLIDE_SIZE_EMU.w / 12700, pageHpt = NATIVE_BASE_SLIDE_SIZE_EMU.h / 12700;
      var kx = 960 / pageWpt, ky = 540 / pageHpt;
      var margins = (typeof getContentMarginsPx === 'function') ? getContentMarginsPx(settings) : null;
      var r = createLayoutManager(pageWpt, pageHpt, margins).getRect('bottomBar');
      return { x: r.left * kx, y: r.top * ky, w: r.width * kx, h: r.height * ky };
    } catch (e) {  }
  }
  return { x: P.left, y: P.top, w: P.width, h: P.height };
}





var _TYPE_UNDERLINE_KEY = {
  content: 'contentSlide', cards: 'cardsSlide', compare: 'cardsSlide', process: 'processSlide',
  processList: 'processSlide', timeline: 'timelineSlide', diagram: 'diagramSlide', cycle: 'contentSlide',
  headerCards: 'cardsSlide', table: 'tableSlide', progress: 'progressSlide', quote: 'quoteSlide',
  kpi: 'kpiSlide', bulletCards: 'contentSlide', agenda: 'processSlide', faq: 'contentSlide',
  statsCompare: 'compareSlide', barCompare: 'compareSlide', triangle: 'triangleSlide',
  pyramid: 'pyramidSlide', flowChart: 'flowChartSlide', stepUp: 'stepUpSlide',
  imageText: 'imageTextSlide', integratedContent: 'contentSlide', splitImage: 'contentSlide'
};


function _titleForUnderlineNative(d) {
  if (!d) return '';
  var t = d.title;
  if (d.type === 'kpi') t = t || '主要指標';
  else if (d.type === 'quote') t = t || '引用';
  else if (d.type === 'faq') t = t || 'よくあるご質問';
  return t || '';
}


function _underlineVbForSlideNative(d, settings, L) {
  if (!(settings && settings.showTitleUnderline) || !d || !L) return null;
  var key = _TYPE_UNDERLINE_KEY[d.type];
  if (!key) return null;
  var title = _titleForUnderlineNative(d);
  if (!title || !String(title).trim()) return null;
  var uRect;
  try { uRect = L.getRect(key + '.titleUnderline'); } catch (e) { return null; }
  if (!uRect || !uRect.width) return null;
  var initSize = (typeof CONFIG !== 'undefined' && CONFIG && CONFIG.FONTS && CONFIG.FONTS.sizes && CONFIG.FONTS.sizes.contentTitle) || 24;
  var preW = (typeof d._title_widthPt === 'number' && d._title_widthPt > 0) ? d._title_widthPt
    : ((typeof estimateTextWidthPt === 'function') ? estimateTextWidthPt(title, initSize) : String(title).length * initSize * 0.6);
  var maxW = L.contentRight_pt - uRect.left - L.pxToPt(25);
  var finalWpt = Math.min(preW + 10, maxW);
  if (!(finalWpt > 0)) return null;
  var pageWpt = NATIVE_BASE_SLIDE_SIZE_EMU.w / 12700, pageHpt = NATIVE_BASE_SLIDE_SIZE_EMU.h / 12700;
  var kx = 960 / pageWpt, ky = 540 / pageHpt;
  return { x: uRect.left * kx, y: uRect.top * ky, w: finalWpt * kx, h: uRect.height * ky };
}


function _underlineInnerMajin(settings, vb) {
  var x = vb.x, y = vb.y, w = vb.w, h = vb.h, r = h / 2;
  if (settings && settings.enableGradient) {
    var gs = _escXmlNative(settings.gradientStart || '#4285F4'), ge = _escXmlNative(settings.gradientEnd || '#ff52df'); 
    return '<defs><linearGradient id="ulGradDecor" x1="0" y1="0" x2="1" y2="0">'
      + '<stop offset="0" stop-color="' + gs + '"/><stop offset="1" stop-color="' + ge + '"/></linearGradient></defs>'
      + '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" rx="' + r + '" ry="' + r + '" fill="url(#ulGradDecor)"/>';
  }
  var pc = _escXmlNative((settings && settings.primaryColor) || '#4285F4'); 
  return '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" rx="' + r + '" ry="' + r + '" fill="' + pc + '"/>';
}



function _extractSvgViewBox(svg) {
  var m = svg.match(/viewBox\s*=\s*["']\s*([\d.\-]+)[\s,]+([\d.\-]+)[\s,]+([\d.\-]+)[\s,]+([\d.\-]+)\s*["']/i);
  if (m) { var w = parseFloat(m[3]), h = parseFloat(m[4]); if (w > 0 && h > 0) return { w: w, h: h }; }
  var mw = svg.match(/<svg[^>]*\bwidth\s*=\s*["']([\d.]+)/i);
  var mh = svg.match(/<svg[^>]*\bheight\s*=\s*["']([\d.]+)/i);
  if (mw && mh) { var w2 = parseFloat(mw[1]), h2 = parseFloat(mh[1]); if (w2 > 0 && h2 > 0) return { w: w2, h: h2 }; }
  return null;
}

function _extractSvgInner(svg) {
  return String(svg).replace(/^[\s\S]*?<svg[^>]*>/i, '').replace(/<\/svg\s*>\s*$/i, '');
}

function _imageTextChartRectPt(d, settings, L, vbW, vbH, info) {
  if (!d || d.type !== 'imageText' || !L) return null;
  var dy = (d.subhead && String(d.subhead).trim()) ? L.pxToPt(36) : 0;
  var imagePosition = (d.imagePosition === 'right') ? 'right' : 'left';
  var textKey = (imagePosition === 'left') ? 'imageTextSlide.rightText' : 'imageTextSlide.leftText';
  var ta;
  try { ta = L.getRect(textKey); } catch (e) { return null; }
  if (!ta || !ta.width) return null;
  var taTop = ta.top + dy; 
  var maxW = L.pxToPt(400);
  var maxH = (info === 'chart') ? L.pxToPt(400) : L.pxToPt(300);
  var aspect = vbW / vbH;
  var w = Math.min(maxW, maxH * aspect);
  var h = Math.min(maxW / aspect, maxH);
  var pageCenterX = L.contentCenterX_pt;
  if (typeof pageCenterX !== 'number') return null;
  var textCenterX = ta.left + ta.width / 2;
  var textCenterY = taTop + ta.height / 2;
  var mirroredCenterX = pageCenterX + (pageCenterX - textCenterX);
  var mirroredCenterY = textCenterY;
  return { left: mirroredCenterX - w / 2, top: mirroredCenterY - h / 2, width: w, height: h };
}

function _chartInnerMajin(d, settings, L) {
  if (!d || !L) return '';
  var img = d.image;
  if (!img || typeof img !== 'object') return '';
  var info = img.info;
  if (info !== 'chart' && info !== 'svg_code') return '';      
  
  var __reason = function (reason) {
    try { if (typeof perfCount === 'function') perfCount('chart:skip:' + reason, 1); } catch (e) {}
    try { Logger.log('[nativeChart] skip(' + reason + ') type=' + (d && d.type) + ' info=' + info + ' caption=' + !!(d && d.imageCaption)); } catch (e) {}
    return '';
  };
  var svg = img.nativeSvg;
  if (!svg || typeof svg !== 'string' || svg.indexOf('<svg') < 0) return __reason('noNativeSvg'); 
  if (/<image\b/i.test(svg)) return __reason('rasterImage');                                       
  var vb = _extractSvgViewBox(svg);
  if (!vb) return __reason('noViewBox');
  
  var rectPt = _imageTextChartRectPt(d, settings, L, vb.w, vb.h, info);
  if (!rectPt || !(rectPt.width > 0) || !(rectPt.height > 0)) return __reason('unsupportedType'); 
  var pageWpt = NATIVE_BASE_SLIDE_SIZE_EMU.w / 12700, pageHpt = NATIVE_BASE_SLIDE_SIZE_EMU.h / 12700;
  var kx = 960 / pageWpt, ky = 540 / pageHpt;
  var x = rectPt.left * kx, y = rectPt.top * ky, w = rectPt.width * kx, h = rectPt.height * ky;
  var sx = w / vb.w, sy = h / vb.h;
  var inner = _extractSvgInner(svg);
  if (!inner || !inner.trim()) return __reason('emptyInner');
  try { img._nativeComposited = true; } catch (e) {}
  try { if (typeof perfCount === 'function') perfCount('chart:native', 1); } catch (e) {}
  return '<g transform="translate(' + _roundCoord(x) + ',' + _roundCoord(y) + ') scale(' + _roundCoord(sx) + ',' + _roundCoord(sy) + ')">' + inner + '</g>';
}


function _roundCoord(v) { return Math.round(v * 1000) / 1000; }


function _triangleArrowsInnerMajin(d, settings, L) {
  if (!L || !d) return '';
  if (typeof computeTriangleGeometry !== 'function' || typeof _triangleArrowLocalShape !== 'function') return '';
  if (typeof TRIANGLE_ARROW_CONFIGS === 'undefined') return '';
  var geo;
  try { geo = computeTriangleGeometry(d, L); } catch (e) { return ''; }
  if (!geo || !geo.items || geo.items.length !== 3 || !geo.positions || !geo.selectedAdjustments) return '';
  var pageWpt = NATIVE_BASE_SLIDE_SIZE_EMU.w / 12700, pageHpt = NATIVE_BASE_SLIDE_SIZE_EMU.h / 12700;
  var kx = 960 / pageWpt, ky = 540 / pageHpt;
  var p = geo.positions, sel = geo.selectedAdjustments;
  var color = (typeof CONFIG !== 'undefined' && CONFIG && CONFIG.COLORS && CONFIG.COLORS.ghost_gray) || '#BDBDBD';
  var CFG = TRIANGLE_ARROW_CONFIGS;
  
  
  var arrows = [
    { cfg: CFG.topLeft,  adj: sel.topLeft,  a: p[2], b: p[0] },
    { cfg: CFG.topRight, adj: sel.topRight, a: p[0], b: p[1] },
    { cfg: CFG.bottom,   adj: sel.bottom,   a: p[1], b: p[2] }
  ];
  var frag = '';
  for (var i = 0; i < arrows.length; i++) {
    var ar = arrows[i];
    if (!ar.adj || !ar.a || !ar.b) continue;
    var shp = _triangleArrowLocalShape(ar.cfg);
    var bb = shp.bbox;
    var span = Math.max(bb.maxX - bb.minX, bb.maxY - bb.minY);
    if (!(span > 0)) continue;
    var scale = ar.adj.sizePt / span; 
    var bcx = (bb.minX + bb.maxX) / 2, bcy = (bb.minY + bb.maxY) / 2;
    
    var ctX = (ar.a.x + ar.b.x) / 2 + ar.adj.offsetXPt;
    var ctY = (ar.a.y + ar.b.y) / 2 + ar.adj.offsetYPt;
    
    var sX = scale * kx, sY = scale * ky; 
    var tX = (ctX - bcx * scale) * kx, tY = (ctY - bcy * scale) * ky;
    frag += '<g transform="translate(' + _roundCoord(tX) + ',' + _roundCoord(tY) + ') scale(' + _roundCoord(sX) + ',' + _roundCoord(sY) + ')">'
      + '<path d="' + shp.stemD + '" fill="none" stroke="' + color + '" stroke-width="' + TRIANGLE_ARROW_STROKE_WIDTH + '" stroke-linecap="butt" stroke-linejoin="bevel"/>'
      + '<path d="' + TRIANGLE_ARROWHEAD_PATH_D + '" transform="translate(' + shp.startX + ',' + shp.startY + ') rotate(' + _roundCoord(shp.angleDeg) + ')" fill="' + color + '"/>'
      + '</g>';
  }
  return frag;
}


function _blankSlideSvgNative() {
  return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540"></svg>';
}


function _createNativeBasePresentation(slideData, settings, finalName, existingDeckId) {
  
  var hasCfg = (typeof CONFIG !== 'undefined' && CONFIG);
  var opts = {
    fontFamily: (settings && settings.fontFamily) || (hasCfg && CONFIG.FONTS && CONFIG.FONTS.family) || 'Noto Sans JP',
    ghostColor: (hasCfg && CONFIG.COLORS && CONFIG.COLORS.ghost_gray) || undefined,
    ghostTopAlpha: (settings && settings.ghostTopAlpha != null && !isNaN(parseFloat(settings.ghostTopAlpha))) ? parseFloat(settings.ghostTopAlpha) : 0.30,
    ghostBottomAlpha: (settings && settings.ghostBottomAlpha != null && !isNaN(parseFloat(settings.ghostBottomAlpha))) ? parseFloat(settings.ghostBottomAlpha) : 0.10
  };
  
  var wantDecor = !!(settings && settings.nativeDecorMode === true);
  
  var wantChart = !!(settings && settings.nativeChartMode === true);
  var barFrag = wantDecor ? _bottomBarInnerMajin(settings, _bottomBarVbRectMajin(settings)) : '';
  
  var L = null;
  if ((wantDecor || wantChart) && typeof createLayoutManager === 'function') {
    try {
      var margins = (typeof getContentMarginsPx === 'function') ? getContentMarginsPx(settings) : null;
      L = createLayoutManager(NATIVE_BASE_SLIDE_SIZE_EMU.w / 12700, NATIVE_BASE_SLIDE_SIZE_EMU.h / 12700, margins);
    } catch (e) { L = null; }
  }
  var sectionCounter = 0;
  var pageCounter = 0; 
  var svgs = (slideData || []).map(function (d) {
    var inner = '';
    if (d && d.type !== 'title' && d.type !== 'closing') pageCounter++;
    
    
    
    
    var __noDecor = !!(d && d._suppressHeaderFooter);
    var __noUnderline = __noDecor || ((pageCounter === 1) && d && (d.type === 'splitImage' || d.type === 'integratedContent'));
    if (wantDecor && d && !__noDecor && slideHasBottomBarNative(d.type, settings)) inner += barFrag;  
    if (wantDecor && d && !__noUnderline) {                                                
      var uvb = _underlineVbForSlideNative(d, settings, L);
      if (uvb) inner += _underlineInnerMajin(settings, uvb);
    }
    if (wantDecor && d && d.type === 'triangle') inner += _triangleArrowsInnerMajin(d, settings, L); 
    if (wantChart && d) inner += _chartInnerMajin(d, settings, L);                          
    if (d && d.type === 'section') {                                                       
      sectionCounter++;
      inner += _ghostInnerMajin(_resolveSectionNumNative(d, sectionCounter), opts);
    }
    if (!inner) return _blankSlideSvgNative();
    return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540">' + inner + '</svg>';
  });
  if (!svgs.length) throw new Error('slideData が空です');

  
  
  
  
  
  
  
  var svgHash = _nativeDeckFingerprint_(svgs, settings, slideData);

  
  
  
  
  
  
  if (existingDeckId) {
    try {
      var rec = _loadNativeDecoRecord(existingDeckId);
      if (rec && rec.h === svgHash && rec.d && rec.d.length === svgs.length) {
        
        
        
        var removedByRest = false;
        try {
          var idsNow = _npT_('skip:ids', function () { return _slideElementIdsRest_(existingDeckId); });
          if (idsNow && idsNow.length === svgs.length) {
            var delReqs = [];
            for (var di = 0; di < idsNow.length; di++) {
              var keepRest = {};
              (rec.d[di] || []).forEach(function (id) { keepRest[id] = true; });
              idsNow[di].forEach(function (id) { if (!keepRest[id]) delReqs.push({ deleteObject: { objectId: id } }); });
            }
            if (delReqs.length) {
              _npT_('skip:delete', function () { Slides.Presentations.batchUpdate({ requests: delReqs }, existingDeckId); });
            }
            removedByRest = true;
          }
        } catch (eDel) {
          removedByRest = false;
          Logger.log('[native] 一括削除に失敗→SlidesAppの逐次削除へ: ' + ((eDel && eDel.message) || eDel));
        }
        var presSkip = SlidesApp.openById(existingDeckId);
        var slidesSkip = presSkip.getSlides();
        if (slidesSkip.length === svgs.length) {
          if (!removedByRest) {
            for (var si = 0; si < slidesSkip.length; si++) {
              var keepSet = {};
              (rec.d[si] || []).forEach(function (id) { keepSet[id] = true; });
              var els = slidesSkip[si].getPageElements();
              for (var ei = els.length - 1; ei >= 0; ei--) {
                if (!keepSet[els[ei].getObjectId()]) { try { els[ei].remove(); } catch (e) {} }
              }
            }
          }
          
          
          
          try { presSkip.setName(finalName); } catch (eName) {}
          
          
          
          
          try { _saveNativeDecoRecord(existingDeckId, rec); } catch (eTouch) {}
          if (typeof perfAdd === 'function') perfAdd('nativeBase(skipConvert)', 1);
          Logger.log('[native] 装飾不変→Drive変換スキップ(本文のみ更新): id=' + existingDeckId);
          return presSkip;
        }
      }
    } catch (eSkip) {
      Logger.log('[native] スキップ判定に失敗、通常変換にフォールバック: ' + eSkip.message);
    }
  }

  
  
  
  
  
  
  
  
  
  if ((settings && settings.templateUrl) || (settings && settings.seedTemplateId) || (settings && settings.useEmbeddedSeed)) {
    var __isRealTpl = !!(settings && settings.templateUrl);
    
    var __useEmbeddedSeed = !__isRealTpl && !!(settings && settings.useEmbeddedSeed);
    var __tplId = __isRealTpl ? _extractTemplateIdNative(settings.templateUrl)
      : (__useEmbeddedSeed ? '' : String(settings.seedTemplateId).trim());
    if (__tplId || __useEmbeddedSeed) {
      var __tplTypes = (slideData || []).map(function (d) { return d && d.type; });
      var __tplBuilt = _npT_('tpl:build', function () {
        return buildTemplatePptxWithNativeSlides(__tplId, svgs,
          { preserveSlides: __isRealTpl, slideTypes: __tplTypes, useEmbeddedSeed: __useEmbeddedSeed });
      });
      var fileTpl = _npT_('tpl:convert', function () { return convertPptxToSlides(__tplBuilt.blob, finalName, existingDeckId); });
      
      var __idsTpl = _npT_('tpl:wait', function () { return _waitForConvertedSlides_(fileTpl.id, svgs.length); });
      var presTpl = _npT_('tpl:open', function () { return SlidesApp.openById(fileTpl.id); });
      if (settings.driveFolderId && settings.driveFolderId.trim()) {
        try { DriveApp.getFileById(presTpl.getId()).moveTo(DriveApp.getFolderById(settings.driveFolderId.trim())); } catch (e) {}
      }
      Logger.log('[native][templateMode] テンプレ土台で生成: tpl=' + __tplId + ' layout=' + __tplBuilt.layoutTarget + ' media=' + __tplBuilt.mediaCount + ' id=' + presTpl.getId());
      try {
        _npT_('tpl:record', function () {
          var decoSlidesTpl = __idsTpl 
            || _slideElementIdsRest_(presTpl.getId())
            || presTpl.getSlides().map(function (s) { return s.getPageElements().map(function (el) { return el.getObjectId(); }); });
          _saveNativeDecoRecord(presTpl.getId(), { h: svgHash, d: decoSlidesTpl, dm: wantDecor });
        });
      } catch (eRec) {}
      return presTpl;
    }
  }

  var built = _npT_('sm:svgparts', function () {
    return buildMultiSlidePptxParts(svgs, { aspect: '16:9', slideSizeEmu: NATIVE_BASE_SLIDE_SIZE_EMU });
  });

  
  
  
  
  if (settings && settings.nativeSingleMaster) {
    try {
      var __themeKey = 'ppt/theme/theme1.xml';
      var __themeName = (settings.nativeThemeName && String(settings.nativeThemeName).trim()) ? String(settings.nativeThemeName).trim() : 'まじんスライド';
      if (built.parts[__themeKey]) {
        built.parts[__themeKey] = built.parts[__themeKey].replace(/name="Office Theme"/, 'name="' + __themeName + '"');
      }
      var blobSM = _npT_('sm:zip', function () { return zipPptxWithMedia(built.parts, [], 'majin_native_base.pptx'); });
      var fileSM = _npT_('sm:convert', function () { return convertPptxToSlides(blobSM, finalName, existingDeckId); }); 
      
      var __idsSM = _npT_('sm:wait', function () { return _waitForConvertedSlides_(fileSM.id, (slideData || []).length); });
      var presSM = _npT_('sm:open', function () { return SlidesApp.openById(fileSM.id); });
      if (settings.driveFolderId && settings.driveFolderId.trim()) {
        try { DriveApp.getFileById(presSM.getId()).moveTo(DriveApp.getFolderById(settings.driveFolderId.trim())); } catch (e) {}
      }
      Logger.log('[native][singleMaster] 単一マスターで生成: slides=' + built.slideCount + ' shapes=' + built.shapeCount
        + ' theme="' + __themeName + '" id=' + presSM.getId() + ' inPlace=' + (!!existingDeckId));
      try {
        _npT_('sm:record', function () {
          var decoSlidesSM = __idsSM
            || _slideElementIdsRest_(presSM.getId())
            || presSM.getSlides().map(function (s) { return s.getPageElements().map(function (el) { return el.getObjectId(); }); });
          _saveNativeDecoRecord(presSM.getId(), { h: svgHash, d: decoSlidesSM, dm: wantDecor });
        });
      } catch (eRec) { Logger.log('[native][singleMaster] 装飾ID記録失敗(生成は正常): ' + eRec.message); }
      return presSM;
    } catch (eSM) {
      Logger.log('[native][singleMaster] 単一マスター生成に失敗、従来のA+B合体にフォールバック: ' + eSM.message);
      
    }
  }

  var blob = zipPptxWithMedia(built.parts, [], 'majin_native_base.pptx');
  
  
  
  
  
  var __tempBId = null;
  try {
    var bFile = convertPptxToSlides(blob, finalName + ' (native-temp)', null); 
    __tempBId = bFile.id;
    
    _waitForConvertedSlides_(bFile.id, (slideData || []).length);
    var B = SlidesApp.openById(bFile.id);
    
    var A;
    if (existingDeckId) {
      A = SlidesApp.openById(existingDeckId);
      try { A.setName(finalName); } catch (e) {}
      var aOld = A.getSlides();           
      for (var ai = aOld.length - 1; ai >= 0; ai--) { try { aOld[ai].remove(); } catch (e) {} }
    } else {
      A = SlidesApp.create(finalName);
      try { A.getSlides()[0].remove(); } catch (e) {}
    }
    
    var bSlides = B.getSlides();
    for (var bi = 0; bi < bSlides.length; bi++) { A.appendSlide(bSlides[bi]); }
    A.saveAndClose();
    var presFull = SlidesApp.openById(A.getId());
    Logger.log('[native] base deck built(標準マスター保持): slides=' + built.slideCount + ' shapes=' + built.shapeCount
      + ' warnings=' + (built.warnings ? built.warnings.length : 0) + ' inPlace=' + (!!existingDeckId)
      + ' id=' + presFull.getId() + ' tempB=' + bFile.id);
    
    try {
      var decoSlides = presFull.getSlides().map(function (s) {
        return s.getPageElements().map(function (el) { return el.getObjectId(); });
      });
      _saveNativeDecoRecord(presFull.getId(), { h: svgHash, d: decoSlides, dm: wantDecor });
    } catch (eRec) { Logger.log('[native] 装飾ID記録に失敗(次回スキップ最適化は無効、生成は正常): ' + eRec.message); }
    return presFull;
  } finally {
    
    if (__tempBId) { try { Drive.Files.remove(__tempBId); } catch (e) { try { DriveApp.getFileById(__tempBId).setTrashed(true); } catch (e2) {} } }
  }
}


function _nativeSvgHash(svgs) {
  var joined = (svgs || []).join('');
  var bytes = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, joined, Utilities.Charset.UTF_8);
  var hex = '';
  for (var i = 0; i < bytes.length; i++) { var b = (bytes[i] + 256) % 256; hex += (b < 16 ? '0' : '') + b.toString(16); }
  return hex;
}





function _nativeDeckFingerprint_(svgs, settings, slideData) {
  var parts = [(svgs || []).join('')];
  
  var tplId = '';
  try {
    if (settings && settings.templateUrl) tplId = _extractTemplateIdNative(settings.templateUrl) || '';
    else if (settings && settings.seedTemplateId) tplId = String(settings.seedTemplateId).trim();
  } catch (e1) { tplId = ''; }
  parts.push(tplId);
  
  var mtime = '';
  if (tplId) {
    var ms = _templateLastUpdatedMs_(tplId); 
    mtime = (ms === null) ? '' : String(ms);
  } else if (settings && settings.useEmbeddedSeed) {
    mtime = 'embedded-seed'; 
  }
  parts.push(mtime);
  
  var types = '';
  try { types = JSON.stringify((slideData || []).map(function (d) { return d && d.type; })); } catch (e3) { types = ''; }
  parts.push(types);
  return _nativeSvgHash([parts.join('\u0001')]); 
}






function _nativeDecoKey(deckId) { return 'majin_deco_' + deckId; }
function _loadNativeDecoRecord(deckId) {
  try {
    var raw = PropertiesService.getScriptProperties().getProperty(_nativeDecoKey(deckId));
    if (!raw) return null;
    
    if (raw.indexOf('GZ:') === 0) {
      var json = Utilities.ungzip(Utilities.newBlob(Utilities.base64Decode(raw.slice(3)), 'application/x-gzip')).getDataAsString();
      return JSON.parse(json);
    }
    return JSON.parse(raw); 
  } catch (e) { return null; }
}
function _saveNativeDecoRecord(deckId, rec) {
  try {
    if (rec && typeof rec === 'object') rec.ts = Date.now(); 
    var props = PropertiesService.getScriptProperties();
    var json = JSON.stringify(rec);
    var payload;
    if (json.length <= 8500) {
      payload = json; 
    } else {
      
      var gz = 'GZ:' + Utilities.base64Encode(Utilities.gzip(Utilities.newBlob(json)).getBytes());
      if (gz.length > 8500) { Logger.log('[native] 装飾ID記録が大きすぎ保存スキップ(raw=' + json.length + 'B gz=' + gz.length + 'B)'); return; }
      payload = gz;
    }
    props.setProperty(_nativeDecoKey(deckId), payload);
    
    
    
    
    try { _pruneNativeDecoRecords_(props, 30); } catch (ePrune) {}
  } catch (e) { Logger.log('[native] 装飾ID記録の保存失敗: ' + e.message); }
}



function _pruneNativeDecoRecords_(props, maxKeep) {
  var all = props.getProperties();
  
  
  var keys = Object.keys(all).filter(function (k) { return k.indexOf('majin_deco_') === 0; });
  if (keys.length <= maxKeep) return;
  var items = [];
  keys.forEach(function (k) {
    var v = all[k], ts = 0; 
    try {
      var obj = (v && v.indexOf('GZ:') === 0)
        ? JSON.parse(Utilities.ungzip(Utilities.newBlob(Utilities.base64Decode(v.slice(3)), 'application/x-gzip')).getDataAsString())
        : JSON.parse(v);
      if (obj && typeof obj.ts === 'number') ts = obj.ts;
    } catch (e) { ts = 0; }
    items.push({ k: k, ts: ts });
  });
  if (items.length <= maxKeep) return;
  items.sort(function (a, b) { return a.ts - b.ts; }); 
  var removeN = items.length - maxKeep;
  for (var i = 0; i < removeN; i++) { try { props.deleteProperty(items[i].k); } catch (e) {} }
}


function testNativeMixedDeck() {
  var sampleMixed = [
    { type: 'title', title: 'まじん式スライド ネイティブ混在テスト', date: '2026.06.15' },
    { type: 'agenda', title: '本日のアジェンダ', items: ['市場の現状分析', '主要KPIの確認', '成長戦略の提案'] },
    { type: 'section', sectionNo: 1, title: '市場の現状分析' },
    { type: 'kpi', title: '主要指標サマリー', columns: 3, items: [
      { label: '売上高', value: '1.2億円', change: '+15%', status: 'good' },
      { label: '解約率', value: '3.2%', change: '-0.5%', status: 'good' },
      { label: '新規顧客', value: '320件', change: '+8%', status: 'good' }
    ] },
    { type: 'section', sectionNo: 2, title: '主要KPIの確認' },
    { type: 'compare', title: '自社と競合の比較', leftTitle: '自社', rightTitle: '競合A',
      leftItems: ['24時間サポート', '低価格プラン'], rightItems: ['実績豊富', 'ブランド力'] },
    { type: 'section', sectionNo: 3, title: '成長戦略の提案' },
    { type: 'closing' }
  ];
  var s = (typeof loadSettings === 'function') ? loadSettings() : {};
  s.nativeSectionMode = true;
  var url = createPresentation(sampleMixed, s);
  Logger.log('[testNativeMixedDeck] nativeSectionMode=ON url = ' + url);
  return url;
}


function testNativeDecorDeck() {
  var sampleMixed = [
    { type: 'title', title: 'まじん式スライド 装飾ネイティブ化テスト', date: '2026.06.15' },
    { type: 'agenda', title: '本日のアジェンダ', items: ['現状', 'KPI', '戦略'] },
    { type: 'section', sectionNo: 1, title: '市場の現状分析' },
    { type: 'content', title: '本文スライド', points: ['ポイントA', 'ポイントB', 'ポイントC'] },
    { type: 'kpi', title: '主要指標', columns: 3, items: [
      { label: '売上', value: '1.2億', change: '+15%', status: 'good' },
      { label: '解約率', value: '3.2%', change: '-0.5%', status: 'good' },
      { label: '新規', value: '320', change: '+8%', status: 'good' }
    ] },
    { type: 'closing' }
  ];
  var s = (typeof loadSettings === 'function') ? loadSettings() : {};
  s.nativeDecorMode = true;     
  s.enableGradient = true;      
  s.showBottomBar = true;
  var url = createPresentation(sampleMixed, s);
  Logger.log('[testNativeDecorDeck] nativeDecorMode=ON url = ' + url);
  return url;
}
