diff --git a/launcher/src/main.c b/launcher/src/main.c
index 160e4e4..6ed78aa 100644
--- a/launcher/src/main.c
+++ b/launcher/src/main.c
@@ -33,6 +33,19 @@ eadk_event_t np_event_get(int32_t *timeout) {
 static bool home_pressed;
 #endif
 
+/* Calcade : 0 Calcade, 1 Tableur, 2 invisible (d'après le nom de l'appli, lu en mémoire) */
+#if PLATFORM_DEVICE && !NP_SIMULATOR
+extern const char eadk_app_name[];
+static int np_variante(void) {
+  const volatile char *n = (const volatile char *)eadk_app_name;
+  if (n[0] == ' ' && n[1] == 0) return 2;
+  if (n[0] == 'T' && n[1] == 'a' && n[2] == 'b' && n[3] == 'l') return 1;
+  return 0;
+}
+#else
+static int np_variante(void) { return NP_VARIANTE; }
+#endif
+
 int main(void) {
 #if NP_COEUR
   np_coeur_init();
@@ -46,20 +59,19 @@ int main(void) {
   /* started as Matrices (Settings): NumPlay opens only through its secret */
   np_config_t cfg;
   np_config_load(&cfg);
-#if NP_VARIANTE == 1
-  /* Calcade : l'appli « Tableur » ; le code ouvre le vrai Calcade */
-  if (!np_tableur()) return np_session_end();
-#elif NP_VARIANTE == 2
-  /* Calcade : l'appli invisible ouvre directement les jeux */
-#else
-  if (cfg.disguise && !np_matrices(&cfg)) return np_session_end();
-#endif
+  /* Calcade (v70) : le même cœur pour les trois applis ; son nom dit laquelle on est.
+   * « Tableur » : le code ouvre le vrai Calcade ; « » (invisible) : directement les jeux. */
+  int variante = np_variante();
+  if (variante == 1) {
+    if (!np_tableur()) return np_session_end();
+  } else if (variante == 0 && cfg.disguise && !np_matrices(&cfg))
+    return np_session_end();
   np_wait_release();
   /* Calcade: this opening is counted in numplay.sys (every 20th sets the "update required" flag),
    * then the studio's intro; with the flag set, NumPlay stays on the update screen (Home quits) */
   bool update_required = cc_launch();
   np_progress_backup();
-  if (NP_VARIANTE != 2 && !cc_intro()) return np_session_end();
+  if (variante != 2 && !cc_intro()) return np_session_end();
   if (update_required) {
     cc_update_required();
     return np_session_end();
diff --git a/launcher/src/tableur.c b/launcher/src/tableur.c
index 887ce08..e061ce7 100644
--- a/launcher/src/tableur.c
+++ b/launcher/src/tableur.c
@@ -1,15 +1,19 @@
-/* Calcade, 23 octobre 2026 — fait partie de NumPlay modifié, sous licence GNU GPL v3.
- * Tableur : le déguisement de l'appli « Tableur » (variante NP_VARIANTE == 1). Un vrai petit tableur,
- * dessiné comme les applis de la calculatrice : 5 colonnes (A à E), 40 lignes, des nombres à saisir,
- * et la somme et la moyenne de la colonne en bas. Taper le CODE dans une case (puis OK ou EXE) ouvre
- * le vrai Calcade. Le code est écrit par le site à l'installation, juste après la marque
- * « CALCADE-TABLEUR1 » (calculatrice.js) ; sans code : 1234.
- * Home ou On/Off : on quitte. Rien touché pendant NP_IDLE_QUIT_MS : on quitte aussi (la calculatrice
- * qui s'éteint toute seule ne doit pas se rallumer dans le tableur, ni dans Calcade). */
+/* Calcade, 23 octobre 2026 (refait le 24 pour la v70) — fait partie de NumPlay modifié, sous licence
+ * GNU GPL v3.
+ * Tableur : le déguisement de l'appli « Tableur ». Un vrai petit tableur, dessiné comme les applis de
+ * la calculatrice : 5 colonnes (A à E), 40 lignes, des nombres à saisir, et la somme et la moyenne de
+ * la colonne en bas. Taper le CODE dans une case (puis OK ou EXE) ouvre le vrai Calcade. Le code est
+ * écrit par le site à l'installation, juste après la marque « CALCADE-TABLEUR1 » (calculatrice.js) ;
+ * sans code : 1234.
+ * v70 : tout est dessiné par NumPlay lui-même (gfx, comme le menu des jeux) : plus aucun appel au
+ * texte du firmware ni à la batterie, et des nombres entiers (centièmes) au lieu de flottants. Le
+ * même code est dans les trois applis (Calcade, Tableur, invisible) : seul le nom change.
+ * Home ou On/Off : on quitte. Rien touché pendant NP_IDLE_QUIT_MS : on quitte aussi. */
 #include <eadk.h>
 #include <string.h>
 #include "np.h"
 #include "sys.h"
+#include "gfx.h"
 
 #define C_BAR RGB(0xFF, 0xB6, 0x31)
 #define C_BG RGB(0xE7, 0xE7, 0xEF)
@@ -28,6 +32,7 @@
 #define CW 58
 #define Y0 40
 #define RH 22
+#define MAXV 999999999 /* en centièmes */
 
 /* la marque et le code, tamponnés par le site */
 typedef struct { char mark[16]; char code[12]; } tb_code_t;
@@ -36,43 +41,26 @@ __attribute__((used, aligned(4))) const volatile tb_code_t np_tableur_code = {
 
 enum {
   KEY_LEFT = 0, KEY_UP = 1, KEY_DOWN = 2, KEY_RIGHT = 3, KEY_OK = 4, KEY_BACK = 5, KEY_HOME = 6, KEY_ONOFF = 8,
-  KEY_TOOLBOX = 16, KEY_BACKSPACE = 17, KEY_MINUS = 46, KEY_DOT = 49, KEY_EXE = 52
+  KEY_BACKSPACE = 17, KEY_MINUS = 46, KEY_DOT = 49, KEY_EXE = 52
 };
 static const int8_t digit_keys[10] = {48, 42, 43, 44, 36, 37, 38, 30, 31, 32};
 
-static struct {
-  float v[ROWS][COLS];
+typedef struct {
+  int32_t v[ROWS][COLS]; /* centièmes */
   uint8_t set[ROWS][COLS];
   int row, col, top;
   char edit[12];
-} TB;
+} tb_t;
+static tb_t *TB; /* dans l'arena (np_alloc), pas en RAM statique */
 
-static void fill(int x, int y, int w, int h, color_t c) {
-  if (w > 0 && h > 0) eadk_display_push_rect_uniform((eadk_rect_t){x, y, w, h}, c);
-}
-static void text(const char *s, int x, int y, bool big, color_t fg, color_t bg) {
-  np_display_draw_string(s, (eadk_point_t){x, y}, big, fg, bg);
-}
-static int width(const char *s, bool big) { return (int)strlen(s) * (big ? 10 : 7); }
-
-static void fmt(float v, char *o) {
-  bool neg = v < 0;
-  if (neg) v = -v;
-  if (v >= 1e9f) {
-    strcpy(o, neg ? "-1E9+" : "1E9+");
-    return;
-  }
-  uint32_t ip, fp;
-  if (v >= 1e5f) ip = (uint32_t)(v + 0.5f), fp = 0;
-  else {
-    uint32_t s = (uint32_t)(v * 100 + 0.5f);
-    ip = s / 100, fp = s % 100;
-  }
-  if (neg && (ip || fp)) *o++ = '-';
+/* centièmes -> texte (« -12.5 », « 3 ») */
+static void fmt(int32_t v, char *o) {
+  if (v < 0) *o++ = '-', v = -v;
+  uint32_t ip = (uint32_t)v / 100, fp = (uint32_t)v % 100;
   char t[12];
   int n = 0;
   do t[n++] = (char)('0' + ip % 10);
-  while (ip /= 10);
+  while ((ip /= 10) && n < 11);
   while (n) *o++ = t[--n];
   if (fp) {
     *o++ = '.';
@@ -81,17 +69,24 @@ static void fmt(float v, char *o) {
   }
   *o = 0;
 }
-static float parse(const char *s) {
-  float sign = 1, v = 0, scale = 0;
-  if (*s == '-') sign = -1, s++;
+/* texte -> centièmes (bornés) */
+static int32_t parse(const char *s) {
+  int neg = 0, frac = -1;
+  int64_t v = 0;
+  if (*s == '-') neg = 1, s++;
   for (; *s; s++) {
-    if (*s == '.') scale = scale ? scale : 1;
-    else if (*s >= '0' && *s <= '9') {
-      if (scale) scale /= 10, v += (float)(*s - '0') * scale;
-      else v = v * 10 + (float)(*s - '0');
+    if (*s == '.') {
+      if (frac < 0) frac = 0;
+    } else if (*s >= '0' && *s <= '9') {
+      if (frac >= 2) continue;
+      if (v < MAXV) v = v * 10 + (*s - '0');
+      if (frac >= 0) frac++;
     }
   }
-  return sign * v;
+  if (frac < 0) frac = 0;
+  while (frac < 2) v *= 10, frac++;
+  if (v > MAXV) v = MAXV;
+  return (int32_t)(neg ? -v : v);
 }
 
 /* le code attendu (chiffres seulement) */
@@ -106,88 +101,90 @@ static void code(char out[12]) {
   if (n < 3) strcpy(out, "1234");
 }
 
-static void status_bar(void) {
-  fill(0, 0, SCREEN_W, 18, C_BAR);
-  text("rad", 4, 2, false, C_WHITE, C_BAR);
-  text("TABLEUR", 160 - width("TABLEUR", false) / 2, 2, false, C_WHITE, C_BAR);
-  int level = np_battery_level();
-  fill(296, 5, 15, 8, C_WHITE);
-  fill(297, 6, 13, 6, C_BAR);
-  fill(311, 7, 2, 4, C_WHITE);
-  int bars = level < 0 ? 3 : NP_MIN(level, 3);
-  fill(298, 7, bars * 11 / 3, 4, C_WHITE);
-  fill(0, 18, SCREEN_W, 22, C_WHITE);
-  text("Feuille 1", 160 - width("Feuille 1", false) / 2, 22, false, C_TAB, C_WHITE);
-  fill(0, 39, SCREEN_W, 1, C_LINE);
-}
-
-static void cell(int r, int c) {
-  int vr = r - TB.top;
-  if (vr < 0 || vr >= VIS) return;
-  int x = X0 + c * CW, y = Y0 + RH + vr * RH;
-  bool sel = r == TB.row && c == TB.col;
-  color_t bg = sel ? C_SEL : C_WHITE;
-  fill(x, y, CW - 1, RH - 1, bg);
-  char s[14] = "";
-  if (sel && TB.edit[0]) strcpy(s, TB.edit);
-  else if (TB.set[r][c]) fmt(TB.v[r][c], s);
-  if (s[0]) text(s, x + CW - 5 - width(s, false), y + 4, false, C_TEXT, bg);
-  if (sel && TB.edit[0]) fill(x + CW - 4, y + 3, 1, 15, C_TEXT);
-}
+/* ---- l'écran entier, dessiné par bandes (gfx_render) */
+#define F (&np_font_small)
+static int base(int y, int h) { return y + (h + F->ascent - F->descent) / 2; }
 
-static void footer(void) {
+static void scene(void *ctx) {
+  (void)ctx;
+  /* la barre du haut et l'onglet */
+  gfx_fill(0, 0, SCREEN_W, 18, C_BAR);
+  gfx_text(F, 4, base(0, 18), "rad", C_WHITE, 256);
+  gfx_text_center(F, 160, base(0, 18), "TABLEUR", C_WHITE, 256);
+  gfx_fill(296, 5, 15, 8, C_WHITE); /* la pile (dessinée pleine) */
+  gfx_fill(297, 6, 13, 6, C_BAR);
+  gfx_fill(311, 7, 2, 4, C_WHITE);
+  gfx_fill(298, 7, 11, 4, C_WHITE);
+  gfx_fill(0, 18, SCREEN_W, 21, C_WHITE);
+  gfx_text_center(F, 160, base(18, 21), "Feuille 1", C_TAB, 256);
+  gfx_fill(0, 39, SCREEN_W, 1, C_LINE);
+  /* les en-têtes de colonnes */
+  gfx_fill(0, Y0, SCREEN_W, RH, C_HEAD);
+  for (int c = 0; c < COLS; c++) {
+    char h[2] = {(char)('A' + c), 0};
+    gfx_text_center(F, X0 + c * CW + CW / 2, base(Y0, RH), h, C_GRAY, 256);
+    gfx_fill(X0 + c * CW + CW - 1, Y0, 1, RH + VIS * RH, C_LINE);
+  }
+  gfx_fill(0, Y0 + RH - 1, SCREEN_W, 1, C_LINE);
+  /* les lignes */
+  for (int vr = 0; vr < VIS; vr++) {
+    int y = Y0 + RH + vr * RH, r = TB->top + vr;
+    if (y + RH <= gfx_y0 || y >= gfx_y1) continue;
+    gfx_fill(0, y, X0 - 1, RH - 1, C_HEAD);
+    char n[4];
+    fmt((r + 1) * 100, n);
+    gfx_text_right(F, X0 - 6, base(y, RH), n, C_GRAY, 256);
+    gfx_fill(X0 - 1, y, 1, RH, C_LINE);
+    gfx_fill(0, y + RH - 1, SCREEN_W, 1, C_LINE);
+    for (int c = 0; c < COLS; c++) {
+      int x = X0 + c * CW;
+      bool sel = r == TB->row && c == TB->col;
+      gfx_fill(x, y, CW - 1, RH - 1, sel ? C_SEL : C_WHITE);
+      char s[16] = "";
+      if (sel && TB->edit[0]) strcpy(s, TB->edit);
+      else if (TB->set[r][c]) fmt(TB->v[r][c], s);
+      if (s[0]) gfx_text_right(F, x + CW - 6, base(y, RH), s, C_TEXT, 256);
+      if (sel && TB->edit[0]) gfx_fill(x + CW - 4, y + 3, 1, 15, C_TEXT);
+    }
+  }
+  /* le bas : somme et moyenne de la colonne */
   int y = Y0 + RH + VIS * RH;
-  fill(0, y, SCREEN_W, SCREEN_H - y, C_BG);
-  float s = 0;
+  gfx_fill(0, y, SCREEN_W, SCREEN_H - y, C_BG);
+  int64_t s = 0;
   int n = 0;
   for (int r = 0; r < ROWS; r++)
-    if (TB.set[r][TB.col]) s += TB.v[r][TB.col], n++;
-  char a[16], b[16], l[48];
-  fmt(s, a);
-  fmt(n ? s / (float)n : 0, b);
-  char col[2] = {(char)('A' + TB.col), 0};
+    if (TB->set[r][TB->col]) s += TB->v[r][TB->col], n++;
+  if (s > MAXV) s = MAXV;
+  if (s < -MAXV) s = -MAXV;
+  char a[16], b[16], l[40];
+  fmt((int32_t)s, a);
+  fmt(n ? (int32_t)(s / n) : 0, b);
+  char col[2] = {(char)('A' + TB->col), 0};
   strcpy(l, "Somme(");
   strcat(l, col);
   strcat(l, ") = ");
   strcat(l, a);
-  text(l, 6, y + 6, false, C_TEXT, C_BG);
+  gfx_text(F, 6, base(y, 26), l, C_TEXT, 256);
   strcpy(l, "Moyenne = ");
   strcat(l, b);
-  text(l, SCREEN_W - 6 - width(l, false), y + 6, false, C_TEXT, C_BG);
+  gfx_text_right(F, SCREEN_W - 6, base(y, 26), l, C_TEXT, 256);
 }
 
-static void grid(void) {
-  fill(0, Y0, SCREEN_W, RH, C_HEAD);
-  for (int c = 0; c < COLS; c++) {
-    char h[2] = {(char)('A' + c), 0};
-    text(h, X0 + c * CW + CW / 2 - 3, Y0 + 4, false, C_GRAY, C_HEAD);
-    fill(X0 + c * CW + CW - 1, Y0, 1, RH + VIS * RH, C_LINE);
-  }
-  fill(0, Y0 + RH - 1, SCREEN_W, 1, C_LINE);
-  for (int vr = 0; vr < VIS; vr++) {
-    int y = Y0 + RH + vr * RH;
-    fill(0, y, X0 - 1, RH - 1, C_HEAD);
-    char n[4];
-    int r = TB.top + vr + 1;
-    n[0] = r >= 10 ? (char)('0' + r / 10) : ' ', n[1] = (char)('0' + r % 10), n[2] = 0;
-    text(n, 6, y + 4, false, C_GRAY, C_HEAD);
-    fill(X0 - 1, y, 1, RH, C_LINE);
-    fill(0, y + RH - 1, SCREEN_W, 1, C_LINE);
-    for (int c = 0; c < COLS; c++) cell(TB.top + vr, c);
-  }
-  footer();
-}
+static void draw(void) { gfx_render(scene, NULL, 0, SCREEN_H); }
 
 /* true : le code a été tapé (ouvrir Calcade) ; false : quitter */
 bool np_tableur(void) {
-  memset(&TB, 0, sizeof TB);
+  np_alloc_reset();
+  gfx_init();
+  TB = np_alloc(sizeof(tb_t));
+  if (!gfx_buf || !TB) return true; /* (jamais : l'arena est grande) */
+  memset(TB, 0, sizeof(tb_t));
   char secret[12];
   code(secret);
-  status_bar();
-  grid();
+  draw();
   uint64_t prev = eadk_keyboard_scan();
   uint32_t repeat_at = 0, last_key = np_millis();
-  for (int frame = 0;; frame++) {
+  for (;;) {
     uint64_t k = eadk_keyboard_scan(), hit = k & ~prev;
     uint32_t now = np_millis();
     const uint64_t arrows = 0xF;
@@ -196,52 +193,44 @@ bool np_tableur(void) {
     prev = k;
     if (k) last_key = now;
 #define HIT(key) ((hit >> (key)) & 1)
-    if (HIT(KEY_ONOFF)) {
+    if (HIT(KEY_ONOFF) || (k >> KEY_ONOFF & 1)) {
       np_onoff = true;
       return false;
     }
     if (HIT(KEY_HOME)) return false;
     if (now - last_key > NP_IDLE_QUIT_MS) return false;
-    if (frame % 600 == 0) status_bar();
     if (!hit) {
       np_sleep(16);
       continue;
     }
-    int r = TB.row, c = TB.col;
-    size_t len = strlen(TB.edit);
-    bool commit = HIT(KEY_OK) || HIT(KEY_EXE);
-    if (HIT(KEY_UP) || HIT(KEY_DOWN) || HIT(KEY_LEFT) || HIT(KEY_RIGHT)) commit = true;
-    if (commit && TB.edit[0]) {
-      if (!strcmp(TB.edit, secret)) return true; /* le code : le vrai Calcade */
-      TB.v[r][c] = parse(TB.edit);
-      TB.set[r][c] = 1;
-      TB.edit[0] = 0;
-      if (HIT(KEY_OK) || HIT(KEY_EXE)) r = NP_MIN(r + 1, ROWS - 1);
+    int r = TB->row, c = TB->col;
+    size_t len = strlen(TB->edit);
+    bool enter = HIT(KEY_OK) || HIT(KEY_EXE);
+    bool commit = enter || HIT(KEY_UP) || HIT(KEY_DOWN) || HIT(KEY_LEFT) || HIT(KEY_RIGHT);
+    if (commit && TB->edit[0]) {
+      if (!strcmp(TB->edit, secret)) return true; /* le code : le vrai Calcade */
+      TB->v[r][c] = parse(TB->edit);
+      TB->set[r][c] = 1;
+      TB->edit[0] = 0;
+      len = 0;
+      if (enter) r = NP_MIN(r + 1, ROWS - 1);
     }
     if (HIT(KEY_UP)) r = NP_MAX(r - 1, 0);
     if (HIT(KEY_DOWN)) r = NP_MIN(r + 1, ROWS - 1);
     if (HIT(KEY_LEFT)) c = NP_MAX(c - 1, 0);
     if (HIT(KEY_RIGHT)) c = NP_MIN(c + 1, COLS - 1);
     for (int d = 0; d < 10; d++)
-      if (HIT(digit_keys[d]) && len < 9) TB.edit[len++] = (char)('0' + d), TB.edit[len] = 0;
-    if (HIT(KEY_DOT) && len < 9 && !strchr(TB.edit, '.')) TB.edit[len++] = '.', TB.edit[len] = 0;
-    if (HIT(KEY_MINUS) && len == 0) TB.edit[len++] = '-', TB.edit[len] = 0;
+      if (HIT(digit_keys[d]) && len < 9) TB->edit[len++] = (char)('0' + d), TB->edit[len] = 0;
+    if (HIT(KEY_DOT) && len < 9 && !strchr(TB->edit, '.')) TB->edit[len++] = '.', TB->edit[len] = 0;
+    if (HIT(KEY_MINUS) && len == 0) TB->edit[len++] = '-', TB->edit[len] = 0;
     if (HIT(KEY_BACKSPACE)) {
-      if (len) TB.edit[--len] = 0;
-      else TB.set[r][c] = 0, TB.v[r][c] = 0;
-    }
-    if (HIT(KEY_BACK) && TB.edit[0]) TB.edit[0] = 0;
-    int top = TB.top;
-    if (r < top) top = r;
-    if (r >= top + VIS) top = r - VIS + 1;
-    int or_ = TB.row, oc = TB.col;
-    TB.row = r, TB.col = c;
-    if (top != TB.top || c != oc) {
-      TB.top = top;
-      grid();
-    } else {
-      cell(or_, oc);
-      cell(r, c);
+      if (len) TB->edit[--len] = 0;
+      else TB->set[r][c] = 0, TB->v[r][c] = 0;
     }
+    if (HIT(KEY_BACK)) TB->edit[0] = 0;
+    if (r < TB->top) TB->top = r;
+    if (r >= TB->top + VIS) TB->top = r - VIS + 1;
+    TB->row = r, TB->col = c;
+    draw();
   }
 }
diff --git a/tools/calcade_pack.py b/tools/calcade_pack.py
index 9d177b6..bd20a26 100644
--- a/tools/calcade_pack.py
+++ b/tools/calcade_pack.py
@@ -84,7 +84,7 @@ def main():
     # Calcade, 23 octobre 2026 : VARIANTE=NumPlay-Tableur (1) ou NumPlay-Invisible (2), le cœur déguisé
     variante = os.environ.get("VARIANTE", "NumPlay")
     num_variante = {"NumPlay": 0, "NumPlay-Tableur": 1, "NumPlay-Invisible": 2}[variante]
-    cflags += [f"-DNP_VARIANTE={num_variante}"]
+    # (v70 : plus de -DNP_VARIANTE : le même code pour les trois, seul le nom et l icône changent)
     cflags += ["-std=gnu11", f'-DNP_VERSION="{a.version}"', "-DNP_COEUR=1", "-Os", "-Wall",
                "-Wno-unused-parameter", "-fno-math-errno", "-ffunction-sections", "-fdata-sections",
                "-Ilauncher/src"]
