{"id":2509,"date":"2023-07-05T09:35:55","date_gmt":"2023-07-05T09:35:55","guid":{"rendered":"https:\/\/calais-dover.com\/?page_id=2509"},"modified":"2026-10-26T11:41:04","modified_gmt":"2026-10-26T11:41:04","slug":"ferry-calais-douvres-horaires","status":"publish","type":"page","link":"https:\/\/calais-dover.com\/fr\/ferry-calais-douvres-horaires\/","title":{"rendered":"Horaires ferry Calais-Douvres"},"content":{"rendered":"\n<div class=\"wp-block-group cd-page is-layout-flow wp-block-group-is-layout-flow\">\n\n<p class=\"cd-intro wp-block-paragraph\"><strong>Consultez ici les horaires du jour du bateau et du train (Le Shuttle) entre Calais et Douvres, avec les d\u00e9parts de DFDS, P&amp;O Ferries, Irish Ferries et Le Shuttle. Tout en un seul aper\u00e7u.<\/strong><\/p>\n\n\n<p class=\"wp-block-paragraph\">Travers\u00e9e retard\u00e9e ou annul\u00e9e ? Toutes les sources officielles sont sur notre <a href=\"https:\/\/calais-dover.com\/fr\/sailing-updates\/\">page info trafic<\/a>.<\/p>\n\n\n<div class=\"wp-block-group alignfull cd-subnav is-layout-flow wp-block-group-is-layout-flow\">\n<ul class=\"wp-block-list\">\n<li><a href=\"#horaires\">Bateau Calais-Douvres<\/a><\/li>\n\n<li><a href=\"#dover-calais-ferry-timetable\">Bateau Douvres-Calais<\/a><\/li>\n\n<li><a href=\"#calais-folkestone-eurotunnel-timetable\">Le Shuttle Calais-Folkestone<\/a><\/li>\n\n<li><a href=\"#folkestone-calais-eurotunnel-timetable\">Le Shuttle Folkestone-Calais<\/a><\/li>\n\n<li><a href=\"#enregistrement\">Enregistrement<\/a><\/li>\n<\/ul>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\">Vous voyez les horaires en direct des <strong>ferries entre Calais et Douvres<\/strong> et de la <strong>navette Eurotunnel entre Calais et Folkestone<\/strong>. Par d\u00e9faut, nous affichons <strong>1 jour<\/strong> pour garder les horaires lisibles ; vous pouvez aussi choisir une autre date. Attention au d\u00e9calage horaire : il est une heure plus tard en France qu&rsquo;en Angleterre.<\/p>\n\n\n<h2 class=\"wp-block-heading\" id=\"horaires\">Horaires bateau Calais-Douvres<\/h2>\n\n\n<p class=\"wp-block-paragraph\">Ci-dessous, les d\u00e9parts en direct depuis Calais et les arriv\u00e9es pr\u00e9vues \u00e0 Douvres. Pour chaque compagnie, la travers\u00e9e est d\u00e9crite sur nos pages <a href=\"https:\/\/calais-dover.com\/fr\/po-ferries\/\">P&amp;O Ferries<\/a>, <a href=\"https:\/\/calais-dover.com\/fr\/dfds\/\">DFDS<\/a> et <a href=\"https:\/\/calais-dover.com\/fr\/irish-ferries\/\">Irish Ferries<\/a>.<\/p>\n\n<div class=\"tp-wrap tp-theme-extended_cards tp-day-setup-modern\" data-config=\"{&quot;rest&quot;:&quot;https:\\\/\\\/calais-dover.com\\\/fr\\\/wp-json\\\/timetables-pro\\\/v1\\\/timetables&quot;,&quot;route&quot;:97,&quot;days&quot;:1,&quot;autoload&quot;:true,&quot;theme&quot;:&quot;extended_cards&quot;,&quot;daySetup&quot;:&quot;modern&quot;,&quot;limit10&quot;:true,&quot;label&quot;:&quot;Calais - Dover&quot;,&quot;labels&quot;:{&quot;ui_date&quot;:&quot;Date&quot;,&quot;ui_days&quot;:&quot;Jours&quot;,&quot;ui_button&quot;:&quot;Afficher les travers\\u00e9es&quot;,&quot;status_idle&quot;:&quot;Choisissez une date et cliquez sur Afficher les travers\\u00e9es&quot;,&quot;status_loading&quot;:&quot;Chargement...&quot;,&quot;status_empty&quot;:&quot;Aucune travers\\u00e9e trouv\\u00e9e&quot;,&quot;status_found&quot;:&quot;%d travers\\u00e9es trouv\\u00e9es&quot;,&quot;th_date&quot;:&quot;Date&quot;,&quot;th_dep&quot;:&quot;D\\u00e9part&quot;,&quot;th_arr&quot;:&quot;Arriv\\u00e9e&quot;,&quot;th_dur&quot;:&quot;Dur\\u00e9e&quot;,&quot;th_ship&quot;:&quot;Navire&quot;,&quot;th_op&quot;:&quot;Op\\u00e9rateur&quot;,&quot;th_route&quot;:&quot;Route&quot;,&quot;wd_sun&quot;:&quot;DIM&quot;,&quot;wd_mon&quot;:&quot;LUN&quot;,&quot;wd_tue&quot;:&quot;MAR&quot;,&quot;wd_wed&quot;:&quot;MER&quot;,&quot;wd_thu&quot;:&quot;JEU&quot;,&quot;wd_fri&quot;:&quot;VEN&quot;,&quot;wd_sat&quot;:&quot;SAM&quot;,&quot;summary_footer_one&quot;:&quot;\\u00bb Voir tous les d\\u00e9parts %s dans notre horaire&quot;,&quot;summary_footer_two&quot;:&quot;\\u00bb Voir plus de d\\u00e9parts %1$s et %2$s dans notre horaire&quot;,&quot;summary_footer_generic&quot;:&quot;Consultez notre horaire pour plus de travers\\u00e9es&quot;,&quot;summary_header&quot;:&quot;Les deux prochains d\\u00e9parts :&quot;,&quot;summary_no_upcoming&quot;:&quot;Aucun prochain d\\u00e9part trouv\\u00e9&quot;,&quot;more_link&quot;:&quot;Plus de travers\\u00e9es&quot;,&quot;more_departures_button&quot;:&quot;Afficher plus de d\\u00e9parts&quot;,&quot;day_nav_previous&quot;:&quot;Afficher le jour pr\\u00e9c\\u00e9dent&quot;,&quot;day_nav_next&quot;:&quot;Afficher le jour suivant&quot;,&quot;day_nav_today&quot;:&quot;Afficher aujourd\\u2019hui&quot;,&quot;day_nav_tomorrow&quot;:&quot;Afficher demain&quot;,&quot;local_time_pair&quot;:&quot;Heure locale du port de d\\u00e9part : %1$s \\\/ Heure locale du port d&#039;arriv\\u00e9e : %2$s&quot;,&quot;local_time_combined&quot;:&quot;Heure locale des ports de d\\u00e9part et d&#039;arriv\\u00e9e : %s&quot;,&quot;tag_often_booked&quot;:&quot;\\u2b50 Souvent r\\u00e9serv\\u00e9&quot;,&quot;tag_leaving_soon&quot;:&quot;\\ud83d\\udd34 D\\u00e9part imminent&quot;,&quot;tag_currently_sailing&quot;:&quot;\\ud83d\\udfe2 En navigation&quot;,&quot;tag_arrived&quot;:&quot;\\u26ab Arriv\\u00e9&quot;,&quot;tag_overnight&quot;:&quot;Nuit \\u00e0 bord&quot;},&quot;showLogo&quot;:true,&quot;activityBoosters&quot;:true,&quot;shipMap&quot;:{&quot;p&amp;o ferries&quot;:&quot;P&amp;O Ferries&quot;,&quot;dfds&quot;:&quot;DFDS&quot;,&quot;irish ferries&quot;:&quot;Irish Ferries&quot;,&quot;norfolk line&quot;:&quot;DFDS&quot;},&quot;shipLinks&quot;:{&quot;p&amp;o ferries&quot;:&quot;https:\\\/\\\/calais-dover.com\\\/fr\\\/po-ferries\\\/&quot;,&quot;dfds&quot;:&quot;https:\\\/\\\/calais-dover.com\\\/fr\\\/dfds\\\/&quot;,&quot;irish ferries&quot;:&quot;https:\\\/\\\/calais-dover.com\\\/fr\\\/irish-ferries\\\/&quot;,&quot;norfolk line&quot;:&quot;https:\\\/\\\/calais-dover.com\\\/fr\\\/dfds\\\/&quot;},&quot;showShip&quot;:true,&quot;departurePort&quot;:&quot;Calais&quot;,&quot;arrivalPort&quot;:&quot;Dover&quot;,&quot;departureTimezone&quot;:&quot;Europe\\\/Paris&quot;,&quot;arrivalTimezone&quot;:&quot;Europe\\\/London&quot;}\">\n  <div class=\"tp-controls\" role=\"group\" aria-label=\"Timetable controls\">\n    <div>\n      <label for=\"tp-date\">Date<\/label><br>\n      <input id=\"tp-date\" class=\"tp-date\" type=\"date\" aria-label=\"Date\">\n    <\/div>\n    <div class=\"tp-day-field\">\n      <label for=\"tp-range\">Jours<\/label><br>\n      <select id=\"tp-range\" class=\"tp-range\" aria-label=\"Jours\">\n        <option value=\"1\">1<\/option><option value=\"3\">3<\/option><option value=\"7\">7<\/option><option value=\"14\">14<\/option>\n      <\/select>\n    <\/div>\n    <div><button id=\"tp-load\" class=\"tp-btn\">Afficher les travers\u00e9es<\/button><\/div>\n  <\/div>\n\n  <div class=\"tp-local-nav-row\">\n    <div class=\"tp-local-times\" id=\"tp-local-times\" aria-live=\"polite\"><\/div>\n    <div class=\"tp-day-nav-host\"><\/div>\n  <\/div>\n  <div id=\"tp-status\" class=\"tp-muted\" aria-live=\"polite\">Choisissez une date et cliquez sur Afficher les travers\u00e9es<\/div>\n  <div id=\"tp-results\"><\/div>\n\n  <div class=\"tp-loader\" aria-hidden=\"true\">\n    <div class=\"tp-loader-card\">\n      <div class=\"tp-spinner\" aria-hidden=\"true\"><\/div>\n      <div class=\"tp-loader-text\">Chargement...<\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\n(function(){\n  const wrap   = document.currentScript.previousElementSibling;\n  const cfg    = JSON.parse(wrap.getAttribute('data-config')||'{}');\n  const L      = cfg.labels||{};\n  const dateEl = wrap.querySelector('#tp-date');\n  const daysEl = wrap.querySelector('#tp-range');\n  const btn    = wrap.querySelector('#tp-load');\n  const status = wrap.querySelector('#tp-status');\n  const out    = wrap.querySelector('#tp-results');\n  const loader = wrap.querySelector('.tp-loader');\n  const controls = wrap.querySelector('.tp-controls');\n  const navHost = wrap.querySelector('.tp-day-nav-host');\n  const localTimesEl = wrap.querySelector('#tp-local-times');\n\n  const showShip = (cfg.showShip !== false && cfg.showShip !== 0 && cfg.showShip !== '0');\n  const activityBoosters = !!cfg.activityBoosters;\n  const daySetup = (cfg.daySetup === 'modern') ? 'modern' : 'classic';\n  const limit10 = !!cfg.limit10;\n  let featuredKey = '';\n  let soonKey = '';\n  let revealAll = false;\n\n  if (daySetup === 'modern') {\n    const dayField = wrap.querySelector('.tp-day-field');\n    if (dayField) dayField.style.display = 'none';\n  }\n\n  dateEl.valueAsDate = new Date();\n  dateEl.min = new Date().toISOString().split('T')[0];\n  Array.from(daysEl.options).forEach(o=>{ if(parseInt(o.value,10)===parseInt(cfg.days||7,10)) o.selected=true; });\n\n  function pad(n){ return String(n).padStart(2,'0'); }\n  function iso(d){ return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate()); }\n  function toLocal(s){ return new Date(s); }\n  function hhmm(d){ return d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}); }\n  function dShort(d){ return d.toLocaleDateString([], {day:'numeric', month:'short'}); } \/\/ short, no year\n  function dur(m){ const h=Math.floor(m\/60), r=m%60; return r? (h+'h '+r+'m') : (h+'h'); }\n\n  const WD = [\n    L.wd_sun || 'SUN',\n    L.wd_mon || 'MON',\n    L.wd_tue || 'TUE',\n    L.wd_wed || 'WED',\n    L.wd_thu || 'THU',\n    L.wd_fri || 'FRI',\n    L.wd_sat || 'SAT'\n  ];\n\n  function setLoader(v){ loader.classList.toggle('show', !!v); loader.setAttribute('aria-hidden', v?'false':'true'); }\n\n  function formatZoneTime(tz){\n    if (!tz) return '--:--';\n    try {\n      return new Intl.DateTimeFormat([], {hour:'2-digit', minute:'2-digit', hour12:false, timeZone: tz}).format(new Date());\n    } catch (e) {\n      return '--:--';\n    }\n  }\n\n  function renderLocalTimes(){\n    if (!localTimesEl) return;\n    if (!cfg.departureTimezone || !cfg.arrivalTimezone) {\n      localTimesEl.textContent = '';\n      return;\n    }\n    const depTime = formatZoneTime(cfg.departureTimezone);\n    const arrTime = formatZoneTime(cfg.arrivalTimezone);\n    const pairTpl = L.local_time_pair || 'Departure Port Local Time: %1$s \/ Arrival Port Local Time: %2$s';\n    const combinedTpl = L.local_time_combined || 'Departure & Arrival Port Local Time: %s';\n    if (depTime === arrTime) {\n      localTimesEl.textContent = combinedTpl.replace('%s', depTime);\n      return;\n    }\n    localTimesEl.textContent = pairTpl.replace('%1$s', depTime).replace('%2$s', arrTime);\n  }\n\n  function setStatusInline(html){\n    let box = wrap.querySelector('.tp-controls-status');\n    if (!box) {\n      box = document.createElement('div');\n      box.className = 'tp-controls-status';\n      controls.appendChild(box);\n    }\n    box.innerHTML = html || '';\n  }\n\n  function clearStatusInline(){\n    const box = wrap.querySelector('.tp-controls-status');\n    if (box) box.innerHTML = '';\n  }\n\n  function navHtml(){\n    if (daySetup !== 'modern') return '';\n    const base = new Date(dateEl.value || new Date());\n    const today = new Date();\n    const tomorrow = new Date(today);\n    tomorrow.setDate(today.getDate() + 1);\n    const isToday = base.toDateString() === today.toDateString();\nlet prevLabel = L.day_nav_previous || 'Show previous day';\nlet nextLabel = L.day_nav_next || 'Show next day';\nif (isToday) nextLabel = L.day_nav_tomorrow || 'Show tomorrow';\nif (base.toDateString() === tomorrow.toDateString()) prevLabel = L.day_nav_today || 'Show today';\n    return '<div class=\"tp-day-nav\">'\n      + (isToday ? '' : '<a href=\"#\" data-shift=\"-1\">'+prevLabel+'<\/a>')\n      + '<a href=\"#\" data-shift=\"1\">'+nextLabel+'<\/a>'\n      + '<\/div>';\n  }\n\n  function shiftDate(delta){\n    const base = new Date(dateEl.value || new Date());\n    base.setDate(base.getDate() + delta);\n    dateEl.value = iso(base);\n    load();\n  }\n\n  function moreButtonHtml(hiddenCount){\n    if (!limit10 || revealAll || hiddenCount <= 0) return '';\n    return '<div style=\"margin-top:12px;text-align:center\">'\n      + '<button type=\"button\" class=\"tp-btn tp-more-btn\">'+(L.more_departures_button || 'Show more departures')+' ('+hiddenCount+')<\/button>'\n      + '<\/div>';\n  }\n\n  function opCell(r){\n    const name = r.opName || ('Operator '+(r.opId||''));\n    const logo = (cfg.showLogo && r.opLogo) ? '<img decoding=\"async\" src=\"'+r.opLogo+'\" alt=\"'+name+'\"> ' : '';\n    const label = logo + '<span>'+name+'<\/span>';\n    return r.opLink ? '<a class=\"tp-op\" href=\"'+r.opLink+'\" target=\"_blank\" rel=\"nofollow noopener\">'+label+'<\/a>' : '<span class=\"tp-op\">'+label+'<\/span>';\n  }\n\n  function shipInfo(original){\n    const key = (original||'').toLowerCase().trim();\n    const label = (cfg.shipMap && cfg.shipMap[key]) || original || '';\n    const href  = (cfg.shipLinks && cfg.shipLinks[key]) || '';\n    return {label, href};\n  }\n\n  function shipBadge(original){\n    const s = shipInfo(original);\n    const badge = '<span class=\"tp-badge\">'+(s.label||'')+'<\/span>';\n    return s.href ? ('<a href=\"'+s.href+'\" target=\"_blank\" rel=\"nofollow noopener\">'+badge+'<\/a>') : badge;\n  }\n\n  function rowKey(r){\n    return [String(r.opId||''), String(+r.dep||0), String(+r.arr||0), String(r.ship||'')].join('|');\n  }\n\n  function pickFeaturedKey(rows, avoidKey){\n    if (!activityBoosters || !rows.length) return '';\n    const routeSeed = parseInt(cfg.route, 10) || 0;\n    const dateSeed = parseInt(String(dateEl.value || '').replace(\/-\/g,''), 10) || 0;\n\n    let pool = rows;\n    if (avoidKey) {\n      const soonIdx = rows.findIndex(r => rowKey(r) === avoidKey);\n      if (soonIdx >= 0) {\n        const later = rows.slice(soonIdx + 1).filter(r => rowKey(r) !== avoidKey);\n        pool = later.length ? later : rows.filter(r => rowKey(r) !== avoidKey);\n      } else {\n        pool = rows.filter(r => rowKey(r) !== avoidKey);\n      }\n    }\n\n    if (!pool.length) return '';\n    const idx = Math.abs((routeSeed + dateSeed + rows.length) % pool.length);\n    return rowKey(pool[idx]);\n  }\n\n  function pickSoonKey(rows){\n    if (!activityBoosters || !rows.length) return '';\n    const now = Date.now();\n    const todayStr = new Date(now).toDateString();\n    const firstUpcoming = rows.find(r => (r.dep instanceof Date) && r.dep.getTime() > now && r.dep.toDateString() === todayStr);\n    return firstUpcoming ? rowKey(firstUpcoming) : '';\n  }\n\n  function boostersFor(r){\n    if (!activityBoosters) return [];\n    const now = Date.now();\n    const depMs = (r.dep instanceof Date) ? r.dep.getTime() : 0;\n    const arrMs = (r.arr instanceof Date) ? r.arr.getTime() : 0;\n    const depIsToday = (r.dep instanceof Date) && (new Date(now).toDateString() === r.dep.toDateString());\n\n    if (depMs > 0 && arrMs > 0 && depMs <= now && arrMs > now) {\n      return [{cls:'tp-booster-sailing', text:(L.tag_currently_sailing || '\ud83d\udfe2 Currently sailing')}];\n    }\n    if (depIsToday && arrMs > 0 && arrMs <= now) {\n      return [{cls:'tp-booster-arrived', text:(L.tag_arrived || '\u26ab Arrived')}];\n    }\n    if (rowKey(r) === soonKey) return [{cls:'tp-booster-soon', text:(L.tag_leaving_soon || '\ud83d\udd34 Leaving soon')}];\n    if (r.dep && r.arr && r.dep.toDateString() !== r.arr.toDateString()) return [{cls:'tp-booster-overnight', text:(L.tag_overnight || 'Overnight')}];\n\n    return [];\n  }\n\n  function boostersHtml(r, floating){\n    const tags = boostersFor(r);\n    if (!tags.length) return '';\n    const cls = floating ? 'tp-boosters tp-boosters-float' : 'tp-boosters';\n    return '<div class=\"'+cls+'\">' + tags.map(t => '<span class=\"tp-booster '+t.cls+'\">'+boosterLabelHtml(t.text)+'<\/span>').join('') + '<\/div>';\n  }\n\n  function boosterLabelHtml(text){\n    return String(text || '').replace(\/^(\ud83d\udd34|\ud83d\udfe2|\u26ab|\u2b50)\\s*\/, '<span class=\"tp-booster-emoji\">$1<\/span>');\n  }\n\n  function routeTicketHtml(label){\n    const raw = String(label || '').trim();\n    if (!raw) return '';\n    if (cfg.theme !== 'small_cards') return escAttr(raw);\n    const parts = raw.split(\/\\s*[\\-\u2013]\\s*\/, 2);\n    if (parts.length < 2) return escAttr(raw);\n    return escAttr(parts[0]) + '<br>' + escAttr(parts[1]);\n  }\n\n  function escAttr(v){\n    return String(v||'').replace(\/[&<>\"']\/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[m]));\n  }\n\n  function bindRowLinks(){\n    if (cfg.theme !== 'max_clickouts') return;\n    out.querySelectorAll('tr.tp-row-link').forEach((row)=>{\n      row.setAttribute('role','link');\n      row.setAttribute('tabindex','0');\n      row.addEventListener('click', (e)=>{\n        if (e.target && e.target.closest('a')) return;\n        const href = row.getAttribute('data-row-link');\n        if (!href) return;\n        window.open(href, '_blank', 'noopener,noreferrer');\n      });\n      row.addEventListener('keydown', (e)=>{\n        if (e.key !== 'Enter' && e.key !== ' ') return;\n        e.preventDefault();\n        row.click();\n      });\n    });\n  }\n\n  function renderTable(rows){\n    const th = {date:L.th_date,dep:L.th_dep,arr:L.th_arr,dur:L.th_dur,ship:L.th_ship,op:L.th_op};\n\n    const headCells = [\n      '<th>'+th.date+'<\/th>',\n      '<th>'+th.dep+'<\/th>',\n      '<th>'+th.arr+'<\/th>',\n      '<th>'+th.dur+'<\/th>'\n    ];\n    if (showShip) {\n      headCells.push('<th>'+th.ship+'<\/th>');\n    }\n    headCells.push('<th>'+th.op+'<\/th>');\n\n    let html = '<table class=\"tp-table\"><thead><tr>'+headCells.join('')+'<\/tr><\/thead><tbody>';\n\n    rows.forEach(r=>{\n      const dow = WD[r.dep.getDay()] || '';\n      const clickable = (cfg.theme === 'max_clickouts' && r.opLink);\n      const rowAttr = clickable ? (' class=\"tp-row-link\" data-row-link=\"'+escAttr(r.opLink)+'\"') : '';\n      const cells = [\n        '<td>'+dShort(r.dep)+' <span class=\"tp-day\">'+dow+'<\/span>'+boostersHtml(r, false)+'<\/td>',\n        '<td>'+hhmm(r.dep)+'<\/td>',\n        '<td>'+hhmm(r.arr)+'<\/td>',\n        '<td>'+dur(r.min)+'<\/td>'\n      ];\n      if (showShip) {\n        cells.push('<td>'+shipBadge(r.ship)+'<\/td>');\n      }\n      cells.push('<td>'+opCell(r)+'<\/td>');\n      html += '<tr'+rowAttr+'>'+cells.join('')+'<\/tr>';\n    });\n    html += '<\/tbody><\/table>';\n    return html;\n  }\n\n  function renderCards(rows){\n    if (cfg.theme === 'extended_cards') {\n      return renderExtendedCards(rows);\n    }\n\n    const routeName = cfg.label || '';\n    let html = '<div class=\"tp-cardlist\">';\n    rows.forEach(r=>{\n      const dow = WD[r.dep.getDay()] || '';\n      const times = hhmm(r.dep) + ' <span class=\"tp-arrow\">\u2192<\/span> ' + hhmm(r.arr);\n      html += '<div class=\"tp-card\">'+boostersHtml(r, true)+\n        \/\/ Row 1: Date + weekday + times\n        '<div class=\"tp-mrow\">'+\n          '<div class=\"lhs\"><span class=\"tp-sub\">'+dShort(r.dep)+'<\/span><span class=\"tp-day\">'+dow+'<\/span><\/div>'+\n          '<div class=\"rhs\"><span class=\"tp-time\">'+times+'<\/span><\/div>'+\n        '<\/div>';\n\n      if (showShip) {\n        html +=\n        \/\/ Row 2: Ship\n        '<div class=\"tp-mrow\">'+\n          '<div class=\"lhs\">'+(showShip ? shipBadge(r.ship) : '')+'<\/div>'+\n          '<div class=\"rhs\"><\/div>'+\n        '<\/div>';\n      }\n\n      html +=\n        \/\/ Row 3: Operator + route label (no book button)\n        '<div class=\"tp-mrow\">'+\n          '<div class=\"lhs\">'+opCell(r)+'<\/div>'+\n          (routeName ? ('<div class=\"rhs\"><span class=\"tp-route-ticket\">'+routeTicketHtml(routeName)+'<\/span><\/div>') : '<div class=\"rhs\"><\/div>')+\n        '<\/div>'+\n      '<\/div>';\n    });\n    html += '<\/div>';\n    return html;\n  }\n\n  function renderExtendedCards(rows){\n    const routeName = cfg.label || '';\n    const ctaText = L.more_link || 'More sailings';\n    let html = '<div class=\"tp-cardlist\">';\n    rows.forEach(r=>{\n      const dow = WD[r.dep.getDay()] || '';\n      const link = r.opLink ? ('<a class=\"tp-ext-cta\" href=\"'+r.opLink+'\" target=\"_blank\" rel=\"nofollow noopener\">'+ctaText+' <span class=\"tp-ext-cta-arrow\">\u00bb<\/span><\/a>') : '<span class=\"tp-ext-cta\">'+ctaText+' <span class=\"tp-ext-cta-arrow\">\u00bb<\/span><\/span>';\n      html += '<div class=\"tp-ext-card\">'\n        + boostersHtml(r, true)\n        + '<div class=\"tp-ext-left\">'\n        +   '<div>'+opCell(r)+'<\/div>'\n        +   (showShip ? ('<div>'+shipBadge(r.ship)+'<\/div>') : '')\n        + '<\/div>'\n        + '<div class=\"tp-ext-mid\">'\n        +   '<div class=\"tp-ext-top\"><span class=\"tp-sub\">'+dShort(r.dep)+' <span class=\"tp-day\">'+dow+'<\/span><\/span><span class=\"tp-ext-dur\">'+dur(r.min)+'<\/span><\/div>'\n        +   '<div class=\"tp-ext-times\"><span class=\"tp-ext-time\">'+hhmm(r.dep)+'<\/span><span class=\"tp-ext-line\"><\/span><span class=\"tp-ext-dur\">'+dur(r.min)+'<\/span><span class=\"tp-ext-line\"><\/span><span class=\"tp-ext-time\">'+hhmm(r.arr)+'<\/span><\/div>'\n        +   (routeName ? ('<div class=\"tp-ext-route\">'+routeName+'<\/div>') : '')\n        + '<\/div>'\n        + '<div class=\"tp-ext-right\">'+link+'<\/div>'\n        + '<\/div>';\n    });\n    html += '<\/div>';\n    return html;\n  }\n\n  async function load(){\n    btn.disabled = true; setLoader(true); status.textContent = L.status_loading;\n    const start = new Date(dateEl.value || new Date());\n    const days  = (daySetup === 'modern') ? 1 : Math.max(1, parseInt(daysEl.value,10)||1);\n    const end   = new Date(start); end.setDate(start.getDate()+days-1);\n    const url = new URL(cfg.rest); url.searchParams.set('route', String(cfg.route));\n    url.searchParams.set('from', iso(start)); url.searchParams.set('to', iso(end));\n    try{\n      const res = await fetch(url.toString(), {credentials:'same-origin'});\n      if(!res.ok) throw new Error('HTTP '+res.status);\n      const json = await res.json();\n      const list = (json && json.data && Array.isArray(json.data.rows)) ? json.data.rows : [];\n      let rows = list.map(t=>({\n        opId:t.operatorId||null, opName:t.operatorName||'', opLogo:t.operatorLogo||'', opLink:t.operatorLink||'',\n        dep:toLocal(t.departureTime), arr:toLocal(t.arrivalTime), min:t.durationInMinutes||0, ship:t.shipName||''\n      })).sort((a,b)=>a.dep-b.dep);\n      const todayStr = new Date().toDateString();\n      const nowMs = Date.now();\n      const arrivedToday = rows\n        .filter(r => (r.dep instanceof Date) && (r.arr instanceof Date) && r.dep.toDateString() === todayStr && r.arr.getTime() <= nowMs)\n        .sort((a,b) => b.arr - a.arr);\n      if (arrivedToday.length > 2) {\n        const keep = new Set(arrivedToday.slice(0,2).map(rowKey));\n        rows = rows.filter(r => {\n          const isArrivedToday = (r.dep instanceof Date) && (r.arr instanceof Date) && r.dep.toDateString() === todayStr && r.arr.getTime() <= nowMs;\n          return !isArrivedToday || keep.has(rowKey(r));\n        });\n      }\n      if(rows.length===0){ status.textContent = L.status_empty; clearStatusInline(); out.innerHTML=''; return; }\n      const visibleRows = (limit10 && !revealAll && rows.length > 10) ? rows.slice(0,10) : rows;\n      const hiddenCount = rows.length - visibleRows.length;\n      soonKey = pickSoonKey(visibleRows);\n      featuredKey = pickFeaturedKey(visibleRows, soonKey);\n      status.textContent = '';\n      setStatusInline(\n  (L.status_found || '%d sailings found').replace('%d', '<strong>'+rows.length+'<\/strong>')\n);\n      out.innerHTML = renderTable(visibleRows) + renderCards(visibleRows) + moreButtonHtml(hiddenCount);\n      bindRowLinks();\n      if (navHost) navHost.innerHTML = navHtml();\n    }catch(e){\n      status.textContent = 'Failed to load data'; out.innerHTML = '<pre>'+String(e.message||e)+'<\/pre>';\n    }finally{ setLoader(false); btn.disabled=false; }\n  }\n\n  wrap.addEventListener('click', function(e){\n    const nav = e.target.closest('.tp-day-nav a[data-shift]');\n    if (nav) {\n      e.preventDefault();\n      shiftDate(parseInt(nav.getAttribute('data-shift'),10) || 0);\n      return;\n    }\n    const more = e.target.closest('.tp-more-btn');\n    if (more) {\n      e.preventDefault();\n      revealAll = true;\n      load();\n    }\n  });\n\n  btn.addEventListener('click', load);\n  if (navHost) navHost.innerHTML = navHtml();\n  renderLocalTimes();\n  setInterval(renderLocalTimes, 30000);\n  if (cfg.autoload) load();\n})();\n<\/script>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"dover-calais-ferry-timetable\">Horaires bateau Douvres-Calais<\/h2>\n\n\n<p class=\"wp-block-paragraph\">Ces horaires montrent les d\u00e9parts en direct depuis Douvres et les arriv\u00e9es pr\u00e9vues \u00e0 Calais. Vous h\u00e9sitez entre les compagnies ? Nous avons voyag\u00e9 avec les trois et compar\u00e9 nos exp\u00e9riences dans notre <a href=\"https:\/\/calais-dover.com\/fr\/comparez\/\">comparatif des compagnies<\/a>.<\/p>\n\n<div class=\"tp-wrap tp-theme-extended_cards tp-day-setup-modern\" data-config=\"{&quot;rest&quot;:&quot;https:\\\/\\\/calais-dover.com\\\/fr\\\/wp-json\\\/timetables-pro\\\/v1\\\/timetables&quot;,&quot;route&quot;:98,&quot;days&quot;:1,&quot;autoload&quot;:true,&quot;theme&quot;:&quot;extended_cards&quot;,&quot;daySetup&quot;:&quot;modern&quot;,&quot;limit10&quot;:true,&quot;label&quot;:&quot;Dover - Calais&quot;,&quot;labels&quot;:{&quot;ui_date&quot;:&quot;Date&quot;,&quot;ui_days&quot;:&quot;Jours&quot;,&quot;ui_button&quot;:&quot;Afficher les travers\\u00e9es&quot;,&quot;status_idle&quot;:&quot;Choisissez une date et cliquez sur Afficher les travers\\u00e9es&quot;,&quot;status_loading&quot;:&quot;Chargement...&quot;,&quot;status_empty&quot;:&quot;Aucune travers\\u00e9e trouv\\u00e9e&quot;,&quot;status_found&quot;:&quot;%d travers\\u00e9es trouv\\u00e9es&quot;,&quot;th_date&quot;:&quot;Date&quot;,&quot;th_dep&quot;:&quot;D\\u00e9part&quot;,&quot;th_arr&quot;:&quot;Arriv\\u00e9e&quot;,&quot;th_dur&quot;:&quot;Dur\\u00e9e&quot;,&quot;th_ship&quot;:&quot;Navire&quot;,&quot;th_op&quot;:&quot;Op\\u00e9rateur&quot;,&quot;th_route&quot;:&quot;Route&quot;,&quot;wd_sun&quot;:&quot;DIM&quot;,&quot;wd_mon&quot;:&quot;LUN&quot;,&quot;wd_tue&quot;:&quot;MAR&quot;,&quot;wd_wed&quot;:&quot;MER&quot;,&quot;wd_thu&quot;:&quot;JEU&quot;,&quot;wd_fri&quot;:&quot;VEN&quot;,&quot;wd_sat&quot;:&quot;SAM&quot;,&quot;summary_footer_one&quot;:&quot;\\u00bb Voir tous les d\\u00e9parts %s dans notre horaire&quot;,&quot;summary_footer_two&quot;:&quot;\\u00bb Voir plus de d\\u00e9parts %1$s et %2$s dans notre horaire&quot;,&quot;summary_footer_generic&quot;:&quot;Consultez notre horaire pour plus de travers\\u00e9es&quot;,&quot;summary_header&quot;:&quot;Les deux prochains d\\u00e9parts :&quot;,&quot;summary_no_upcoming&quot;:&quot;Aucun prochain d\\u00e9part trouv\\u00e9&quot;,&quot;more_link&quot;:&quot;Plus de travers\\u00e9es&quot;,&quot;more_departures_button&quot;:&quot;Afficher plus de d\\u00e9parts&quot;,&quot;day_nav_previous&quot;:&quot;Afficher le jour pr\\u00e9c\\u00e9dent&quot;,&quot;day_nav_next&quot;:&quot;Afficher le jour suivant&quot;,&quot;day_nav_today&quot;:&quot;Afficher aujourd\\u2019hui&quot;,&quot;day_nav_tomorrow&quot;:&quot;Afficher demain&quot;,&quot;local_time_pair&quot;:&quot;Heure locale du port de d\\u00e9part : %1$s \\\/ Heure locale du port d&#039;arriv\\u00e9e : %2$s&quot;,&quot;local_time_combined&quot;:&quot;Heure locale des ports de d\\u00e9part et d&#039;arriv\\u00e9e : %s&quot;,&quot;tag_often_booked&quot;:&quot;\\u2b50 Souvent r\\u00e9serv\\u00e9&quot;,&quot;tag_leaving_soon&quot;:&quot;\\ud83d\\udd34 D\\u00e9part imminent&quot;,&quot;tag_currently_sailing&quot;:&quot;\\ud83d\\udfe2 En navigation&quot;,&quot;tag_arrived&quot;:&quot;\\u26ab Arriv\\u00e9&quot;,&quot;tag_overnight&quot;:&quot;Nuit \\u00e0 bord&quot;},&quot;showLogo&quot;:true,&quot;activityBoosters&quot;:true,&quot;shipMap&quot;:{&quot;p&amp;o ferries&quot;:&quot;P&amp;O Ferries&quot;,&quot;dfds&quot;:&quot;DFDS&quot;,&quot;irish ferries&quot;:&quot;Irish Ferries&quot;,&quot;norfolk line&quot;:&quot;DFDS&quot;},&quot;shipLinks&quot;:{&quot;p&amp;o ferries&quot;:&quot;https:\\\/\\\/calais-dover.com\\\/fr\\\/po-ferries\\\/&quot;,&quot;dfds&quot;:&quot;https:\\\/\\\/calais-dover.com\\\/fr\\\/dfds\\\/&quot;,&quot;irish ferries&quot;:&quot;https:\\\/\\\/calais-dover.com\\\/fr\\\/irish-ferries\\\/&quot;,&quot;norfolk line&quot;:&quot;https:\\\/\\\/calais-dover.com\\\/fr\\\/dfds\\\/&quot;},&quot;showShip&quot;:true,&quot;departurePort&quot;:&quot;Dover&quot;,&quot;arrivalPort&quot;:&quot;Calais&quot;,&quot;departureTimezone&quot;:&quot;Europe\\\/London&quot;,&quot;arrivalTimezone&quot;:&quot;Europe\\\/Paris&quot;}\">\n  <div class=\"tp-controls\" role=\"group\" aria-label=\"Timetable controls\">\n    <div>\n      <label for=\"tp-date\">Date<\/label><br>\n      <input id=\"tp-date\" class=\"tp-date\" type=\"date\" aria-label=\"Date\">\n    <\/div>\n    <div class=\"tp-day-field\">\n      <label for=\"tp-range\">Jours<\/label><br>\n      <select id=\"tp-range\" class=\"tp-range\" aria-label=\"Jours\">\n        <option value=\"1\">1<\/option><option value=\"3\">3<\/option><option value=\"7\">7<\/option><option value=\"14\">14<\/option>\n      <\/select>\n    <\/div>\n    <div><button id=\"tp-load\" class=\"tp-btn\">Afficher les travers\u00e9es<\/button><\/div>\n  <\/div>\n\n  <div class=\"tp-local-nav-row\">\n    <div class=\"tp-local-times\" id=\"tp-local-times\" aria-live=\"polite\"><\/div>\n    <div class=\"tp-day-nav-host\"><\/div>\n  <\/div>\n  <div id=\"tp-status\" class=\"tp-muted\" aria-live=\"polite\">Choisissez une date et cliquez sur Afficher les travers\u00e9es<\/div>\n  <div id=\"tp-results\"><\/div>\n\n  <div class=\"tp-loader\" aria-hidden=\"true\">\n    <div class=\"tp-loader-card\">\n      <div class=\"tp-spinner\" aria-hidden=\"true\"><\/div>\n      <div class=\"tp-loader-text\">Chargement...<\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\n(function(){\n  const wrap   = document.currentScript.previousElementSibling;\n  const cfg    = JSON.parse(wrap.getAttribute('data-config')||'{}');\n  const L      = cfg.labels||{};\n  const dateEl = wrap.querySelector('#tp-date');\n  const daysEl = wrap.querySelector('#tp-range');\n  const btn    = wrap.querySelector('#tp-load');\n  const status = wrap.querySelector('#tp-status');\n  const out    = wrap.querySelector('#tp-results');\n  const loader = wrap.querySelector('.tp-loader');\n  const controls = wrap.querySelector('.tp-controls');\n  const navHost = wrap.querySelector('.tp-day-nav-host');\n  const localTimesEl = wrap.querySelector('#tp-local-times');\n\n  const showShip = (cfg.showShip !== false && cfg.showShip !== 0 && cfg.showShip !== '0');\n  const activityBoosters = !!cfg.activityBoosters;\n  const daySetup = (cfg.daySetup === 'modern') ? 'modern' : 'classic';\n  const limit10 = !!cfg.limit10;\n  let featuredKey = '';\n  let soonKey = '';\n  let revealAll = false;\n\n  if (daySetup === 'modern') {\n    const dayField = wrap.querySelector('.tp-day-field');\n    if (dayField) dayField.style.display = 'none';\n  }\n\n  dateEl.valueAsDate = new Date();\n  dateEl.min = new Date().toISOString().split('T')[0];\n  Array.from(daysEl.options).forEach(o=>{ if(parseInt(o.value,10)===parseInt(cfg.days||7,10)) o.selected=true; });\n\n  function pad(n){ return String(n).padStart(2,'0'); }\n  function iso(d){ return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate()); }\n  function toLocal(s){ return new Date(s); }\n  function hhmm(d){ return d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}); }\n  function dShort(d){ return d.toLocaleDateString([], {day:'numeric', month:'short'}); } \/\/ short, no year\n  function dur(m){ const h=Math.floor(m\/60), r=m%60; return r? (h+'h '+r+'m') : (h+'h'); }\n\n  const WD = [\n    L.wd_sun || 'SUN',\n    L.wd_mon || 'MON',\n    L.wd_tue || 'TUE',\n    L.wd_wed || 'WED',\n    L.wd_thu || 'THU',\n    L.wd_fri || 'FRI',\n    L.wd_sat || 'SAT'\n  ];\n\n  function setLoader(v){ loader.classList.toggle('show', !!v); loader.setAttribute('aria-hidden', v?'false':'true'); }\n\n  function formatZoneTime(tz){\n    if (!tz) return '--:--';\n    try {\n      return new Intl.DateTimeFormat([], {hour:'2-digit', minute:'2-digit', hour12:false, timeZone: tz}).format(new Date());\n    } catch (e) {\n      return '--:--';\n    }\n  }\n\n  function renderLocalTimes(){\n    if (!localTimesEl) return;\n    if (!cfg.departureTimezone || !cfg.arrivalTimezone) {\n      localTimesEl.textContent = '';\n      return;\n    }\n    const depTime = formatZoneTime(cfg.departureTimezone);\n    const arrTime = formatZoneTime(cfg.arrivalTimezone);\n    const pairTpl = L.local_time_pair || 'Departure Port Local Time: %1$s \/ Arrival Port Local Time: %2$s';\n    const combinedTpl = L.local_time_combined || 'Departure & Arrival Port Local Time: %s';\n    if (depTime === arrTime) {\n      localTimesEl.textContent = combinedTpl.replace('%s', depTime);\n      return;\n    }\n    localTimesEl.textContent = pairTpl.replace('%1$s', depTime).replace('%2$s', arrTime);\n  }\n\n  function setStatusInline(html){\n    let box = wrap.querySelector('.tp-controls-status');\n    if (!box) {\n      box = document.createElement('div');\n      box.className = 'tp-controls-status';\n      controls.appendChild(box);\n    }\n    box.innerHTML = html || '';\n  }\n\n  function clearStatusInline(){\n    const box = wrap.querySelector('.tp-controls-status');\n    if (box) box.innerHTML = '';\n  }\n\n  function navHtml(){\n    if (daySetup !== 'modern') return '';\n    const base = new Date(dateEl.value || new Date());\n    const today = new Date();\n    const tomorrow = new Date(today);\n    tomorrow.setDate(today.getDate() + 1);\n    const isToday = base.toDateString() === today.toDateString();\nlet prevLabel = L.day_nav_previous || 'Show previous day';\nlet nextLabel = L.day_nav_next || 'Show next day';\nif (isToday) nextLabel = L.day_nav_tomorrow || 'Show tomorrow';\nif (base.toDateString() === tomorrow.toDateString()) prevLabel = L.day_nav_today || 'Show today';\n    return '<div class=\"tp-day-nav\">'\n      + (isToday ? '' : '<a href=\"#\" data-shift=\"-1\">'+prevLabel+'<\/a>')\n      + '<a href=\"#\" data-shift=\"1\">'+nextLabel+'<\/a>'\n      + '<\/div>';\n  }\n\n  function shiftDate(delta){\n    const base = new Date(dateEl.value || new Date());\n    base.setDate(base.getDate() + delta);\n    dateEl.value = iso(base);\n    load();\n  }\n\n  function moreButtonHtml(hiddenCount){\n    if (!limit10 || revealAll || hiddenCount <= 0) return '';\n    return '<div style=\"margin-top:12px;text-align:center\">'\n      + '<button type=\"button\" class=\"tp-btn tp-more-btn\">'+(L.more_departures_button || 'Show more departures')+' ('+hiddenCount+')<\/button>'\n      + '<\/div>';\n  }\n\n  function opCell(r){\n    const name = r.opName || ('Operator '+(r.opId||''));\n    const logo = (cfg.showLogo && r.opLogo) ? '<img decoding=\"async\" src=\"'+r.opLogo+'\" alt=\"'+name+'\"> ' : '';\n    const label = logo + '<span>'+name+'<\/span>';\n    return r.opLink ? '<a class=\"tp-op\" href=\"'+r.opLink+'\" target=\"_blank\" rel=\"nofollow noopener\">'+label+'<\/a>' : '<span class=\"tp-op\">'+label+'<\/span>';\n  }\n\n  function shipInfo(original){\n    const key = (original||'').toLowerCase().trim();\n    const label = (cfg.shipMap && cfg.shipMap[key]) || original || '';\n    const href  = (cfg.shipLinks && cfg.shipLinks[key]) || '';\n    return {label, href};\n  }\n\n  function shipBadge(original){\n    const s = shipInfo(original);\n    const badge = '<span class=\"tp-badge\">'+(s.label||'')+'<\/span>';\n    return s.href ? ('<a href=\"'+s.href+'\" target=\"_blank\" rel=\"nofollow noopener\">'+badge+'<\/a>') : badge;\n  }\n\n  function rowKey(r){\n    return [String(r.opId||''), String(+r.dep||0), String(+r.arr||0), String(r.ship||'')].join('|');\n  }\n\n  function pickFeaturedKey(rows, avoidKey){\n    if (!activityBoosters || !rows.length) return '';\n    const routeSeed = parseInt(cfg.route, 10) || 0;\n    const dateSeed = parseInt(String(dateEl.value || '').replace(\/-\/g,''), 10) || 0;\n\n    let pool = rows;\n    if (avoidKey) {\n      const soonIdx = rows.findIndex(r => rowKey(r) === avoidKey);\n      if (soonIdx >= 0) {\n        const later = rows.slice(soonIdx + 1).filter(r => rowKey(r) !== avoidKey);\n        pool = later.length ? later : rows.filter(r => rowKey(r) !== avoidKey);\n      } else {\n        pool = rows.filter(r => rowKey(r) !== avoidKey);\n      }\n    }\n\n    if (!pool.length) return '';\n    const idx = Math.abs((routeSeed + dateSeed + rows.length) % pool.length);\n    return rowKey(pool[idx]);\n  }\n\n  function pickSoonKey(rows){\n    if (!activityBoosters || !rows.length) return '';\n    const now = Date.now();\n    const todayStr = new Date(now).toDateString();\n    const firstUpcoming = rows.find(r => (r.dep instanceof Date) && r.dep.getTime() > now && r.dep.toDateString() === todayStr);\n    return firstUpcoming ? rowKey(firstUpcoming) : '';\n  }\n\n  function boostersFor(r){\n    if (!activityBoosters) return [];\n    const now = Date.now();\n    const depMs = (r.dep instanceof Date) ? r.dep.getTime() : 0;\n    const arrMs = (r.arr instanceof Date) ? r.arr.getTime() : 0;\n    const depIsToday = (r.dep instanceof Date) && (new Date(now).toDateString() === r.dep.toDateString());\n\n    if (depMs > 0 && arrMs > 0 && depMs <= now && arrMs > now) {\n      return [{cls:'tp-booster-sailing', text:(L.tag_currently_sailing || '\ud83d\udfe2 Currently sailing')}];\n    }\n    if (depIsToday && arrMs > 0 && arrMs <= now) {\n      return [{cls:'tp-booster-arrived', text:(L.tag_arrived || '\u26ab Arrived')}];\n    }\n    if (rowKey(r) === soonKey) return [{cls:'tp-booster-soon', text:(L.tag_leaving_soon || '\ud83d\udd34 Leaving soon')}];\n    if (r.dep && r.arr && r.dep.toDateString() !== r.arr.toDateString()) return [{cls:'tp-booster-overnight', text:(L.tag_overnight || 'Overnight')}];\n\n    return [];\n  }\n\n  function boostersHtml(r, floating){\n    const tags = boostersFor(r);\n    if (!tags.length) return '';\n    const cls = floating ? 'tp-boosters tp-boosters-float' : 'tp-boosters';\n    return '<div class=\"'+cls+'\">' + tags.map(t => '<span class=\"tp-booster '+t.cls+'\">'+boosterLabelHtml(t.text)+'<\/span>').join('') + '<\/div>';\n  }\n\n  function boosterLabelHtml(text){\n    return String(text || '').replace(\/^(\ud83d\udd34|\ud83d\udfe2|\u26ab|\u2b50)\\s*\/, '<span class=\"tp-booster-emoji\">$1<\/span>');\n  }\n\n  function routeTicketHtml(label){\n    const raw = String(label || '').trim();\n    if (!raw) return '';\n    if (cfg.theme !== 'small_cards') return escAttr(raw);\n    const parts = raw.split(\/\\s*[\\-\u2013]\\s*\/, 2);\n    if (parts.length < 2) return escAttr(raw);\n    return escAttr(parts[0]) + '<br>' + escAttr(parts[1]);\n  }\n\n  function escAttr(v){\n    return String(v||'').replace(\/[&<>\"']\/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[m]));\n  }\n\n  function bindRowLinks(){\n    if (cfg.theme !== 'max_clickouts') return;\n    out.querySelectorAll('tr.tp-row-link').forEach((row)=>{\n      row.setAttribute('role','link');\n      row.setAttribute('tabindex','0');\n      row.addEventListener('click', (e)=>{\n        if (e.target && e.target.closest('a')) return;\n        const href = row.getAttribute('data-row-link');\n        if (!href) return;\n        window.open(href, '_blank', 'noopener,noreferrer');\n      });\n      row.addEventListener('keydown', (e)=>{\n        if (e.key !== 'Enter' && e.key !== ' ') return;\n        e.preventDefault();\n        row.click();\n      });\n    });\n  }\n\n  function renderTable(rows){\n    const th = {date:L.th_date,dep:L.th_dep,arr:L.th_arr,dur:L.th_dur,ship:L.th_ship,op:L.th_op};\n\n    const headCells = [\n      '<th>'+th.date+'<\/th>',\n      '<th>'+th.dep+'<\/th>',\n      '<th>'+th.arr+'<\/th>',\n      '<th>'+th.dur+'<\/th>'\n    ];\n    if (showShip) {\n      headCells.push('<th>'+th.ship+'<\/th>');\n    }\n    headCells.push('<th>'+th.op+'<\/th>');\n\n    let html = '<table class=\"tp-table\"><thead><tr>'+headCells.join('')+'<\/tr><\/thead><tbody>';\n\n    rows.forEach(r=>{\n      const dow = WD[r.dep.getDay()] || '';\n      const clickable = (cfg.theme === 'max_clickouts' && r.opLink);\n      const rowAttr = clickable ? (' class=\"tp-row-link\" data-row-link=\"'+escAttr(r.opLink)+'\"') : '';\n      const cells = [\n        '<td>'+dShort(r.dep)+' <span class=\"tp-day\">'+dow+'<\/span>'+boostersHtml(r, false)+'<\/td>',\n        '<td>'+hhmm(r.dep)+'<\/td>',\n        '<td>'+hhmm(r.arr)+'<\/td>',\n        '<td>'+dur(r.min)+'<\/td>'\n      ];\n      if (showShip) {\n        cells.push('<td>'+shipBadge(r.ship)+'<\/td>');\n      }\n      cells.push('<td>'+opCell(r)+'<\/td>');\n      html += '<tr'+rowAttr+'>'+cells.join('')+'<\/tr>';\n    });\n    html += '<\/tbody><\/table>';\n    return html;\n  }\n\n  function renderCards(rows){\n    if (cfg.theme === 'extended_cards') {\n      return renderExtendedCards(rows);\n    }\n\n    const routeName = cfg.label || '';\n    let html = '<div class=\"tp-cardlist\">';\n    rows.forEach(r=>{\n      const dow = WD[r.dep.getDay()] || '';\n      const times = hhmm(r.dep) + ' <span class=\"tp-arrow\">\u2192<\/span> ' + hhmm(r.arr);\n      html += '<div class=\"tp-card\">'+boostersHtml(r, true)+\n        \/\/ Row 1: Date + weekday + times\n        '<div class=\"tp-mrow\">'+\n          '<div class=\"lhs\"><span class=\"tp-sub\">'+dShort(r.dep)+'<\/span><span class=\"tp-day\">'+dow+'<\/span><\/div>'+\n          '<div class=\"rhs\"><span class=\"tp-time\">'+times+'<\/span><\/div>'+\n        '<\/div>';\n\n      if (showShip) {\n        html +=\n        \/\/ Row 2: Ship\n        '<div class=\"tp-mrow\">'+\n          '<div class=\"lhs\">'+(showShip ? shipBadge(r.ship) : '')+'<\/div>'+\n          '<div class=\"rhs\"><\/div>'+\n        '<\/div>';\n      }\n\n      html +=\n        \/\/ Row 3: Operator + route label (no book button)\n        '<div class=\"tp-mrow\">'+\n          '<div class=\"lhs\">'+opCell(r)+'<\/div>'+\n          (routeName ? ('<div class=\"rhs\"><span class=\"tp-route-ticket\">'+routeTicketHtml(routeName)+'<\/span><\/div>') : '<div class=\"rhs\"><\/div>')+\n        '<\/div>'+\n      '<\/div>';\n    });\n    html += '<\/div>';\n    return html;\n  }\n\n  function renderExtendedCards(rows){\n    const routeName = cfg.label || '';\n    const ctaText = L.more_link || 'More sailings';\n    let html = '<div class=\"tp-cardlist\">';\n    rows.forEach(r=>{\n      const dow = WD[r.dep.getDay()] || '';\n      const link = r.opLink ? ('<a class=\"tp-ext-cta\" href=\"'+r.opLink+'\" target=\"_blank\" rel=\"nofollow noopener\">'+ctaText+' <span class=\"tp-ext-cta-arrow\">\u00bb<\/span><\/a>') : '<span class=\"tp-ext-cta\">'+ctaText+' <span class=\"tp-ext-cta-arrow\">\u00bb<\/span><\/span>';\n      html += '<div class=\"tp-ext-card\">'\n        + boostersHtml(r, true)\n        + '<div class=\"tp-ext-left\">'\n        +   '<div>'+opCell(r)+'<\/div>'\n        +   (showShip ? ('<div>'+shipBadge(r.ship)+'<\/div>') : '')\n        + '<\/div>'\n        + '<div class=\"tp-ext-mid\">'\n        +   '<div class=\"tp-ext-top\"><span class=\"tp-sub\">'+dShort(r.dep)+' <span class=\"tp-day\">'+dow+'<\/span><\/span><span class=\"tp-ext-dur\">'+dur(r.min)+'<\/span><\/div>'\n        +   '<div class=\"tp-ext-times\"><span class=\"tp-ext-time\">'+hhmm(r.dep)+'<\/span><span class=\"tp-ext-line\"><\/span><span class=\"tp-ext-dur\">'+dur(r.min)+'<\/span><span class=\"tp-ext-line\"><\/span><span class=\"tp-ext-time\">'+hhmm(r.arr)+'<\/span><\/div>'\n        +   (routeName ? ('<div class=\"tp-ext-route\">'+routeName+'<\/div>') : '')\n        + '<\/div>'\n        + '<div class=\"tp-ext-right\">'+link+'<\/div>'\n        + '<\/div>';\n    });\n    html += '<\/div>';\n    return html;\n  }\n\n  async function load(){\n    btn.disabled = true; setLoader(true); status.textContent = L.status_loading;\n    const start = new Date(dateEl.value || new Date());\n    const days  = (daySetup === 'modern') ? 1 : Math.max(1, parseInt(daysEl.value,10)||1);\n    const end   = new Date(start); end.setDate(start.getDate()+days-1);\n    const url = new URL(cfg.rest); url.searchParams.set('route', String(cfg.route));\n    url.searchParams.set('from', iso(start)); url.searchParams.set('to', iso(end));\n    try{\n      const res = await fetch(url.toString(), {credentials:'same-origin'});\n      if(!res.ok) throw new Error('HTTP '+res.status);\n      const json = await res.json();\n      const list = (json && json.data && Array.isArray(json.data.rows)) ? json.data.rows : [];\n      let rows = list.map(t=>({\n        opId:t.operatorId||null, opName:t.operatorName||'', opLogo:t.operatorLogo||'', opLink:t.operatorLink||'',\n        dep:toLocal(t.departureTime), arr:toLocal(t.arrivalTime), min:t.durationInMinutes||0, ship:t.shipName||''\n      })).sort((a,b)=>a.dep-b.dep);\n      const todayStr = new Date().toDateString();\n      const nowMs = Date.now();\n      const arrivedToday = rows\n        .filter(r => (r.dep instanceof Date) && (r.arr instanceof Date) && r.dep.toDateString() === todayStr && r.arr.getTime() <= nowMs)\n        .sort((a,b) => b.arr - a.arr);\n      if (arrivedToday.length > 2) {\n        const keep = new Set(arrivedToday.slice(0,2).map(rowKey));\n        rows = rows.filter(r => {\n          const isArrivedToday = (r.dep instanceof Date) && (r.arr instanceof Date) && r.dep.toDateString() === todayStr && r.arr.getTime() <= nowMs;\n          return !isArrivedToday || keep.has(rowKey(r));\n        });\n      }\n      if(rows.length===0){ status.textContent = L.status_empty; clearStatusInline(); out.innerHTML=''; return; }\n      const visibleRows = (limit10 && !revealAll && rows.length > 10) ? rows.slice(0,10) : rows;\n      const hiddenCount = rows.length - visibleRows.length;\n      soonKey = pickSoonKey(visibleRows);\n      featuredKey = pickFeaturedKey(visibleRows, soonKey);\n      status.textContent = '';\n      setStatusInline(\n  (L.status_found || '%d sailings found').replace('%d', '<strong>'+rows.length+'<\/strong>')\n);\n      out.innerHTML = renderTable(visibleRows) + renderCards(visibleRows) + moreButtonHtml(hiddenCount);\n      bindRowLinks();\n      if (navHost) navHost.innerHTML = navHtml();\n    }catch(e){\n      status.textContent = 'Failed to load data'; out.innerHTML = '<pre>'+String(e.message||e)+'<\/pre>';\n    }finally{ setLoader(false); btn.disabled=false; }\n  }\n\n  wrap.addEventListener('click', function(e){\n    const nav = e.target.closest('.tp-day-nav a[data-shift]');\n    if (nav) {\n      e.preventDefault();\n      shiftDate(parseInt(nav.getAttribute('data-shift'),10) || 0);\n      return;\n    }\n    const more = e.target.closest('.tp-more-btn');\n    if (more) {\n      e.preventDefault();\n      revealAll = true;\n      load();\n    }\n  });\n\n  btn.addEventListener('click', load);\n  if (navHost) navHost.innerHTML = navHtml();\n  renderLocalTimes();\n  setInterval(renderLocalTimes, 30000);\n  if (cfg.autoload) load();\n})();\n<\/script>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"calais-folkestone-eurotunnel-timetable\">Le Shuttle Calais-Folkestone<\/h2>\n\n\n<p class=\"wp-block-paragraph\">Voici les d\u00e9parts en direct de <a href=\"https:\/\/calais-dover.com\/fr\/eurotunnel\/\">Le Shuttle \/ Eurotunnel<\/a> de Calais vers Folkestone. C&rsquo;est la travers\u00e9e la plus rapide de cette ligne pour les voitures, utilitaires, motos, camping-cars et poids lourds.<\/p>\n\n<div class=\"tp-wrap tp-theme-extended_cards tp-day-setup-modern\" data-config=\"{&quot;rest&quot;:&quot;https:\\\/\\\/calais-dover.com\\\/fr\\\/wp-json\\\/timetables-pro\\\/v1\\\/timetables&quot;,&quot;route&quot;:8,&quot;days&quot;:1,&quot;autoload&quot;:true,&quot;theme&quot;:&quot;extended_cards&quot;,&quot;daySetup&quot;:&quot;modern&quot;,&quot;limit10&quot;:true,&quot;label&quot;:&quot;Calais - Folkestone&quot;,&quot;labels&quot;:{&quot;ui_date&quot;:&quot;Date&quot;,&quot;ui_days&quot;:&quot;Jours&quot;,&quot;ui_button&quot;:&quot;Afficher les travers\\u00e9es&quot;,&quot;status_idle&quot;:&quot;Choisissez une date et cliquez sur Afficher les travers\\u00e9es&quot;,&quot;status_loading&quot;:&quot;Chargement...&quot;,&quot;status_empty&quot;:&quot;Aucune travers\\u00e9e trouv\\u00e9e&quot;,&quot;status_found&quot;:&quot;%d travers\\u00e9es trouv\\u00e9es&quot;,&quot;th_date&quot;:&quot;Date&quot;,&quot;th_dep&quot;:&quot;D\\u00e9part&quot;,&quot;th_arr&quot;:&quot;Arriv\\u00e9e&quot;,&quot;th_dur&quot;:&quot;Dur\\u00e9e&quot;,&quot;th_ship&quot;:&quot;Navire&quot;,&quot;th_op&quot;:&quot;Op\\u00e9rateur&quot;,&quot;th_route&quot;:&quot;Route&quot;,&quot;wd_sun&quot;:&quot;DIM&quot;,&quot;wd_mon&quot;:&quot;LUN&quot;,&quot;wd_tue&quot;:&quot;MAR&quot;,&quot;wd_wed&quot;:&quot;MER&quot;,&quot;wd_thu&quot;:&quot;JEU&quot;,&quot;wd_fri&quot;:&quot;VEN&quot;,&quot;wd_sat&quot;:&quot;SAM&quot;,&quot;summary_footer_one&quot;:&quot;\\u00bb Voir tous les d\\u00e9parts %s dans notre horaire&quot;,&quot;summary_footer_two&quot;:&quot;\\u00bb Voir plus de d\\u00e9parts %1$s et %2$s dans notre horaire&quot;,&quot;summary_footer_generic&quot;:&quot;Consultez notre horaire pour plus de travers\\u00e9es&quot;,&quot;summary_header&quot;:&quot;Les deux prochains d\\u00e9parts :&quot;,&quot;summary_no_upcoming&quot;:&quot;Aucun prochain d\\u00e9part trouv\\u00e9&quot;,&quot;more_link&quot;:&quot;Plus de travers\\u00e9es&quot;,&quot;more_departures_button&quot;:&quot;Afficher plus de d\\u00e9parts&quot;,&quot;day_nav_previous&quot;:&quot;Afficher le jour pr\\u00e9c\\u00e9dent&quot;,&quot;day_nav_next&quot;:&quot;Afficher le jour suivant&quot;,&quot;day_nav_today&quot;:&quot;Afficher aujourd\\u2019hui&quot;,&quot;day_nav_tomorrow&quot;:&quot;Afficher demain&quot;,&quot;local_time_pair&quot;:&quot;Heure locale du port de d\\u00e9part : %1$s \\\/ Heure locale du port d&#039;arriv\\u00e9e : %2$s&quot;,&quot;local_time_combined&quot;:&quot;Heure locale des ports de d\\u00e9part et d&#039;arriv\\u00e9e : %s&quot;,&quot;tag_often_booked&quot;:&quot;\\u2b50 Souvent r\\u00e9serv\\u00e9&quot;,&quot;tag_leaving_soon&quot;:&quot;\\ud83d\\udd34 D\\u00e9part imminent&quot;,&quot;tag_currently_sailing&quot;:&quot;\\ud83d\\udfe2 En navigation&quot;,&quot;tag_arrived&quot;:&quot;\\u26ab Arriv\\u00e9&quot;,&quot;tag_overnight&quot;:&quot;Nuit \\u00e0 bord&quot;},&quot;showLogo&quot;:true,&quot;activityBoosters&quot;:true,&quot;shipMap&quot;:{&quot;eurotunnel&quot;:&quot;Eurotunnel&quot;},&quot;shipLinks&quot;:{&quot;eurotunnel&quot;:&quot;https:\\\/\\\/calais-dover.com\\\/fr\\\/eurotunnel\\\/&quot;},&quot;showShip&quot;:false,&quot;departurePort&quot;:&quot;Calais&quot;,&quot;arrivalPort&quot;:&quot;Folkestone&quot;,&quot;departureTimezone&quot;:&quot;Europe\\\/Paris&quot;,&quot;arrivalTimezone&quot;:&quot;Europe\\\/London&quot;}\">\n  <div class=\"tp-controls\" role=\"group\" aria-label=\"Timetable controls\">\n    <div>\n      <label for=\"tp-date\">Date<\/label><br>\n      <input id=\"tp-date\" class=\"tp-date\" type=\"date\" aria-label=\"Date\">\n    <\/div>\n    <div class=\"tp-day-field\">\n      <label for=\"tp-range\">Jours<\/label><br>\n      <select id=\"tp-range\" class=\"tp-range\" aria-label=\"Jours\">\n        <option value=\"1\">1<\/option><option value=\"3\">3<\/option><option value=\"7\">7<\/option><option value=\"14\">14<\/option>\n      <\/select>\n    <\/div>\n    <div><button id=\"tp-load\" class=\"tp-btn\">Afficher les travers\u00e9es<\/button><\/div>\n  <\/div>\n\n  <div class=\"tp-local-nav-row\">\n    <div class=\"tp-local-times\" id=\"tp-local-times\" aria-live=\"polite\"><\/div>\n    <div class=\"tp-day-nav-host\"><\/div>\n  <\/div>\n  <div id=\"tp-status\" class=\"tp-muted\" aria-live=\"polite\">Choisissez une date et cliquez sur Afficher les travers\u00e9es<\/div>\n  <div id=\"tp-results\"><\/div>\n\n  <div class=\"tp-loader\" aria-hidden=\"true\">\n    <div class=\"tp-loader-card\">\n      <div class=\"tp-spinner\" aria-hidden=\"true\"><\/div>\n      <div class=\"tp-loader-text\">Chargement...<\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\n(function(){\n  const wrap   = document.currentScript.previousElementSibling;\n  const cfg    = JSON.parse(wrap.getAttribute('data-config')||'{}');\n  const L      = cfg.labels||{};\n  const dateEl = wrap.querySelector('#tp-date');\n  const daysEl = wrap.querySelector('#tp-range');\n  const btn    = wrap.querySelector('#tp-load');\n  const status = wrap.querySelector('#tp-status');\n  const out    = wrap.querySelector('#tp-results');\n  const loader = wrap.querySelector('.tp-loader');\n  const controls = wrap.querySelector('.tp-controls');\n  const navHost = wrap.querySelector('.tp-day-nav-host');\n  const localTimesEl = wrap.querySelector('#tp-local-times');\n\n  const showShip = (cfg.showShip !== false && cfg.showShip !== 0 && cfg.showShip !== '0');\n  const activityBoosters = !!cfg.activityBoosters;\n  const daySetup = (cfg.daySetup === 'modern') ? 'modern' : 'classic';\n  const limit10 = !!cfg.limit10;\n  let featuredKey = '';\n  let soonKey = '';\n  let revealAll = false;\n\n  if (daySetup === 'modern') {\n    const dayField = wrap.querySelector('.tp-day-field');\n    if (dayField) dayField.style.display = 'none';\n  }\n\n  dateEl.valueAsDate = new Date();\n  dateEl.min = new Date().toISOString().split('T')[0];\n  Array.from(daysEl.options).forEach(o=>{ if(parseInt(o.value,10)===parseInt(cfg.days||7,10)) o.selected=true; });\n\n  function pad(n){ return String(n).padStart(2,'0'); }\n  function iso(d){ return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate()); }\n  function toLocal(s){ return new Date(s); }\n  function hhmm(d){ return d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}); }\n  function dShort(d){ return d.toLocaleDateString([], {day:'numeric', month:'short'}); } \/\/ short, no year\n  function dur(m){ const h=Math.floor(m\/60), r=m%60; return r? (h+'h '+r+'m') : (h+'h'); }\n\n  const WD = [\n    L.wd_sun || 'SUN',\n    L.wd_mon || 'MON',\n    L.wd_tue || 'TUE',\n    L.wd_wed || 'WED',\n    L.wd_thu || 'THU',\n    L.wd_fri || 'FRI',\n    L.wd_sat || 'SAT'\n  ];\n\n  function setLoader(v){ loader.classList.toggle('show', !!v); loader.setAttribute('aria-hidden', v?'false':'true'); }\n\n  function formatZoneTime(tz){\n    if (!tz) return '--:--';\n    try {\n      return new Intl.DateTimeFormat([], {hour:'2-digit', minute:'2-digit', hour12:false, timeZone: tz}).format(new Date());\n    } catch (e) {\n      return '--:--';\n    }\n  }\n\n  function renderLocalTimes(){\n    if (!localTimesEl) return;\n    if (!cfg.departureTimezone || !cfg.arrivalTimezone) {\n      localTimesEl.textContent = '';\n      return;\n    }\n    const depTime = formatZoneTime(cfg.departureTimezone);\n    const arrTime = formatZoneTime(cfg.arrivalTimezone);\n    const pairTpl = L.local_time_pair || 'Departure Port Local Time: %1$s \/ Arrival Port Local Time: %2$s';\n    const combinedTpl = L.local_time_combined || 'Departure & Arrival Port Local Time: %s';\n    if (depTime === arrTime) {\n      localTimesEl.textContent = combinedTpl.replace('%s', depTime);\n      return;\n    }\n    localTimesEl.textContent = pairTpl.replace('%1$s', depTime).replace('%2$s', arrTime);\n  }\n\n  function setStatusInline(html){\n    let box = wrap.querySelector('.tp-controls-status');\n    if (!box) {\n      box = document.createElement('div');\n      box.className = 'tp-controls-status';\n      controls.appendChild(box);\n    }\n    box.innerHTML = html || '';\n  }\n\n  function clearStatusInline(){\n    const box = wrap.querySelector('.tp-controls-status');\n    if (box) box.innerHTML = '';\n  }\n\n  function navHtml(){\n    if (daySetup !== 'modern') return '';\n    const base = new Date(dateEl.value || new Date());\n    const today = new Date();\n    const tomorrow = new Date(today);\n    tomorrow.setDate(today.getDate() + 1);\n    const isToday = base.toDateString() === today.toDateString();\nlet prevLabel = L.day_nav_previous || 'Show previous day';\nlet nextLabel = L.day_nav_next || 'Show next day';\nif (isToday) nextLabel = L.day_nav_tomorrow || 'Show tomorrow';\nif (base.toDateString() === tomorrow.toDateString()) prevLabel = L.day_nav_today || 'Show today';\n    return '<div class=\"tp-day-nav\">'\n      + (isToday ? '' : '<a href=\"#\" data-shift=\"-1\">'+prevLabel+'<\/a>')\n      + '<a href=\"#\" data-shift=\"1\">'+nextLabel+'<\/a>'\n      + '<\/div>';\n  }\n\n  function shiftDate(delta){\n    const base = new Date(dateEl.value || new Date());\n    base.setDate(base.getDate() + delta);\n    dateEl.value = iso(base);\n    load();\n  }\n\n  function moreButtonHtml(hiddenCount){\n    if (!limit10 || revealAll || hiddenCount <= 0) return '';\n    return '<div style=\"margin-top:12px;text-align:center\">'\n      + '<button type=\"button\" class=\"tp-btn tp-more-btn\">'+(L.more_departures_button || 'Show more departures')+' ('+hiddenCount+')<\/button>'\n      + '<\/div>';\n  }\n\n  function opCell(r){\n    const name = r.opName || ('Operator '+(r.opId||''));\n    const logo = (cfg.showLogo && r.opLogo) ? '<img decoding=\"async\" src=\"'+r.opLogo+'\" alt=\"'+name+'\"> ' : '';\n    const label = logo + '<span>'+name+'<\/span>';\n    return r.opLink ? '<a class=\"tp-op\" href=\"'+r.opLink+'\" target=\"_blank\" rel=\"nofollow noopener\">'+label+'<\/a>' : '<span class=\"tp-op\">'+label+'<\/span>';\n  }\n\n  function shipInfo(original){\n    const key = (original||'').toLowerCase().trim();\n    const label = (cfg.shipMap && cfg.shipMap[key]) || original || '';\n    const href  = (cfg.shipLinks && cfg.shipLinks[key]) || '';\n    return {label, href};\n  }\n\n  function shipBadge(original){\n    const s = shipInfo(original);\n    const badge = '<span class=\"tp-badge\">'+(s.label||'')+'<\/span>';\n    return s.href ? ('<a href=\"'+s.href+'\" target=\"_blank\" rel=\"nofollow noopener\">'+badge+'<\/a>') : badge;\n  }\n\n  function rowKey(r){\n    return [String(r.opId||''), String(+r.dep||0), String(+r.arr||0), String(r.ship||'')].join('|');\n  }\n\n  function pickFeaturedKey(rows, avoidKey){\n    if (!activityBoosters || !rows.length) return '';\n    const routeSeed = parseInt(cfg.route, 10) || 0;\n    const dateSeed = parseInt(String(dateEl.value || '').replace(\/-\/g,''), 10) || 0;\n\n    let pool = rows;\n    if (avoidKey) {\n      const soonIdx = rows.findIndex(r => rowKey(r) === avoidKey);\n      if (soonIdx >= 0) {\n        const later = rows.slice(soonIdx + 1).filter(r => rowKey(r) !== avoidKey);\n        pool = later.length ? later : rows.filter(r => rowKey(r) !== avoidKey);\n      } else {\n        pool = rows.filter(r => rowKey(r) !== avoidKey);\n      }\n    }\n\n    if (!pool.length) return '';\n    const idx = Math.abs((routeSeed + dateSeed + rows.length) % pool.length);\n    return rowKey(pool[idx]);\n  }\n\n  function pickSoonKey(rows){\n    if (!activityBoosters || !rows.length) return '';\n    const now = Date.now();\n    const todayStr = new Date(now).toDateString();\n    const firstUpcoming = rows.find(r => (r.dep instanceof Date) && r.dep.getTime() > now && r.dep.toDateString() === todayStr);\n    return firstUpcoming ? rowKey(firstUpcoming) : '';\n  }\n\n  function boostersFor(r){\n    if (!activityBoosters) return [];\n    const now = Date.now();\n    const depMs = (r.dep instanceof Date) ? r.dep.getTime() : 0;\n    const arrMs = (r.arr instanceof Date) ? r.arr.getTime() : 0;\n    const depIsToday = (r.dep instanceof Date) && (new Date(now).toDateString() === r.dep.toDateString());\n\n    if (depMs > 0 && arrMs > 0 && depMs <= now && arrMs > now) {\n      return [{cls:'tp-booster-sailing', text:(L.tag_currently_sailing || '\ud83d\udfe2 Currently sailing')}];\n    }\n    if (depIsToday && arrMs > 0 && arrMs <= now) {\n      return [{cls:'tp-booster-arrived', text:(L.tag_arrived || '\u26ab Arrived')}];\n    }\n    if (rowKey(r) === soonKey) return [{cls:'tp-booster-soon', text:(L.tag_leaving_soon || '\ud83d\udd34 Leaving soon')}];\n    if (r.dep && r.arr && r.dep.toDateString() !== r.arr.toDateString()) return [{cls:'tp-booster-overnight', text:(L.tag_overnight || 'Overnight')}];\n\n    return [];\n  }\n\n  function boostersHtml(r, floating){\n    const tags = boostersFor(r);\n    if (!tags.length) return '';\n    const cls = floating ? 'tp-boosters tp-boosters-float' : 'tp-boosters';\n    return '<div class=\"'+cls+'\">' + tags.map(t => '<span class=\"tp-booster '+t.cls+'\">'+boosterLabelHtml(t.text)+'<\/span>').join('') + '<\/div>';\n  }\n\n  function boosterLabelHtml(text){\n    return String(text || '').replace(\/^(\ud83d\udd34|\ud83d\udfe2|\u26ab|\u2b50)\\s*\/, '<span class=\"tp-booster-emoji\">$1<\/span>');\n  }\n\n  function routeTicketHtml(label){\n    const raw = String(label || '').trim();\n    if (!raw) return '';\n    if (cfg.theme !== 'small_cards') return escAttr(raw);\n    const parts = raw.split(\/\\s*[\\-\u2013]\\s*\/, 2);\n    if (parts.length < 2) return escAttr(raw);\n    return escAttr(parts[0]) + '<br>' + escAttr(parts[1]);\n  }\n\n  function escAttr(v){\n    return String(v||'').replace(\/[&<>\"']\/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[m]));\n  }\n\n  function bindRowLinks(){\n    if (cfg.theme !== 'max_clickouts') return;\n    out.querySelectorAll('tr.tp-row-link').forEach((row)=>{\n      row.setAttribute('role','link');\n      row.setAttribute('tabindex','0');\n      row.addEventListener('click', (e)=>{\n        if (e.target && e.target.closest('a')) return;\n        const href = row.getAttribute('data-row-link');\n        if (!href) return;\n        window.open(href, '_blank', 'noopener,noreferrer');\n      });\n      row.addEventListener('keydown', (e)=>{\n        if (e.key !== 'Enter' && e.key !== ' ') return;\n        e.preventDefault();\n        row.click();\n      });\n    });\n  }\n\n  function renderTable(rows){\n    const th = {date:L.th_date,dep:L.th_dep,arr:L.th_arr,dur:L.th_dur,ship:L.th_ship,op:L.th_op};\n\n    const headCells = [\n      '<th>'+th.date+'<\/th>',\n      '<th>'+th.dep+'<\/th>',\n      '<th>'+th.arr+'<\/th>',\n      '<th>'+th.dur+'<\/th>'\n    ];\n    if (showShip) {\n      headCells.push('<th>'+th.ship+'<\/th>');\n    }\n    headCells.push('<th>'+th.op+'<\/th>');\n\n    let html = '<table class=\"tp-table\"><thead><tr>'+headCells.join('')+'<\/tr><\/thead><tbody>';\n\n    rows.forEach(r=>{\n      const dow = WD[r.dep.getDay()] || '';\n      const clickable = (cfg.theme === 'max_clickouts' && r.opLink);\n      const rowAttr = clickable ? (' class=\"tp-row-link\" data-row-link=\"'+escAttr(r.opLink)+'\"') : '';\n      const cells = [\n        '<td>'+dShort(r.dep)+' <span class=\"tp-day\">'+dow+'<\/span>'+boostersHtml(r, false)+'<\/td>',\n        '<td>'+hhmm(r.dep)+'<\/td>',\n        '<td>'+hhmm(r.arr)+'<\/td>',\n        '<td>'+dur(r.min)+'<\/td>'\n      ];\n      if (showShip) {\n        cells.push('<td>'+shipBadge(r.ship)+'<\/td>');\n      }\n      cells.push('<td>'+opCell(r)+'<\/td>');\n      html += '<tr'+rowAttr+'>'+cells.join('')+'<\/tr>';\n    });\n    html += '<\/tbody><\/table>';\n    return html;\n  }\n\n  function renderCards(rows){\n    if (cfg.theme === 'extended_cards') {\n      return renderExtendedCards(rows);\n    }\n\n    const routeName = cfg.label || '';\n    let html = '<div class=\"tp-cardlist\">';\n    rows.forEach(r=>{\n      const dow = WD[r.dep.getDay()] || '';\n      const times = hhmm(r.dep) + ' <span class=\"tp-arrow\">\u2192<\/span> ' + hhmm(r.arr);\n      html += '<div class=\"tp-card\">'+boostersHtml(r, true)+\n        \/\/ Row 1: Date + weekday + times\n        '<div class=\"tp-mrow\">'+\n          '<div class=\"lhs\"><span class=\"tp-sub\">'+dShort(r.dep)+'<\/span><span class=\"tp-day\">'+dow+'<\/span><\/div>'+\n          '<div class=\"rhs\"><span class=\"tp-time\">'+times+'<\/span><\/div>'+\n        '<\/div>';\n\n      if (showShip) {\n        html +=\n        \/\/ Row 2: Ship\n        '<div class=\"tp-mrow\">'+\n          '<div class=\"lhs\">'+(showShip ? shipBadge(r.ship) : '')+'<\/div>'+\n          '<div class=\"rhs\"><\/div>'+\n        '<\/div>';\n      }\n\n      html +=\n        \/\/ Row 3: Operator + route label (no book button)\n        '<div class=\"tp-mrow\">'+\n          '<div class=\"lhs\">'+opCell(r)+'<\/div>'+\n          (routeName ? ('<div class=\"rhs\"><span class=\"tp-route-ticket\">'+routeTicketHtml(routeName)+'<\/span><\/div>') : '<div class=\"rhs\"><\/div>')+\n        '<\/div>'+\n      '<\/div>';\n    });\n    html += '<\/div>';\n    return html;\n  }\n\n  function renderExtendedCards(rows){\n    const routeName = cfg.label || '';\n    const ctaText = L.more_link || 'More sailings';\n    let html = '<div class=\"tp-cardlist\">';\n    rows.forEach(r=>{\n      const dow = WD[r.dep.getDay()] || '';\n      const link = r.opLink ? ('<a class=\"tp-ext-cta\" href=\"'+r.opLink+'\" target=\"_blank\" rel=\"nofollow noopener\">'+ctaText+' <span class=\"tp-ext-cta-arrow\">\u00bb<\/span><\/a>') : '<span class=\"tp-ext-cta\">'+ctaText+' <span class=\"tp-ext-cta-arrow\">\u00bb<\/span><\/span>';\n      html += '<div class=\"tp-ext-card\">'\n        + boostersHtml(r, true)\n        + '<div class=\"tp-ext-left\">'\n        +   '<div>'+opCell(r)+'<\/div>'\n        +   (showShip ? ('<div>'+shipBadge(r.ship)+'<\/div>') : '')\n        + '<\/div>'\n        + '<div class=\"tp-ext-mid\">'\n        +   '<div class=\"tp-ext-top\"><span class=\"tp-sub\">'+dShort(r.dep)+' <span class=\"tp-day\">'+dow+'<\/span><\/span><span class=\"tp-ext-dur\">'+dur(r.min)+'<\/span><\/div>'\n        +   '<div class=\"tp-ext-times\"><span class=\"tp-ext-time\">'+hhmm(r.dep)+'<\/span><span class=\"tp-ext-line\"><\/span><span class=\"tp-ext-dur\">'+dur(r.min)+'<\/span><span class=\"tp-ext-line\"><\/span><span class=\"tp-ext-time\">'+hhmm(r.arr)+'<\/span><\/div>'\n        +   (routeName ? ('<div class=\"tp-ext-route\">'+routeName+'<\/div>') : '')\n        + '<\/div>'\n        + '<div class=\"tp-ext-right\">'+link+'<\/div>'\n        + '<\/div>';\n    });\n    html += '<\/div>';\n    return html;\n  }\n\n  async function load(){\n    btn.disabled = true; setLoader(true); status.textContent = L.status_loading;\n    const start = new Date(dateEl.value || new Date());\n    const days  = (daySetup === 'modern') ? 1 : Math.max(1, parseInt(daysEl.value,10)||1);\n    const end   = new Date(start); end.setDate(start.getDate()+days-1);\n    const url = new URL(cfg.rest); url.searchParams.set('route', String(cfg.route));\n    url.searchParams.set('from', iso(start)); url.searchParams.set('to', iso(end));\n    try{\n      const res = await fetch(url.toString(), {credentials:'same-origin'});\n      if(!res.ok) throw new Error('HTTP '+res.status);\n      const json = await res.json();\n      const list = (json && json.data && Array.isArray(json.data.rows)) ? json.data.rows : [];\n      let rows = list.map(t=>({\n        opId:t.operatorId||null, opName:t.operatorName||'', opLogo:t.operatorLogo||'', opLink:t.operatorLink||'',\n        dep:toLocal(t.departureTime), arr:toLocal(t.arrivalTime), min:t.durationInMinutes||0, ship:t.shipName||''\n      })).sort((a,b)=>a.dep-b.dep);\n      const todayStr = new Date().toDateString();\n      const nowMs = Date.now();\n      const arrivedToday = rows\n        .filter(r => (r.dep instanceof Date) && (r.arr instanceof Date) && r.dep.toDateString() === todayStr && r.arr.getTime() <= nowMs)\n        .sort((a,b) => b.arr - a.arr);\n      if (arrivedToday.length > 2) {\n        const keep = new Set(arrivedToday.slice(0,2).map(rowKey));\n        rows = rows.filter(r => {\n          const isArrivedToday = (r.dep instanceof Date) && (r.arr instanceof Date) && r.dep.toDateString() === todayStr && r.arr.getTime() <= nowMs;\n          return !isArrivedToday || keep.has(rowKey(r));\n        });\n      }\n      if(rows.length===0){ status.textContent = L.status_empty; clearStatusInline(); out.innerHTML=''; return; }\n      const visibleRows = (limit10 && !revealAll && rows.length > 10) ? rows.slice(0,10) : rows;\n      const hiddenCount = rows.length - visibleRows.length;\n      soonKey = pickSoonKey(visibleRows);\n      featuredKey = pickFeaturedKey(visibleRows, soonKey);\n      status.textContent = '';\n      setStatusInline(\n  (L.status_found || '%d sailings found').replace('%d', '<strong>'+rows.length+'<\/strong>')\n);\n      out.innerHTML = renderTable(visibleRows) + renderCards(visibleRows) + moreButtonHtml(hiddenCount);\n      bindRowLinks();\n      if (navHost) navHost.innerHTML = navHtml();\n    }catch(e){\n      status.textContent = 'Failed to load data'; out.innerHTML = '<pre>'+String(e.message||e)+'<\/pre>';\n    }finally{ setLoader(false); btn.disabled=false; }\n  }\n\n  wrap.addEventListener('click', function(e){\n    const nav = e.target.closest('.tp-day-nav a[data-shift]');\n    if (nav) {\n      e.preventDefault();\n      shiftDate(parseInt(nav.getAttribute('data-shift'),10) || 0);\n      return;\n    }\n    const more = e.target.closest('.tp-more-btn');\n    if (more) {\n      e.preventDefault();\n      revealAll = true;\n      load();\n    }\n  });\n\n  btn.addEventListener('click', load);\n  if (navHost) navHost.innerHTML = navHtml();\n  renderLocalTimes();\n  setInterval(renderLocalTimes, 30000);\n  if (cfg.autoload) load();\n})();\n<\/script>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"folkestone-calais-eurotunnel-timetable\">Le Shuttle Folkestone-Calais<\/h2>\n\n\n<p class=\"wp-block-paragraph\">Ces horaires montrent les d\u00e9parts en direct depuis Folkestone et les arriv\u00e9es pr\u00e9vues \u00e0 Calais. Le prix des billets est sur notre page <a href=\"https:\/\/calais-dover.com\/fr\/prix\/\">prix du ferry et de Le Shuttle<\/a>.<\/p>\n\n<div class=\"tp-wrap tp-theme-extended_cards tp-day-setup-modern\" data-config=\"{&quot;rest&quot;:&quot;https:\\\/\\\/calais-dover.com\\\/fr\\\/wp-json\\\/timetables-pro\\\/v1\\\/timetables&quot;,&quot;route&quot;:56,&quot;days&quot;:1,&quot;autoload&quot;:true,&quot;theme&quot;:&quot;extended_cards&quot;,&quot;daySetup&quot;:&quot;modern&quot;,&quot;limit10&quot;:true,&quot;label&quot;:&quot;Folkestone - Calais&quot;,&quot;labels&quot;:{&quot;ui_date&quot;:&quot;Date&quot;,&quot;ui_days&quot;:&quot;Jours&quot;,&quot;ui_button&quot;:&quot;Afficher les travers\\u00e9es&quot;,&quot;status_idle&quot;:&quot;Choisissez une date et cliquez sur Afficher les travers\\u00e9es&quot;,&quot;status_loading&quot;:&quot;Chargement...&quot;,&quot;status_empty&quot;:&quot;Aucune travers\\u00e9e trouv\\u00e9e&quot;,&quot;status_found&quot;:&quot;%d travers\\u00e9es trouv\\u00e9es&quot;,&quot;th_date&quot;:&quot;Date&quot;,&quot;th_dep&quot;:&quot;D\\u00e9part&quot;,&quot;th_arr&quot;:&quot;Arriv\\u00e9e&quot;,&quot;th_dur&quot;:&quot;Dur\\u00e9e&quot;,&quot;th_ship&quot;:&quot;Navire&quot;,&quot;th_op&quot;:&quot;Op\\u00e9rateur&quot;,&quot;th_route&quot;:&quot;Route&quot;,&quot;wd_sun&quot;:&quot;DIM&quot;,&quot;wd_mon&quot;:&quot;LUN&quot;,&quot;wd_tue&quot;:&quot;MAR&quot;,&quot;wd_wed&quot;:&quot;MER&quot;,&quot;wd_thu&quot;:&quot;JEU&quot;,&quot;wd_fri&quot;:&quot;VEN&quot;,&quot;wd_sat&quot;:&quot;SAM&quot;,&quot;summary_footer_one&quot;:&quot;\\u00bb Voir tous les d\\u00e9parts %s dans notre horaire&quot;,&quot;summary_footer_two&quot;:&quot;\\u00bb Voir plus de d\\u00e9parts %1$s et %2$s dans notre horaire&quot;,&quot;summary_footer_generic&quot;:&quot;Consultez notre horaire pour plus de travers\\u00e9es&quot;,&quot;summary_header&quot;:&quot;Les deux prochains d\\u00e9parts :&quot;,&quot;summary_no_upcoming&quot;:&quot;Aucun prochain d\\u00e9part trouv\\u00e9&quot;,&quot;more_link&quot;:&quot;Plus de travers\\u00e9es&quot;,&quot;more_departures_button&quot;:&quot;Afficher plus de d\\u00e9parts&quot;,&quot;day_nav_previous&quot;:&quot;Afficher le jour pr\\u00e9c\\u00e9dent&quot;,&quot;day_nav_next&quot;:&quot;Afficher le jour suivant&quot;,&quot;day_nav_today&quot;:&quot;Afficher aujourd\\u2019hui&quot;,&quot;day_nav_tomorrow&quot;:&quot;Afficher demain&quot;,&quot;local_time_pair&quot;:&quot;Heure locale du port de d\\u00e9part : %1$s \\\/ Heure locale du port d&#039;arriv\\u00e9e : %2$s&quot;,&quot;local_time_combined&quot;:&quot;Heure locale des ports de d\\u00e9part et d&#039;arriv\\u00e9e : %s&quot;,&quot;tag_often_booked&quot;:&quot;\\u2b50 Souvent r\\u00e9serv\\u00e9&quot;,&quot;tag_leaving_soon&quot;:&quot;\\ud83d\\udd34 D\\u00e9part imminent&quot;,&quot;tag_currently_sailing&quot;:&quot;\\ud83d\\udfe2 En navigation&quot;,&quot;tag_arrived&quot;:&quot;\\u26ab Arriv\\u00e9&quot;,&quot;tag_overnight&quot;:&quot;Nuit \\u00e0 bord&quot;},&quot;showLogo&quot;:true,&quot;activityBoosters&quot;:true,&quot;shipMap&quot;:{&quot;eurotunnel&quot;:&quot;Eurotunnel&quot;},&quot;shipLinks&quot;:{&quot;eurotunnel&quot;:&quot;https:\\\/\\\/calais-dover.com\\\/fr\\\/eurotunnel\\\/&quot;},&quot;showShip&quot;:false,&quot;departurePort&quot;:&quot;Folkestone&quot;,&quot;arrivalPort&quot;:&quot;Calais&quot;,&quot;departureTimezone&quot;:&quot;Europe\\\/London&quot;,&quot;arrivalTimezone&quot;:&quot;Europe\\\/Paris&quot;}\">\n  <div class=\"tp-controls\" role=\"group\" aria-label=\"Timetable controls\">\n    <div>\n      <label for=\"tp-date\">Date<\/label><br>\n      <input id=\"tp-date\" class=\"tp-date\" type=\"date\" aria-label=\"Date\">\n    <\/div>\n    <div class=\"tp-day-field\">\n      <label for=\"tp-range\">Jours<\/label><br>\n      <select id=\"tp-range\" class=\"tp-range\" aria-label=\"Jours\">\n        <option value=\"1\">1<\/option><option value=\"3\">3<\/option><option value=\"7\">7<\/option><option value=\"14\">14<\/option>\n      <\/select>\n    <\/div>\n    <div><button id=\"tp-load\" class=\"tp-btn\">Afficher les travers\u00e9es<\/button><\/div>\n  <\/div>\n\n  <div class=\"tp-local-nav-row\">\n    <div class=\"tp-local-times\" id=\"tp-local-times\" aria-live=\"polite\"><\/div>\n    <div class=\"tp-day-nav-host\"><\/div>\n  <\/div>\n  <div id=\"tp-status\" class=\"tp-muted\" aria-live=\"polite\">Choisissez une date et cliquez sur Afficher les travers\u00e9es<\/div>\n  <div id=\"tp-results\"><\/div>\n\n  <div class=\"tp-loader\" aria-hidden=\"true\">\n    <div class=\"tp-loader-card\">\n      <div class=\"tp-spinner\" aria-hidden=\"true\"><\/div>\n      <div class=\"tp-loader-text\">Chargement...<\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\n(function(){\n  const wrap   = document.currentScript.previousElementSibling;\n  const cfg    = JSON.parse(wrap.getAttribute('data-config')||'{}');\n  const L      = cfg.labels||{};\n  const dateEl = wrap.querySelector('#tp-date');\n  const daysEl = wrap.querySelector('#tp-range');\n  const btn    = wrap.querySelector('#tp-load');\n  const status = wrap.querySelector('#tp-status');\n  const out    = wrap.querySelector('#tp-results');\n  const loader = wrap.querySelector('.tp-loader');\n  const controls = wrap.querySelector('.tp-controls');\n  const navHost = wrap.querySelector('.tp-day-nav-host');\n  const localTimesEl = wrap.querySelector('#tp-local-times');\n\n  const showShip = (cfg.showShip !== false && cfg.showShip !== 0 && cfg.showShip !== '0');\n  const activityBoosters = !!cfg.activityBoosters;\n  const daySetup = (cfg.daySetup === 'modern') ? 'modern' : 'classic';\n  const limit10 = !!cfg.limit10;\n  let featuredKey = '';\n  let soonKey = '';\n  let revealAll = false;\n\n  if (daySetup === 'modern') {\n    const dayField = wrap.querySelector('.tp-day-field');\n    if (dayField) dayField.style.display = 'none';\n  }\n\n  dateEl.valueAsDate = new Date();\n  dateEl.min = new Date().toISOString().split('T')[0];\n  Array.from(daysEl.options).forEach(o=>{ if(parseInt(o.value,10)===parseInt(cfg.days||7,10)) o.selected=true; });\n\n  function pad(n){ return String(n).padStart(2,'0'); }\n  function iso(d){ return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate()); }\n  function toLocal(s){ return new Date(s); }\n  function hhmm(d){ return d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}); }\n  function dShort(d){ return d.toLocaleDateString([], {day:'numeric', month:'short'}); } \/\/ short, no year\n  function dur(m){ const h=Math.floor(m\/60), r=m%60; return r? (h+'h '+r+'m') : (h+'h'); }\n\n  const WD = [\n    L.wd_sun || 'SUN',\n    L.wd_mon || 'MON',\n    L.wd_tue || 'TUE',\n    L.wd_wed || 'WED',\n    L.wd_thu || 'THU',\n    L.wd_fri || 'FRI',\n    L.wd_sat || 'SAT'\n  ];\n\n  function setLoader(v){ loader.classList.toggle('show', !!v); loader.setAttribute('aria-hidden', v?'false':'true'); }\n\n  function formatZoneTime(tz){\n    if (!tz) return '--:--';\n    try {\n      return new Intl.DateTimeFormat([], {hour:'2-digit', minute:'2-digit', hour12:false, timeZone: tz}).format(new Date());\n    } catch (e) {\n      return '--:--';\n    }\n  }\n\n  function renderLocalTimes(){\n    if (!localTimesEl) return;\n    if (!cfg.departureTimezone || !cfg.arrivalTimezone) {\n      localTimesEl.textContent = '';\n      return;\n    }\n    const depTime = formatZoneTime(cfg.departureTimezone);\n    const arrTime = formatZoneTime(cfg.arrivalTimezone);\n    const pairTpl = L.local_time_pair || 'Departure Port Local Time: %1$s \/ Arrival Port Local Time: %2$s';\n    const combinedTpl = L.local_time_combined || 'Departure & Arrival Port Local Time: %s';\n    if (depTime === arrTime) {\n      localTimesEl.textContent = combinedTpl.replace('%s', depTime);\n      return;\n    }\n    localTimesEl.textContent = pairTpl.replace('%1$s', depTime).replace('%2$s', arrTime);\n  }\n\n  function setStatusInline(html){\n    let box = wrap.querySelector('.tp-controls-status');\n    if (!box) {\n      box = document.createElement('div');\n      box.className = 'tp-controls-status';\n      controls.appendChild(box);\n    }\n    box.innerHTML = html || '';\n  }\n\n  function clearStatusInline(){\n    const box = wrap.querySelector('.tp-controls-status');\n    if (box) box.innerHTML = '';\n  }\n\n  function navHtml(){\n    if (daySetup !== 'modern') return '';\n    const base = new Date(dateEl.value || new Date());\n    const today = new Date();\n    const tomorrow = new Date(today);\n    tomorrow.setDate(today.getDate() + 1);\n    const isToday = base.toDateString() === today.toDateString();\nlet prevLabel = L.day_nav_previous || 'Show previous day';\nlet nextLabel = L.day_nav_next || 'Show next day';\nif (isToday) nextLabel = L.day_nav_tomorrow || 'Show tomorrow';\nif (base.toDateString() === tomorrow.toDateString()) prevLabel = L.day_nav_today || 'Show today';\n    return '<div class=\"tp-day-nav\">'\n      + (isToday ? '' : '<a href=\"#\" data-shift=\"-1\">'+prevLabel+'<\/a>')\n      + '<a href=\"#\" data-shift=\"1\">'+nextLabel+'<\/a>'\n      + '<\/div>';\n  }\n\n  function shiftDate(delta){\n    const base = new Date(dateEl.value || new Date());\n    base.setDate(base.getDate() + delta);\n    dateEl.value = iso(base);\n    load();\n  }\n\n  function moreButtonHtml(hiddenCount){\n    if (!limit10 || revealAll || hiddenCount <= 0) return '';\n    return '<div style=\"margin-top:12px;text-align:center\">'\n      + '<button type=\"button\" class=\"tp-btn tp-more-btn\">'+(L.more_departures_button || 'Show more departures')+' ('+hiddenCount+')<\/button>'\n      + '<\/div>';\n  }\n\n  function opCell(r){\n    const name = r.opName || ('Operator '+(r.opId||''));\n    const logo = (cfg.showLogo && r.opLogo) ? '<img decoding=\"async\" src=\"'+r.opLogo+'\" alt=\"'+name+'\"> ' : '';\n    const label = logo + '<span>'+name+'<\/span>';\n    return r.opLink ? '<a class=\"tp-op\" href=\"'+r.opLink+'\" target=\"_blank\" rel=\"nofollow noopener\">'+label+'<\/a>' : '<span class=\"tp-op\">'+label+'<\/span>';\n  }\n\n  function shipInfo(original){\n    const key = (original||'').toLowerCase().trim();\n    const label = (cfg.shipMap && cfg.shipMap[key]) || original || '';\n    const href  = (cfg.shipLinks && cfg.shipLinks[key]) || '';\n    return {label, href};\n  }\n\n  function shipBadge(original){\n    const s = shipInfo(original);\n    const badge = '<span class=\"tp-badge\">'+(s.label||'')+'<\/span>';\n    return s.href ? ('<a href=\"'+s.href+'\" target=\"_blank\" rel=\"nofollow noopener\">'+badge+'<\/a>') : badge;\n  }\n\n  function rowKey(r){\n    return [String(r.opId||''), String(+r.dep||0), String(+r.arr||0), String(r.ship||'')].join('|');\n  }\n\n  function pickFeaturedKey(rows, avoidKey){\n    if (!activityBoosters || !rows.length) return '';\n    const routeSeed = parseInt(cfg.route, 10) || 0;\n    const dateSeed = parseInt(String(dateEl.value || '').replace(\/-\/g,''), 10) || 0;\n\n    let pool = rows;\n    if (avoidKey) {\n      const soonIdx = rows.findIndex(r => rowKey(r) === avoidKey);\n      if (soonIdx >= 0) {\n        const later = rows.slice(soonIdx + 1).filter(r => rowKey(r) !== avoidKey);\n        pool = later.length ? later : rows.filter(r => rowKey(r) !== avoidKey);\n      } else {\n        pool = rows.filter(r => rowKey(r) !== avoidKey);\n      }\n    }\n\n    if (!pool.length) return '';\n    const idx = Math.abs((routeSeed + dateSeed + rows.length) % pool.length);\n    return rowKey(pool[idx]);\n  }\n\n  function pickSoonKey(rows){\n    if (!activityBoosters || !rows.length) return '';\n    const now = Date.now();\n    const todayStr = new Date(now).toDateString();\n    const firstUpcoming = rows.find(r => (r.dep instanceof Date) && r.dep.getTime() > now && r.dep.toDateString() === todayStr);\n    return firstUpcoming ? rowKey(firstUpcoming) : '';\n  }\n\n  function boostersFor(r){\n    if (!activityBoosters) return [];\n    const now = Date.now();\n    const depMs = (r.dep instanceof Date) ? r.dep.getTime() : 0;\n    const arrMs = (r.arr instanceof Date) ? r.arr.getTime() : 0;\n    const depIsToday = (r.dep instanceof Date) && (new Date(now).toDateString() === r.dep.toDateString());\n\n    if (depMs > 0 && arrMs > 0 && depMs <= now && arrMs > now) {\n      return [{cls:'tp-booster-sailing', text:(L.tag_currently_sailing || '\ud83d\udfe2 Currently sailing')}];\n    }\n    if (depIsToday && arrMs > 0 && arrMs <= now) {\n      return [{cls:'tp-booster-arrived', text:(L.tag_arrived || '\u26ab Arrived')}];\n    }\n    if (rowKey(r) === soonKey) return [{cls:'tp-booster-soon', text:(L.tag_leaving_soon || '\ud83d\udd34 Leaving soon')}];\n    if (r.dep && r.arr && r.dep.toDateString() !== r.arr.toDateString()) return [{cls:'tp-booster-overnight', text:(L.tag_overnight || 'Overnight')}];\n\n    return [];\n  }\n\n  function boostersHtml(r, floating){\n    const tags = boostersFor(r);\n    if (!tags.length) return '';\n    const cls = floating ? 'tp-boosters tp-boosters-float' : 'tp-boosters';\n    return '<div class=\"'+cls+'\">' + tags.map(t => '<span class=\"tp-booster '+t.cls+'\">'+boosterLabelHtml(t.text)+'<\/span>').join('') + '<\/div>';\n  }\n\n  function boosterLabelHtml(text){\n    return String(text || '').replace(\/^(\ud83d\udd34|\ud83d\udfe2|\u26ab|\u2b50)\\s*\/, '<span class=\"tp-booster-emoji\">$1<\/span>');\n  }\n\n  function routeTicketHtml(label){\n    const raw = String(label || '').trim();\n    if (!raw) return '';\n    if (cfg.theme !== 'small_cards') return escAttr(raw);\n    const parts = raw.split(\/\\s*[\\-\u2013]\\s*\/, 2);\n    if (parts.length < 2) return escAttr(raw);\n    return escAttr(parts[0]) + '<br>' + escAttr(parts[1]);\n  }\n\n  function escAttr(v){\n    return String(v||'').replace(\/[&<>\"']\/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[m]));\n  }\n\n  function bindRowLinks(){\n    if (cfg.theme !== 'max_clickouts') return;\n    out.querySelectorAll('tr.tp-row-link').forEach((row)=>{\n      row.setAttribute('role','link');\n      row.setAttribute('tabindex','0');\n      row.addEventListener('click', (e)=>{\n        if (e.target && e.target.closest('a')) return;\n        const href = row.getAttribute('data-row-link');\n        if (!href) return;\n        window.open(href, '_blank', 'noopener,noreferrer');\n      });\n      row.addEventListener('keydown', (e)=>{\n        if (e.key !== 'Enter' && e.key !== ' ') return;\n        e.preventDefault();\n        row.click();\n      });\n    });\n  }\n\n  function renderTable(rows){\n    const th = {date:L.th_date,dep:L.th_dep,arr:L.th_arr,dur:L.th_dur,ship:L.th_ship,op:L.th_op};\n\n    const headCells = [\n      '<th>'+th.date+'<\/th>',\n      '<th>'+th.dep+'<\/th>',\n      '<th>'+th.arr+'<\/th>',\n      '<th>'+th.dur+'<\/th>'\n    ];\n    if (showShip) {\n      headCells.push('<th>'+th.ship+'<\/th>');\n    }\n    headCells.push('<th>'+th.op+'<\/th>');\n\n    let html = '<table class=\"tp-table\"><thead><tr>'+headCells.join('')+'<\/tr><\/thead><tbody>';\n\n    rows.forEach(r=>{\n      const dow = WD[r.dep.getDay()] || '';\n      const clickable = (cfg.theme === 'max_clickouts' && r.opLink);\n      const rowAttr = clickable ? (' class=\"tp-row-link\" data-row-link=\"'+escAttr(r.opLink)+'\"') : '';\n      const cells = [\n        '<td>'+dShort(r.dep)+' <span class=\"tp-day\">'+dow+'<\/span>'+boostersHtml(r, false)+'<\/td>',\n        '<td>'+hhmm(r.dep)+'<\/td>',\n        '<td>'+hhmm(r.arr)+'<\/td>',\n        '<td>'+dur(r.min)+'<\/td>'\n      ];\n      if (showShip) {\n        cells.push('<td>'+shipBadge(r.ship)+'<\/td>');\n      }\n      cells.push('<td>'+opCell(r)+'<\/td>');\n      html += '<tr'+rowAttr+'>'+cells.join('')+'<\/tr>';\n    });\n    html += '<\/tbody><\/table>';\n    return html;\n  }\n\n  function renderCards(rows){\n    if (cfg.theme === 'extended_cards') {\n      return renderExtendedCards(rows);\n    }\n\n    const routeName = cfg.label || '';\n    let html = '<div class=\"tp-cardlist\">';\n    rows.forEach(r=>{\n      const dow = WD[r.dep.getDay()] || '';\n      const times = hhmm(r.dep) + ' <span class=\"tp-arrow\">\u2192<\/span> ' + hhmm(r.arr);\n      html += '<div class=\"tp-card\">'+boostersHtml(r, true)+\n        \/\/ Row 1: Date + weekday + times\n        '<div class=\"tp-mrow\">'+\n          '<div class=\"lhs\"><span class=\"tp-sub\">'+dShort(r.dep)+'<\/span><span class=\"tp-day\">'+dow+'<\/span><\/div>'+\n          '<div class=\"rhs\"><span class=\"tp-time\">'+times+'<\/span><\/div>'+\n        '<\/div>';\n\n      if (showShip) {\n        html +=\n        \/\/ Row 2: Ship\n        '<div class=\"tp-mrow\">'+\n          '<div class=\"lhs\">'+(showShip ? shipBadge(r.ship) : '')+'<\/div>'+\n          '<div class=\"rhs\"><\/div>'+\n        '<\/div>';\n      }\n\n      html +=\n        \/\/ Row 3: Operator + route label (no book button)\n        '<div class=\"tp-mrow\">'+\n          '<div class=\"lhs\">'+opCell(r)+'<\/div>'+\n          (routeName ? ('<div class=\"rhs\"><span class=\"tp-route-ticket\">'+routeTicketHtml(routeName)+'<\/span><\/div>') : '<div class=\"rhs\"><\/div>')+\n        '<\/div>'+\n      '<\/div>';\n    });\n    html += '<\/div>';\n    return html;\n  }\n\n  function renderExtendedCards(rows){\n    const routeName = cfg.label || '';\n    const ctaText = L.more_link || 'More sailings';\n    let html = '<div class=\"tp-cardlist\">';\n    rows.forEach(r=>{\n      const dow = WD[r.dep.getDay()] || '';\n      const link = r.opLink ? ('<a class=\"tp-ext-cta\" href=\"'+r.opLink+'\" target=\"_blank\" rel=\"nofollow noopener\">'+ctaText+' <span class=\"tp-ext-cta-arrow\">\u00bb<\/span><\/a>') : '<span class=\"tp-ext-cta\">'+ctaText+' <span class=\"tp-ext-cta-arrow\">\u00bb<\/span><\/span>';\n      html += '<div class=\"tp-ext-card\">'\n        + boostersHtml(r, true)\n        + '<div class=\"tp-ext-left\">'\n        +   '<div>'+opCell(r)+'<\/div>'\n        +   (showShip ? ('<div>'+shipBadge(r.ship)+'<\/div>') : '')\n        + '<\/div>'\n        + '<div class=\"tp-ext-mid\">'\n        +   '<div class=\"tp-ext-top\"><span class=\"tp-sub\">'+dShort(r.dep)+' <span class=\"tp-day\">'+dow+'<\/span><\/span><span class=\"tp-ext-dur\">'+dur(r.min)+'<\/span><\/div>'\n        +   '<div class=\"tp-ext-times\"><span class=\"tp-ext-time\">'+hhmm(r.dep)+'<\/span><span class=\"tp-ext-line\"><\/span><span class=\"tp-ext-dur\">'+dur(r.min)+'<\/span><span class=\"tp-ext-line\"><\/span><span class=\"tp-ext-time\">'+hhmm(r.arr)+'<\/span><\/div>'\n        +   (routeName ? ('<div class=\"tp-ext-route\">'+routeName+'<\/div>') : '')\n        + '<\/div>'\n        + '<div class=\"tp-ext-right\">'+link+'<\/div>'\n        + '<\/div>';\n    });\n    html += '<\/div>';\n    return html;\n  }\n\n  async function load(){\n    btn.disabled = true; setLoader(true); status.textContent = L.status_loading;\n    const start = new Date(dateEl.value || new Date());\n    const days  = (daySetup === 'modern') ? 1 : Math.max(1, parseInt(daysEl.value,10)||1);\n    const end   = new Date(start); end.setDate(start.getDate()+days-1);\n    const url = new URL(cfg.rest); url.searchParams.set('route', String(cfg.route));\n    url.searchParams.set('from', iso(start)); url.searchParams.set('to', iso(end));\n    try{\n      const res = await fetch(url.toString(), {credentials:'same-origin'});\n      if(!res.ok) throw new Error('HTTP '+res.status);\n      const json = await res.json();\n      const list = (json && json.data && Array.isArray(json.data.rows)) ? json.data.rows : [];\n      let rows = list.map(t=>({\n        opId:t.operatorId||null, opName:t.operatorName||'', opLogo:t.operatorLogo||'', opLink:t.operatorLink||'',\n        dep:toLocal(t.departureTime), arr:toLocal(t.arrivalTime), min:t.durationInMinutes||0, ship:t.shipName||''\n      })).sort((a,b)=>a.dep-b.dep);\n      const todayStr = new Date().toDateString();\n      const nowMs = Date.now();\n      const arrivedToday = rows\n        .filter(r => (r.dep instanceof Date) && (r.arr instanceof Date) && r.dep.toDateString() === todayStr && r.arr.getTime() <= nowMs)\n        .sort((a,b) => b.arr - a.arr);\n      if (arrivedToday.length > 2) {\n        const keep = new Set(arrivedToday.slice(0,2).map(rowKey));\n        rows = rows.filter(r => {\n          const isArrivedToday = (r.dep instanceof Date) && (r.arr instanceof Date) && r.dep.toDateString() === todayStr && r.arr.getTime() <= nowMs;\n          return !isArrivedToday || keep.has(rowKey(r));\n        });\n      }\n      if(rows.length===0){ status.textContent = L.status_empty; clearStatusInline(); out.innerHTML=''; return; }\n      const visibleRows = (limit10 && !revealAll && rows.length > 10) ? rows.slice(0,10) : rows;\n      const hiddenCount = rows.length - visibleRows.length;\n      soonKey = pickSoonKey(visibleRows);\n      featuredKey = pickFeaturedKey(visibleRows, soonKey);\n      status.textContent = '';\n      setStatusInline(\n  (L.status_found || '%d sailings found').replace('%d', '<strong>'+rows.length+'<\/strong>')\n);\n      out.innerHTML = renderTable(visibleRows) + renderCards(visibleRows) + moreButtonHtml(hiddenCount);\n      bindRowLinks();\n      if (navHost) navHost.innerHTML = navHtml();\n    }catch(e){\n      status.textContent = 'Failed to load data'; out.innerHTML = '<pre>'+String(e.message||e)+'<\/pre>';\n    }finally{ setLoader(false); btn.disabled=false; }\n  }\n\n  wrap.addEventListener('click', function(e){\n    const nav = e.target.closest('.tp-day-nav a[data-shift]');\n    if (nav) {\n      e.preventDefault();\n      shiftDate(parseInt(nav.getAttribute('data-shift'),10) || 0);\n      return;\n    }\n    const more = e.target.closest('.tp-more-btn');\n    if (more) {\n      e.preventDefault();\n      revealAll = true;\n      load();\n    }\n  });\n\n  btn.addEventListener('click', load);\n  if (navHost) navHost.innerHTML = navHtml();\n  renderLocalTimes();\n  setInterval(renderLocalTimes, 30000);\n  if (cfg.autoload) load();\n})();\n<\/script>\n\n\n\n<div class=\"wp-block-group alignfull cd-band-grey is-layout-flow wp-block-group-is-layout-flow\">\n<h2 class=\"wp-block-heading\" id=\"about-calais-dover-timetables\">Dur\u00e9e et d\u00e9parts par compagnie<\/h2>\n\n\n<ul class=\"wp-block-list\">\n<li><strong>P&amp;O Ferries :<\/strong> environ 90 minutes, <a href=\"https:\/\/calais-dover.com\/go\/in-text-poferries-fr\" target=\"_blank\" rel=\"nofollow sponsored noopener\">jusqu&rsquo;\u00e0 15 d\u00e9parts par jour \u2197<\/a>. La seule compagnie qui vous prend <a href=\"https:\/\/calais-dover.com\/fr\/passager-pieton\/\">comme passager pi\u00e9ton<\/a>.<\/li>\n\n<li><strong>DFDS :<\/strong> environ 90 minutes, <a href=\"https:\/\/calais-dover.com\/go\/in-text-dfds-fr\" target=\"_blank\" rel=\"nofollow sponsored noopener\">jusqu&rsquo;\u00e0 30 travers\u00e9es par jour \u2197<\/a> dans les deux sens.<\/li>\n\n<li><strong>Irish Ferries :<\/strong> environ 90 minutes, <a href=\"https:\/\/calais-dover.com\/go\/in-text-irish-fr\" target=\"_blank\" rel=\"nofollow sponsored noopener\">jusqu&rsquo;\u00e0 14 d\u00e9parts par jour \u2197<\/a>.<\/li>\n\n<li><strong>Le Shuttle :<\/strong> 35 minutes par le tunnel, plusieurs d\u00e9parts par heure.<\/li>\n<\/ul>\n\n\n<p class=\"wp-block-paragraph\">Si un d\u00e9part manque dans les horaires, il y a de fortes chances que le planning suivant ne soit pas encore publi\u00e9 ou soit en cours de mise \u00e0 jour, surtout aux changements de saison et en p\u00e9riode charg\u00e9e. Un jour d&rsquo;\u00e9t\u00e9 charg\u00e9 ou par vent fort, v\u00e9rifiez aussi les infos trafic de votre compagnie : les horaires en direct peuvent avoir un peu de retard en cas d&rsquo;annulation soudaine ou de retard d\u00fb \u00e0 la m\u00e9t\u00e9o. Le temps qu&rsquo;il fait est visible sur nos <a href=\"https:\/\/calais-dover.com\/fr\/calais-douvres-webcams-et-meteo\/\">webcams Calais et Douvres<\/a>, et la position des navires sur notre <a href=\"https:\/\/calais-dover.com\/fr\/ferry-tracker\/\">ferry tracker<\/a>.<\/p>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"enregistrement\">Enregistrement par compagnie<\/h2>\n\n\n<p class=\"wp-block-paragraph\"><em>\u00c0 noter : les horaires d&rsquo;enregistrement de cette page sont indicatifs, pour estimer le temps \u00e0 pr\u00e9voir pour votre voyage. Suivez toujours les informations officielles de la compagnie avec laquelle vous voyagez ou aupr\u00e8s de laquelle vous avez r\u00e9serv\u00e9. Le d\u00e9roulement de l&rsquo;enregistrement est expliqu\u00e9 sur nos pages <a href=\"https:\/\/calais-dover.com\/fr\/port-de-calais\/\">port de Calais<\/a> et <a href=\"https:\/\/calais-dover.com\/fr\/port-de-douvres\/\">port de Douvres<\/a>.<\/em><\/p>\n\n\n<div class=\"cd-opgrid\">\n  <div class=\"cd-opcard\">\n    <div class=\"cd-opcard-head\"><img decoding=\"async\" src=\"https:\/\/calais-dover.com\/wp-content\/uploads\/2024\/06\/dfds-logo-1-1.jpg\" alt=\"Logo de DFDS\" width=\"48\" height=\"48\" loading=\"lazy\"><div><strong>DFDS<\/strong><a class=\"cd-opcard-more\" href=\"https:\/\/calais-dover.com\/fr\/dfds\/\">Tout sur DFDS<\/a><\/div><\/div>\n    <ul class=\"cd-checkin\">\n      <li><span>Pr\u00e9sentez-vous<\/span><b>60 \u00e0 90 min<\/b><\/li>\n      <li><span>Jours charg\u00e9s<\/span><b>90 min<\/b><\/li>\n    <\/ul>\n    <a class=\"cd-opcard-book\" href=\"https:\/\/calais-dover.com\/go\/in-text-dfds-fr\" target=\"_blank\" rel=\"nofollow sponsored noopener\">R\u00e9server chez DFDS \u2197<\/a>\n  <\/div>\n  <div class=\"cd-opcard\">\n    <div class=\"cd-opcard-head\"><img decoding=\"async\" src=\"https:\/\/calais-dover.com\/wp-content\/uploads\/2024\/06\/po-ferries-logo-1-1.jpg\" alt=\"Logo de P&amp;O Ferries\" width=\"48\" height=\"48\" loading=\"lazy\"><div><strong>P&amp;O Ferries<\/strong><a class=\"cd-opcard-more\" href=\"https:\/\/calais-dover.com\/fr\/po-ferries\/\">Tout sur P&amp;O<\/a><\/div><\/div>\n    <ul class=\"cd-checkin\">\n      <li><span>Avec v\u00e9hicule<\/span><b>60 min<\/b><\/li>\n      <li><span>Pi\u00e9ton<\/span><b>90 min<\/b><\/li>\n    <\/ul>\n    <a class=\"cd-opcard-book\" href=\"https:\/\/calais-dover.com\/go\/in-text-poferries-fr\" target=\"_blank\" rel=\"nofollow sponsored noopener\">R\u00e9server chez P&amp;O \u2197<\/a>\n  <\/div>\n  <div class=\"cd-opcard\">\n    <div class=\"cd-opcard-head\"><img decoding=\"async\" src=\"https:\/\/calais-dover.com\/wp-content\/uploads\/2024\/06\/irish-ferries-logo-2-1.jpg\" alt=\"Logo d'Irish Ferries\" width=\"48\" height=\"48\" loading=\"lazy\"><div><strong>Irish Ferries<\/strong><a class=\"cd-opcard-more\" href=\"https:\/\/calais-dover.com\/fr\/irish-ferries\/\">Tout sur Irish Ferries<\/a><\/div><\/div>\n    <ul class=\"cd-checkin\">\n      <li><span>Avec v\u00e9hicule<\/span><b>60 min<\/b><\/li>\n      <li><span>Autocar<\/span><b>90 min<\/b><\/li>\n    <\/ul>\n    <a class=\"cd-opcard-book\" href=\"https:\/\/calais-dover.com\/go\/in-text-irish-fr\" target=\"_blank\" rel=\"nofollow sponsored noopener\">R\u00e9server chez Irish Ferries \u2197<\/a>\n  <\/div>\n  <div class=\"cd-opcard\">\n    <div class=\"cd-opcard-head\"><img decoding=\"async\" src=\"https:\/\/calais-dover.com\/wp-content\/uploads\/2024\/06\/eurotunnel-le-shuttle-2-1.jpg\" alt=\"Logo d'Eurotunnel Le Shuttle\" width=\"48\" height=\"48\" loading=\"lazy\"><div><strong>Le Shuttle (train)<\/strong><a class=\"cd-opcard-more\" href=\"https:\/\/calais-dover.com\/fr\/eurotunnel\/\">Tout sur le train<\/a><\/div><\/div>\n    <ul class=\"cd-checkin\">\n      <li><span>Au plus t\u00f4t<\/span><b>2 h<\/b><\/li>\n      <li><span>Au plus tard<\/span><b>1 h<\/b><\/li>\n    <\/ul>\n    <a class=\"cd-opcard-book\" href=\"https:\/\/calais-dover.com\/go\/in-text-leshuttle-fr\" target=\"_blank\" rel=\"nofollow sponsored noopener\">R\u00e9server Le Shuttle \u2197<\/a>\n  <\/div>\n<\/div>\n<p class=\"cd-checkin-note\">Temps en minutes ou en heures avant le d\u00e9part. Chez les compagnies de ferry, c&rsquo;est l&rsquo;heure de fermeture de l&rsquo;enregistrement.<\/p>\n<style>\n.cd-opgrid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}\n.cd-opcard{display:flex;flex-direction:column;background:#fff;border:1px solid #e3e9ed;border-radius:14px;padding:16px;box-shadow:0 8px 22px -16px rgba(0,49,78,.45)}\n.cd-opcard-head{display:flex;align-items:center;gap:12px;margin-bottom:12px}\n.cd-opcard-head img{width:48px;height:48px;border-radius:50%;object-fit:cover;flex:none;box-shadow:0 0 0 1px #e3e9ed}\n.cd-opcard-head strong{display:block;color:var(--cd-navy,#00314e);font-size:17px;line-height:1.3}\n.cd-opcard-more{font-size:13px;color:#5b6770!important}\n.cd-page ul.cd-checkin{list-style:none!important;margin:0 0 14px!important;padding:0!important;display:grid;gap:6px}\n.cd-page ul.cd-checkin li{margin:0;display:flex;justify-content:space-between;align-items:baseline;gap:8px;padding:7px 10px;border-radius:8px;background:#f2f6f8;font-size:14px;color:#5b6770}\n.cd-page ul.cd-checkin li b{color:var(--cd-navy,#00314e);font-weight:700;white-space:nowrap}\n.cd-opcard-book{margin-top:auto;display:block;text-align:center;padding:9px 12px;border-radius:20px;background:var(--cd-blue,#0077b6);color:#fff!important;text-decoration:none!important;font-weight:700;font-size:14px}\n.cd-opcard-book:hover{background:var(--cd-green,#14a37f)}\n.cd-page .cd-checkin-note{margin:10px 0 0!important;font-size:14px;color:#5b6770}\n@media (max-width:1024px){.cd-opgrid{grid-template-columns:repeat(2,minmax(0,1fr))}}\n@media (max-width:560px){.cd-opgrid{grid-template-columns:1fr}}\n<\/style>\n\n\n<div class=\"wp-block-group alignfull cd-band is-layout-flow wp-block-group-is-layout-flow\">\n<div class=\"wp-block-columns are-vertically-aligned-center is-layout-flex wp-container-core-columns-is-layout-7387b849 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-vertically-aligned-center is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:62%\">\n<h3 class=\"wp-block-heading\">Vous avez trouv\u00e9 votre d\u00e9part ?<\/h3>\n\n\n<p class=\"wp-block-paragraph\">Comparez les d\u00e9parts et les prix actuels de P&amp;O Ferries, DFDS et Irish Ferries en un seul coup d&rsquo;\u0153il, ou r\u00e9servez le train-auto. Plus d&rsquo;infos sur <a href=\"https:\/\/calais-dover.com\/fr\/calais-douvres-en-voiture\/\">Calais-Douvres en voiture<\/a> et sur notre page <a href=\"https:\/\/calais-dover.com\/fr\/\">ferry Calais-Douvres<\/a>.<\/p>\n<\/div>\n\n\n<div class=\"wp-block-column is-vertically-aligned-center is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:38%\">\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button cd-btn cd-btn-solid\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/calais-dover.com\/go\/in-text-directferries-fr\" target=\"_blank\" rel=\"nofollow sponsored noopener\">Comparer horaires et prix \u2197<\/a><\/div>\n\n\n<div class=\"wp-block-button is-style-outline cd-btn is-style-outline--1\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/calais-dover.com\/go\/in-text-leshuttle-fr\" target=\"_blank\" rel=\"nofollow sponsored noopener\">R\u00e9server Le Shuttle \u2197<\/a><\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Les horaires du jour du ferry et de Le Shuttle, avec l&rsquo;enregistrement par compagnie.<\/p>\n","protected":false},"author":1,"featured_media":6440,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"site-sidebar-layout":"no-sidebar","site-content-layout":"","ast-site-content-layout":"full-width-container","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"disabled","ast-breadcrumbs-content":"","ast-featured-img":"disabled","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"cd_hero_tagline":"","footnotes":""},"class_list":["post-2509","page","type-page","status-publish","has-post-thumbnail","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Horaires ferry Calais-Douvres et Le Shuttle du jour<\/title>\n<meta name=\"description\" content=\"Horaires du jour du ferry Calais-Douvres (P&amp;O, DFDS, Irish Ferries) et de Le Shuttle, dans les deux sens, avec l&#039;enregistrement par compagnie.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/calais-dover.com\/fr\/ferry-calais-douvres-horaires\/\" \/>\n<meta property=\"og:locale\" content=\"fr_FR\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Horaires ferry Calais-Douvres et Le Shuttle du jour\" \/>\n<meta property=\"og:description\" content=\"Horaires du jour du ferry Calais-Douvres (P&amp;O, DFDS, Irish Ferries) et de Le Shuttle, dans les deux sens, avec l&#039;enregistrement par compagnie.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/calais-dover.com\/fr\/ferry-calais-douvres-horaires\/\" \/>\n<meta property=\"og:site_name\" content=\"Calais-Dover.com\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-26T11:41:04+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/calais-dover.com\/wp-content\/uploads\/2024\/06\/timetable-calais-dover.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"800\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Dur\u00e9e de lecture estim\u00e9e\" \/>\n\t<meta name=\"twitter:data1\" content=\"4 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/ferry-calais-douvres-horaires\\\/\",\"url\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/ferry-calais-douvres-horaires\\\/\",\"name\":\"Horaires ferry Calais-Douvres et Le Shuttle du jour\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/ferry-calais-douvres-horaires\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/ferry-calais-douvres-horaires\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/calais-dover.com\\\/wp-content\\\/uploads\\\/2024\\\/06\\\/timetable-calais-dover.jpg\",\"datePublished\":\"2023-07-05T09:35:55+00:00\",\"dateModified\":\"2026-10-26T11:41:04+00:00\",\"description\":\"Horaires du jour du ferry Calais-Douvres (P&O, DFDS, Irish Ferries) et de Le Shuttle, dans les deux sens, avec l'enregistrement par compagnie.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/ferry-calais-douvres-horaires\\\/#breadcrumb\"},\"inLanguage\":\"fr-FR\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/calais-dover.com\\\/fr\\\/ferry-calais-douvres-horaires\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"fr-FR\",\"@id\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/ferry-calais-douvres-horaires\\\/#primaryimage\",\"url\":\"https:\\\/\\\/calais-dover.com\\\/wp-content\\\/uploads\\\/2024\\\/06\\\/timetable-calais-dover.jpg\",\"contentUrl\":\"https:\\\/\\\/calais-dover.com\\\/wp-content\\\/uploads\\\/2024\\\/06\\\/timetable-calais-dover.jpg\",\"width\":1200,\"height\":800,\"caption\":\"Plan des horaires DFDS\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/ferry-calais-douvres-horaires\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Calais-Dover.com\",\"item\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Horaires ferry Calais-Douvres\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/#website\",\"url\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/\",\"name\":\"Calais-Dover.com\",\"description\":\"From Dover to Calais\",\"publisher\":{\"@id\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"fr-FR\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/#organization\",\"name\":\"Calais-Dover.com\",\"url\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"fr-FR\",\"@id\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/calais-dover.com\\\/wp-content\\\/uploads\\\/2023\\\/07\\\/Calais-Dover-logo.png\",\"contentUrl\":\"https:\\\/\\\/calais-dover.com\\\/wp-content\\\/uploads\\\/2023\\\/07\\\/Calais-Dover-logo.png\",\"width\":450,\"height\":80,\"caption\":\"Calais-Dover.com\"},\"image\":{\"@id\":\"https:\\\/\\\/calais-dover.com\\\/fr\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Horaires ferry Calais-Douvres et Le Shuttle du jour","description":"Horaires du jour du ferry Calais-Douvres (P&O, DFDS, Irish Ferries) et de Le Shuttle, dans les deux sens, avec l'enregistrement par compagnie.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/calais-dover.com\/fr\/ferry-calais-douvres-horaires\/","og_locale":"fr_FR","og_type":"article","og_title":"Horaires ferry Calais-Douvres et Le Shuttle du jour","og_description":"Horaires du jour du ferry Calais-Douvres (P&O, DFDS, Irish Ferries) et de Le Shuttle, dans les deux sens, avec l'enregistrement par compagnie.","og_url":"https:\/\/calais-dover.com\/fr\/ferry-calais-douvres-horaires\/","og_site_name":"Calais-Dover.com","article_modified_time":"2026-10-26T11:41:04+00:00","og_image":[{"width":1200,"height":800,"url":"https:\/\/calais-dover.com\/wp-content\/uploads\/2024\/06\/timetable-calais-dover.jpg","type":"image\/jpeg"}],"twitter_card":"summary_large_image","twitter_misc":{"Dur\u00e9e de lecture estim\u00e9e":"4 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/calais-dover.com\/fr\/ferry-calais-douvres-horaires\/","url":"https:\/\/calais-dover.com\/fr\/ferry-calais-douvres-horaires\/","name":"Horaires ferry Calais-Douvres et Le Shuttle du jour","isPartOf":{"@id":"https:\/\/calais-dover.com\/fr\/#website"},"primaryImageOfPage":{"@id":"https:\/\/calais-dover.com\/fr\/ferry-calais-douvres-horaires\/#primaryimage"},"image":{"@id":"https:\/\/calais-dover.com\/fr\/ferry-calais-douvres-horaires\/#primaryimage"},"thumbnailUrl":"https:\/\/calais-dover.com\/wp-content\/uploads\/2024\/06\/timetable-calais-dover.jpg","datePublished":"2023-07-05T09:35:55+00:00","dateModified":"2026-10-26T11:41:04+00:00","description":"Horaires du jour du ferry Calais-Douvres (P&O, DFDS, Irish Ferries) et de Le Shuttle, dans les deux sens, avec l'enregistrement par compagnie.","breadcrumb":{"@id":"https:\/\/calais-dover.com\/fr\/ferry-calais-douvres-horaires\/#breadcrumb"},"inLanguage":"fr-FR","potentialAction":[{"@type":"ReadAction","target":["https:\/\/calais-dover.com\/fr\/ferry-calais-douvres-horaires\/"]}]},{"@type":"ImageObject","inLanguage":"fr-FR","@id":"https:\/\/calais-dover.com\/fr\/ferry-calais-douvres-horaires\/#primaryimage","url":"https:\/\/calais-dover.com\/wp-content\/uploads\/2024\/06\/timetable-calais-dover.jpg","contentUrl":"https:\/\/calais-dover.com\/wp-content\/uploads\/2024\/06\/timetable-calais-dover.jpg","width":1200,"height":800,"caption":"Plan des horaires DFDS"},{"@type":"BreadcrumbList","@id":"https:\/\/calais-dover.com\/fr\/ferry-calais-douvres-horaires\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Calais-Dover.com","item":"https:\/\/calais-dover.com\/fr\/"},{"@type":"ListItem","position":2,"name":"Horaires ferry Calais-Douvres"}]},{"@type":"WebSite","@id":"https:\/\/calais-dover.com\/fr\/#website","url":"https:\/\/calais-dover.com\/fr\/","name":"Calais-Dover.com","description":"From Dover to Calais","publisher":{"@id":"https:\/\/calais-dover.com\/fr\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/calais-dover.com\/fr\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"fr-FR"},{"@type":"Organization","@id":"https:\/\/calais-dover.com\/fr\/#organization","name":"Calais-Dover.com","url":"https:\/\/calais-dover.com\/fr\/","logo":{"@type":"ImageObject","inLanguage":"fr-FR","@id":"https:\/\/calais-dover.com\/fr\/#\/schema\/logo\/image\/","url":"https:\/\/calais-dover.com\/wp-content\/uploads\/2023\/07\/Calais-Dover-logo.png","contentUrl":"https:\/\/calais-dover.com\/wp-content\/uploads\/2023\/07\/Calais-Dover-logo.png","width":450,"height":80,"caption":"Calais-Dover.com"},"image":{"@id":"https:\/\/calais-dover.com\/fr\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/calais-dover.com\/fr\/wp-json\/wp\/v2\/pages\/2509","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/calais-dover.com\/fr\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/calais-dover.com\/fr\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/calais-dover.com\/fr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/calais-dover.com\/fr\/wp-json\/wp\/v2\/comments?post=2509"}],"version-history":[{"count":10,"href":"https:\/\/calais-dover.com\/fr\/wp-json\/wp\/v2\/pages\/2509\/revisions"}],"predecessor-version":[{"id":11394,"href":"https:\/\/calais-dover.com\/fr\/wp-json\/wp\/v2\/pages\/2509\/revisions\/11394"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/calais-dover.com\/fr\/wp-json\/wp\/v2\/media\/6440"}],"wp:attachment":[{"href":"https:\/\/calais-dover.com\/fr\/wp-json\/wp\/v2\/media?parent=2509"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}